重庆分公司,新征程启航
为企业提供网站建设、域名注册、服务器等服务
Gorm是Go语言开发用的比较多的一个ORM。它的功能比较全:
会泽网站建设公司创新互联,会泽网站设计制作,有大型网站制作公司丰富经验。已为会泽近1000家提供企业网站建设服务。企业网站搭建\外贸网站建设要多少钱,请找那个售后服务好的会泽做网站的公司定做!
但是这篇文章中并不会直接看Gorm的源码,我们会先从database/sql分析。原因是Gorm也是基于这个包来封装的一些功能。所以只有先了解了database/sql包才能更加好的理解Gorm源码。
database/sql 其实也是一个对于mysql驱动的上层封装。”github.com/go-sql-driver/mysql”就是一个对于mysql的驱动,database/sql 就是在这个基础上做的基本封装包含连接池的使用
下面这个是最基本的增删改查操作
操作分下面几个步骤:
因为Gorm的连接池就是使用database/sql包中的连接池,所以这里我们需要学习一下包里的连接池的源码实现。其实所有连接池最重要的就是连接池对象、获取函数、释放函数下面来看一下database/sql中的连接池。
DB对象
获取方法
释放连接方法
连接池的实现有很多方法,在database/sql包中使用的是chan阻塞 使用map记录等待列表,等到有连接释放的时候再把连接传入等待列表中的chan 不在阻塞返回连接。
之前我们看到的Redigo是使用一个chan 来阻塞,然后释放的时候放入空闲列表,在往这一个chan中传入struct{}{},让程序继续 获取的时候再从空闲列表中获取。并且使用的是链表的结构来存储空闲列表。
database/sql 是对于mysql驱动的封装,然而Gorm则是对于database/sql的再次封装。让我们可以更加简单的实现对于mysql数据库的操作。
golang 中 map的实现结构为: 哈希表 + 链表。 其中链表,作用是当发生hash冲突时,拉链法生成的结点。
可以看到, []bmap 是一个hash table, 每一个 bmap是我们常说的“桶”。 经过hash 函数计算出来相同的hash值, 放到相同的桶中。 一个 bmap中可以存放 8个 元素, 如果多出8个,则生成新的结点,尾接到队尾。
以上是只是静态文件 src/runtime/map.go 中的定义。 实际上编译期间会给它加料 ,动态地创建一个新的结构:
上图就是 bmap的内存模型, HOB Hash 指的就是 top hash。 注意到 key 和 value 是各自放在一起的,并不是 key/value/key/value/... 这样的形式。源码里说明这样的好处是在某些情况下可以省略掉 padding 字段,节省内存空间。
每个 bmap设计成 最多只能放 8 个 key-value 对 ,如果有第 9 个 key-value 落入当前的 bmap,那就需要再构建一个 bmap,通过 overflow 指针连接起来。
map创建方法:
我们实际上是通过调用的 makemap ,来创建map的。实际工作只是初始化了hmap中的各种字段,如:设置B的大小, 设置hash 种子 hash 0.
注意 :
makemap 返回是*hmap 指针, 即 map 是引用对象, 对map的操作会影响到结构体内部 。
使用方式
对应的是下面两种方法
map的key的类型,实现了自己的hash 方式。每种类型实现hash函数方式不一样。
key 经过哈希计算后得到hash值,共 64 个 bit 位。 其中后B 个bit位置, 用来定位当前元素落在哪一个桶里, 高8个bit 为当前 hash 值的top hash。 实际上定位key的过程是一个双重循环的过程, 外层循环遍历 所有的overflow, 内层循环遍历 当前bmap 中的 8个元素 。
举例说明: 如果当前 B 的值为 5, 那么buckets 的长度 为 2^5 = 32。假设有个key 经过hash函数计算后,得到的hash结果为:
外层遍历bucket 中的链表
内层循环遍历 bmap中的8个 cell
建议先不看此部分内容,看完后续 修改 map中元素 - 扩容 操作后 再回头看此部分内容。
扩容前的数据:
等量扩容后的数据:
等量扩容后,查找方式和原本相同, 不多做赘述。
两倍扩容后的数据
两倍扩容后,oldbuckets 的元素,可能被分配成了两部分。查找顺序如下:
此处只分析 mapaccess1 ,。 mapaccess2 相比 mapaccess1 多添加了是否找到的bool值, 有兴趣可自行看一下。
使用方式:
步骤如下:
扩容条件 :
扩容的标识 : h.oldbuckets != nil
假设当前定位到了新的buckets的3号桶中,首先会判断oldbuckets中的对应的桶有没有被搬迁过。 如果搬迁过了,不需要看原来的桶了,直接遍历新的buckets的3号桶。
扩容前:
等量扩容结果
双倍扩容会将old buckets上的元素分配到x, y两个部key 1 B == 0 分配到x部分,key 1 B == 1 分配到y部分
注意: 当前只对双倍扩容描述, 等量扩容只是重新填充了一下元素, 相对位置没有改变。
假设当前map 的B == 5,原本元素经过hash函数计算的 hash 值为:
因为双倍扩容之后 B = B + 1,此时B == 6。key 1 B == 1, 即 当前元素rehash到高位,新buckets中 y 部分. 否则 key 1 B == 0 则rehash到低位,即x 部分。
使用方式:
可以看到,每一遍历生成迭代器的时候,会随机选取一个bucket 以及 一个cell开始。 从前往后遍历,再次遍历到起始位置时,遍历完成。
2021-11-10
列表是一种非连续的存储容器,有多个节点组成,节点通过一些变量记录彼此之间的关系
单链表和双链表就是列表的两种方法。
原理:A、B、C三个人,B懂A的电话,C懂B的电话只是单方知道号码,这样就形成了一个单链表结构。
如果C把自己的号码给B,B把自己的号码给A,因为是双方都知道对方的号码,这样就形成了一个双链表结构
如果B换号码了,他需要通知AC,把自己的号码删了,这个过程就是列表的删除操作。
在Go语言中,列表使用 container/list 包来实现,内部的实现原理是双链表,列表能够高效地进行任意位置的元素插入和删除操作。
列表初始化的两种办法
列表没有给出具体的元素类型的限制,所以列表的元素可以是任意类型的,
例如给列表中放入了一个 interface{} 类型的值,取出值后,如果要将 interface{} 转换为其他类型将会发生宕机。
双链表支持从队列前方或后方插入元素,分别对应的方法是 PushFront 和 PushBack。
列表插入函数的返回值会提供一个 *list.Element 结构,这个结构记录着列表元素的值以及与其他节点之间的关系等信息,从列表中删除元素时,需要用到这个结构进行快速删除。
遍历完也能看到最后的结果
学习地址:
本文目录如下,阅读本文后,将一网打尽下面Golang Map相关面试题
Go中的map是一个指针,占用8个字节,指向hmap结构体; 源码 src/runtime/map.go 中可以看到map的底层结构
每个map的底层结构是hmap,hmap包含若干个结构为bmap的bucket数组。每个bucket底层都采用链表结构。接下来,我们来详细看下map的结构
bmap 就是我们常说的“桶”,一个桶里面会最多装 8 个 key,这些 key 之所以会落入同一个桶,是因为它们经过哈希计算后,哈希结果是“一类”的,关于key的定位我们在map的查询和插入中详细说明。在桶内,又会根据 key 计算出来的 hash 值的高 8 位来决定 key 到底落入桶内的哪个位置(一个桶内最多有8个位置)。
bucket内存数据结构可视化如下:
注意到 key 和 value 是各自放在一起的,并不是 key/value/key/value/... 这样的形式。源码里说明这样的好处是在某些情况下可以省略掉 padding字段,节省内存空间。
当 map 的 key 和 value 都不是指针,并且 size 都小于 128 字节的情况下,会把 bmap 标记为不含指针,这样可以避免 gc 时扫描整个 hmap。但是,我们看 bmap 其实有一个 overflow 的字段,是指针类型的,破坏了 bmap 不含指针的设想,这时会把 overflow 移动到 extra 字段来。
map是个指针,底层指向hmap,所以是个引用类型
golang 有三个常用的高级类型 slice 、map、channel, 它们都是 引用类型 ,当引用类型作为函数参数时,可能会修改原内容数据。
golang 中没有引用传递,只有值和指针传递。所以 map 作为函数实参传递时本质上也是值传递,只不过因为 map 底层数据结构是通过指针指向实际的元素存储空间,在被调函数中修改 map,对调用者同样可见,所以 map 作为函数实参传递时表现出了引用传递的效果。
因此,传递 map 时,如果想修改map的内容而不是map本身,函数形参无需使用指针
map 底层数据结构是通过指针指向实际的元素 存储空间 ,这种情况下,对其中一个map的更改,会影响到其他map
map 在没有被修改的情况下,使用 range 多次遍历 map 时输出的 key 和 value 的顺序可能不同。这是 Go 语言的设计者们有意为之,在每次 range 时的顺序被随机化,旨在提示开发者们,Go 底层实现并不保证 map 遍历顺序稳定,请大家不要依赖 range 遍历结果顺序。
map 本身是无序的,且遍历时顺序还会被随机化,如果想顺序遍历 map,需要对 map key 先排序,再按照 key 的顺序遍历 map。
map默认是并发不安全的,原因如下:
Go 官方在经过了长时间的讨论后,认为 Go map 更应适配典型使用场景(不需要从多个 goroutine 中进行安全访问),而不是为了小部分情况(并发访问),导致大部分程序付出加锁代价(性能),决定了不支持。
场景: 2个协程同时读和写,以下程序会出现致命错误:fatal error: concurrent map writes
如果想实现map线程安全,有两种方式:
方式一:使用读写锁 map + sync.RWMutex
方式二:使用golang提供的 sync.Map
sync.map是用读写分离实现的,其思想是空间换时间。和map+RWLock的实现方式相比,它做了一些优化:可以无锁访问read map,而且会优先操作read map,倘若只操作read map就可以满足要求(增删改查遍历),那就不用去操作write map(它的读写都要加锁),所以在某些特定场景中它发生锁竞争的频率会远远小于map+RWLock的实现方式。
golang中map是一个kv对集合。底层使用hash table,用链表来解决冲突 ,出现冲突时,不是每一个key都申请一个结构通过链表串起来,而是以bmap为最小粒度挂载,一个bmap可以放8个kv。在哈希函数的选择上,会在程序启动时,检测 cpu 是否支持 aes,如果支持,则使用 aes hash,否则使用 memhash。
map有3钟初始化方式,一般通过make方式创建
map的创建通过生成汇编码可以知道,make创建map时调用的底层函数是 runtime.makemap 。如果你的map初始容量小于等于8会发现走的是 runtime.fastrand 是因为容量小于8时不需要生成多个桶,一个桶的容量就可以满足
makemap函数会通过 fastrand 创建一个随机的哈希种子,然后根据传入的 hint 计算出需要的最小需要的桶的数量,最后再使用 makeBucketArray 创建用于保存桶的数组,这个方法其实就是根据传入的 B 计算出的需要创建的桶数量在内存中分配一片连续的空间用于存储数据,在创建桶的过程中还会额外创建一些用于保存溢出数据的桶,数量是 2^(B-4) 个。初始化完成返回hmap指针。
找到一个 B,使得 map 的装载因子在正常范围内
Go 语言中读取 map 有两种语法:带 comma 和 不带 comma。当要查询的 key 不在 map 里,带 comma 的用法会返回一个 bool 型变量提示 key 是否在 map 中;而不带 comma 的语句则会返回一个 value 类型的零值。如果 value 是 int 型就会返回 0,如果 value 是 string 类型,就会返回空字符串。
map的查找通过生成汇编码可以知道,根据 key 的不同类型,编译器会将查找函数用更具体的函数替换,以优化效率:
函数首先会检查 map 的标志位 flags。如果 flags 的写标志位此时被置 1 了,说明有其他协程在执行“写”操作,进而导致程序 panic。这也说明了 map 对协程是不安全的。
key经过哈希函数计算后,得到的哈希值如下(主流64位机下共 64 个 bit 位):
m: 桶的个数
从buckets 通过 hash m 得到对应的bucket,如果bucket正在扩容,并且没有扩容完成,则从oldbuckets得到对应的bucket
计算hash所在桶编号:
用上一步哈希值最后的 5 个 bit 位,也就是 01010 ,值为 10,也就是 10 号桶(范围是0~31号桶)
计算hash所在的槽位:
用上一步哈希值哈希值的高8个bit 位,也就是 10010111 ,转化为十进制,也就是151,在 10 号 bucket 中寻找** tophash 值(HOB hash)为 151* 的 槽位**,即为key所在位置,找到了 2 号槽位,这样整个查找过程就结束了。
如果在 bucket 中没找到,并且 overflow 不为空,还要继续去 overflow bucket 中寻找,直到找到或是所有的 key 槽位都找遍了,包括所有的 overflow bucket。
通过上面找到了对应的槽位,这里我们再详细分析下key/value值是如何获取的:
bucket 里 key 的起始地址就是 unsafe.Pointer(b)+dataOffset。第 i 个 key 的地址就要在此基础上跨过 i 个 key 的大小;而我们又知道,value 的地址是在所有 key 之后,因此第 i 个 value 的地址还需要加上所有 key 的偏移。
通过汇编语言可以看到,向 map 中插入或者修改 key,最终调用的是 mapassign 函数。
实际上插入或修改 key 的语法是一样的,只不过前者操作的 key 在 map 中不存在,而后者操作的 key 存在 map 中。
mapassign 有一个系列的函数,根据 key 类型的不同,编译器会将其优化为相应的“快速函数”。
我们只用研究最一般的赋值函数 mapassign 。
map的赋值会附带着map的扩容和迁移,map的扩容只是将底层数组扩大了一倍,并没有进行数据的转移,数据的转移是在扩容后逐步进行的,在迁移的过程中每进行一次赋值(access或者delete)会至少做一次迁移工作。
1.判断map是否为nil
每一次进行赋值/删除操作时,只要oldbuckets != nil 则认为正在扩容,会做一次迁移工作,下面会详细说下迁移过程
根据上面查找过程,查找key所在位置,如果找到则更新,没找到则找空位插入即可
经过前面迭代寻找动作,若没有找到可插入的位置,意味着需要扩容进行插入,下面会详细说下扩容过程
通过汇编语言可以看到,向 map 中删除 key,最终调用的是 mapdelete 函数
删除的逻辑相对比较简单,大多函数在赋值操作中已经用到过,核心还是找到 key 的具体位置。寻找过程都是类似的,在 bucket 中挨个 cell 寻找。找到对应位置后,对 key 或者 value 进行“清零”操作,将 count 值减 1,将对应位置的 tophash 值置成 Empty
再来说触发 map 扩容的时机:在向 map 插入新 key 的时候,会进行条件检测,符合下面这 2 个条件,就会触发扩容:
1、装载因子超过阈值
源码里定义的阈值是 6.5 (loadFactorNum/loadFactorDen),是经过测试后取出的一个比较合理的因子
我们知道,每个 bucket 有 8 个空位,在没有溢出,且所有的桶都装满了的情况下,装载因子算出来的结果是 8。因此当装载因子超过 6.5 时,表明很多 bucket 都快要装满了,查找效率和插入效率都变低了。在这个时候进行扩容是有必要的。
对于条件 1,元素太多,而 bucket 数量太少,很简单:将 B 加 1,bucket 最大数量( 2^B )直接变成原来 bucket 数量的 2 倍。于是,就有新老 bucket 了。注意,这时候元素都在老 bucket 里,还没迁移到新的 bucket 来。新 bucket 只是最大数量变为原来最大数量的 2 倍( 2^B * 2 ) 。
2、overflow 的 bucket 数量过多
在装载因子比较小的情况下,这时候 map 的查找和插入效率也很低,而第 1 点识别不出来这种情况。表面现象就是计算装载因子的分子比较小,即 map 里元素总数少,但是 bucket 数量多(真实分配的 bucket 数量多,包括大量的 overflow bucket)
不难想像造成这种情况的原因:不停地插入、删除元素。先插入很多元素,导致创建了很多 bucket,但是装载因子达不到第 1 点的临界值,未触发扩容来缓解这种情况。之后,删除元素降低元素总数量,再插入很多元素,导致创建很多的 overflow bucket,但就是不会触发第 1 点的规定,你能拿我怎么办?overflow bucket 数量太多,导致 key 会很分散,查找插入效率低得吓人,因此出台第 2 点规定。这就像是一座空城,房子很多,但是住户很少,都分散了,找起人来很困难
对于条件 2,其实元素没那么多,但是 overflow bucket 数特别多,说明很多 bucket 都没装满。解决办法就是开辟一个新 bucket 空间,将老 bucket 中的元素移动到新 bucket,使得同一个 bucket 中的 key 排列地更紧密。这样,原来,在 overflow bucket 中的 key 可以移动到 bucket 中来。结果是节省空间,提高 bucket 利用率,map 的查找和插入效率自然就会提升。
由于 map 扩容需要将原有的 key/value 重新搬迁到新的内存地址,如果有大量的 key/value 需要搬迁,会非常影响性能。因此 Go map 的扩容采取了一种称为“渐进式”的方式,原有的 key 并不会一次性搬迁完毕,每次最多只会搬迁 2 个 bucket。
上面说的 hashGrow() 函数实际上并没有真正地“搬迁”,它只是分配好了新的 buckets,并将老的 buckets 挂到了 oldbuckets 字段上。真正搬迁 buckets 的动作在 growWork() 函数中,而调用 growWork() 函数的动作是在 mapassign 和 mapdelete 函数中。也就是插入或修改、删除 key 的时候,都会尝试进行搬迁 buckets 的工作。先检查 oldbuckets 是否搬迁完毕,具体来说就是检查 oldbuckets 是否为 nil。
如果未迁移完毕,赋值/删除的时候,扩容完毕后(预分配内存),不会马上就进行迁移。而是采取 增量扩容 的方式,当有访问到具体 bukcet 时,才会逐渐的进行迁移(将 oldbucket 迁移到 bucket)
nevacuate 标识的是当前的进度,如果都搬迁完,应该和2^B的长度是一样的
在evacuate 方法实现是把这个位置对应的bucket,以及其冲突链上的数据都转移到新的buckets上。
转移的判断直接通过tophash 就可以,判断tophash中第一个hash值即可
遍历的过程,就是按顺序遍历 bucket,同时按顺序遍历 bucket 中的 key。
map遍历是无序的,如果想实现有序遍历,可以先对key进行排序
为什么遍历 map 是无序的?
如果发生过迁移,key 的位置发生了重大的变化,有些 key 飞上高枝,有些 key 则原地不动。这样,遍历 map 的结果就不可能按原来的顺序了。
如果就一个写死的 map,不会向 map 进行插入删除的操作,按理说每次遍历这样的 map 都会返回一个固定顺序的 key/value 序列吧。但是 Go 杜绝了这种做法,因为这样会给新手程序员带来误解,以为这是一定会发生的事情,在某些情况下,可能会酿成大错。
Go 做得更绝,当我们在遍历 map 时,并不是固定地从 0 号 bucket 开始遍历,每次都是从一个**随机值序号的 bucket 开始遍历,并且是从这个 bucket 的一个 随机序号的 cell **开始遍历。这样,即使你是一个写死的 map,仅仅只是遍历它,也不太可能会返回一个固定序列的 key/value 对了。
Goroutine调度是一个很复杂的机制,下面尝试用简单的语言描述一下Goroutine调度机制,想要对其有更深入的了解可以去研读一下源码。
首先介绍一下GMP什么意思:
G ----------- goroutine: 即Go协程,每个go关键字都会创建一个协程。
M ---------- thread内核级线程,所有的G都要放在M上才能运行。
P ----------- processor处理器,调度G到M上,其维护了一个队列,存储了所有需要它来调度的G。
Goroutine 调度器P和 OS 调度器是通过 M 结合起来的,每个 M 都代表了 1 个内核线程,OS 调度器负责把内核线程分配到 CPU 的核上执行
模型图:
避免频繁的创建、销毁线程,而是对线程的复用。
1)work stealing机制
当本线程无可运行的G时,尝试从其他线程绑定的P偷取G,而不是销毁线程。
2)hand off机制
当本线程M0因为G0进行系统调用阻塞时,线程释放绑定的P,把P转移给其他空闲的线程执行。进而某个空闲的M1获取P,继续执行P队列中剩下的G。而M0由于陷入系统调用而进被阻塞,M1接替M0的工作,只要P不空闲,就可以保证充分利用CPU。M1的来源有可能是M的缓存池,也可能是新建的。当G0系统调用结束后,根据M0是否能获取到P,将会将G0做不同的处理:
如果有空闲的P,则获取一个P,继续执行G0。
如果没有空闲的P,则将G0放入全局队列,等待被其他的P调度。然后M0将进入缓存池睡眠。
如下图
GOMAXPROCS设置P的数量,最多有GOMAXPROCS个线程分布在多个CPU上同时运行
在Go中一个goroutine最多占用CPU 10ms,防止其他goroutine被饿死。
具体可以去看另一篇文章
【Golang详解】go语言调度机制 抢占式调度
当创建一个新的G之后优先加入本地队列,如果本地队列满了,会将本地队列的G移动到全局队列里面,当M执行work stealing从其他P偷不到G时,它可以从全局G队列获取G。
协程经历过程
我们创建一个协程 go func()经历过程如下图:
说明:
这里有两个存储G的队列,一个是局部调度器P的本地队列、一个是全局G队列。新创建的G会先保存在P的本地队列中,如果P的本地队列已经满了就会保存在全局的队列中;处理器本地队列是一个使用数组构成的环形链表,它最多可以存储 256 个待执行任务。
G只能运行在M中,一个M必须持有一个P,M与P是1:1的关系。M会从P的本地队列弹出一个可执行状态的G来执行,如果P的本地队列为空,就会想其他的MP组合偷取一个可执行的G来执行;
一个M调度G执行的过程是一个循环机制;会一直从本地队列或全局队列中获取G
上面说到P的个数默认等于CPU核数,每个M必须持有一个P才可以执行G,一般情况下M的个数会略大于P的个数,这多出来的M将会在G产生系统调用时发挥作用。类似线程池,Go也提供一个M的池子,需要时从池子中获取,用完放回池子,不够用时就再创建一个。
work-stealing调度算法:当M执行完了当前P的本地队列队列里的所有G后,P也不会就这么在那躺尸啥都不干,它会先尝试从全局队列队列寻找G来执行,如果全局队列为空,它会随机挑选另外一个P,从它的队列里中拿走一半的G到自己的队列中执行。
如果一切正常,调度器会以上述的那种方式顺畅地运行,但这个世界没这么美好,总有意外发生,以下分析goroutine在两种例外情况下的行为。
Go runtime会在下面的goroutine被阻塞的情况下运行另外一个goroutine:
用户态阻塞/唤醒
当goroutine因为channel操作或者network I/O而阻塞时(实际上golang已经用netpoller实现了goroutine网络I/O阻塞不会导致M被阻塞,仅阻塞G,这里仅仅是举个栗子),对应的G会被放置到某个wait队列(如channel的waitq),该G的状态由_Gruning变为_Gwaitting,而M会跳过该G尝试获取并执行下一个G,如果此时没有可运行的G供M运行,那么M将解绑P,并进入sleep状态;当阻塞的G被另一端的G2唤醒时(比如channel的可读/写通知),G被标记为,尝试加入G2所在P的runnext(runnext是线程下一个需要执行的 Goroutine。), 然后再是P的本地队列和全局队列。
系统调用阻塞
当M执行某一个G时候如果发生了阻塞操作,M会阻塞,如果当前有一些G在执行,调度器会把这个线程M从P中摘除,然后再创建一个新的操作系统的线程(如果有空闲的线程可用就复用空闲线程)来服务于这个P。当M系统调用结束时候,这个G会尝试获取一个空闲的P执行,并放入到这个P的本地队列。如果获取不到P,那么这个线程M变成休眠状态, 加入到空闲线程中,然后这个G会被放入全局队列中。
队列轮转
可见每个P维护着一个包含G的队列,不考虑G进入系统调用或IO操作的情况下,P周期性的将G调度到M中执行,执行一小段时间,将上下文保存下来,然后将G放到队列尾部,然后从队列中重新取出一个G进行调度。
除了每个P维护的G队列以外,还有一个全局的队列,每个P会周期性地查看全局队列中是否有G待运行并将其调度到M中执行,全局队列中G的来源,主要有从系统调用中恢复的G。之所以P会周期性地查看全局队列,也是为了防止全局队列中的G被饿死。
除了每个P维护的G队列以外,还有一个全局的队列,每个P会周期性地查看全局队列中是否有G待运行并将其调度到M中执行,全局队列中G的来源,主要有从系统调用中恢复的G。之所以P会周期性地查看全局队列,也是为了防止全局队列中的G被饿死。
M0
M0是启动程序后的编号为0的主线程,这个M对应的实例会在全局变量rutime.m0中,不需要在heap上分配,M0负责执行初始化操作和启动第一个G,在之后M0就和其他的M一样了
G0
G0是每次启动一个M都会第一个创建的goroutine,G0仅用于负责调度G,G0不指向任何可执行的函数,每个M都会有一个自己的G0,在调度或系统调用时会使用G0的栈空间,全局变量的G0是M0的G0
一个G由于调度被中断,此后如何恢复?
中断的时候将寄存器里的栈信息,保存到自己的G对象里面。当再次轮到自己执行时,将自己保存的栈信息复制到寄存器里面,这样就接着上次之后运行了。
我这里只是根据自己的理解进行了简单的介绍,想要详细了解有关GMP的底层原理可以去看Go调度器 G-P-M 模型的设计者的文档或直接看源码
参考: ()
()
Go语言中没有“类”的概念,也不支持“类”的继承等面向对象的概念。Go语言中通过结构体的内嵌再配合接口比面向对象具有更高的扩展性和灵活性。
自定义类型
在Go语言中有一些基本的数据类型,如string、整型、浮点型、布尔等数据类型, Go语言中可以使用type关键字来定义自定义类型。
自定义类型是定义了一个全新的类型。我们可以基于内置的基本类型定义,也可以通过struct定义。例如:
通过Type关键字的定义,MyInt就是一种新的类型,它具有int的特性。
类型别名
类型别名是Go1.9版本添加的新功能。
类型别名规定:TypeAlias只是Type的别名,本质上TypeAlias与Type是同一个类型。就像一个孩子小时候有小名、乳名,上学后用学名,英语老师又会给他起英文名,但这些名字都指的是他本人。
type TypeAlias = Type
我们之前见过的rune和byte就是类型别名,他们的定义如下:
类型定义和类型别名的区别
类型别名与类型定义表面上看只有一个等号的差异,我们通过下面的这段代码来理解它们之间的区别。
结果显示a的类型是main.NewInt,表示main包下定义的NewInt类型。b的类型是int。MyInt类型只会在代码中存在,编译完成时并不会有MyInt类型。
Go语言中的基础数据类型可以表示一些事物的基本属性,但是当我们想表达一个事物的全部或部分属性时,这时候再用单一的基本数据类型明显就无法满足需求了,Go语言提供了一种自定义数据类型,可以封装多个基本数据类型,这种数据类型叫结构体,英文名称struct。 也就是我们可以通过struct来定义自己的类型了。
Go语言中通过struct来实现面向对象。
结构体的定义
使用type和struct关键字来定义结构体,具体代码格式如下:
其中:
举个例子,我们定义一个Person(人)结构体,代码如下:
同样类型的字段也可以写在一行,
这样我们就拥有了一个person的自定义类型,它有name、city、age三个字段,分别表示姓名、城市和年龄。这样我们使用这个person结构体就能够很方便的在程序中表示和存储人信息了。
语言内置的基础数据类型是用来描述一个值的,而结构体是用来描述一组值的。比如一个人有名字、年龄和居住城市等,本质上是一种聚合型的数据类型
结构体实例化
只有当结构体实例化时,才会真正地分配内存。也就是必须实例化后才能使用结构体的字段。
基本实例化
举个例子:
我们通过.来访问结构体的字段(成员变量),例如p1.name和p1.age等。
匿名结构体
在定义一些临时数据结构等场景下还可以使用匿名结构体。
创建指针类型结构体
我们还可以通过使用new关键字对结构体进行实例化,得到的是结构体的地址。 格式如下:
从打印的结果中我们可以看出p2是一个结构体指针。
需要注意的是在Go语言中支持对结构体指针直接使用.来访问结构体的成员。
取结构体的地址实例化
使用对结构体进行取地址操作相当于对该结构体类型进行了一次new实例化操作。
p3.name = "七米"其实在底层是(*p3).name = "七米",这是Go语言帮我们实现的语法糖。
结构体初始化
没有初始化的结构体,其成员变量都是对应其类型的零值。
使用键值对初始化
使用键值对对结构体进行初始化时,键对应结构体的字段,值对应该字段的初始值。
也可以对结构体指针进行键值对初始化,例如:
当某些字段没有初始值的时候,该字段可以不写。此时,没有指定初始值的字段的值就是该字段类型的零值。
使用值的列表初始化
初始化结构体的时候可以简写,也就是初始化的时候不写键,直接写值:
使用这种格式初始化时,需要注意:
结构体内存布局
结构体占用一块连续的内存。
输出:
【进阶知识点】关于Go语言中的内存对齐推荐阅读:在 Go 中恰到好处的内存对齐
面试题
请问下面代码的执行结果是什么?
构造函数
Go语言的结构体没有构造函数,我们可以自己实现。 例如,下方的代码就实现了一个person的构造函数。 因为struct是值类型,如果结构体比较复杂的话,值拷贝性能开销会比较大,所以该构造函数返回的是结构体指针类型。
调用构造函数
方法和接收者
Go语言中的方法(Method)是一种作用于特定类型变量的函数。这种特定类型变量叫做接收者(Receiver)。接收者的概念就类似于其他语言中的this或者 self。
方法的定义格式如下:
其中,
举个例子:
方法与函数的区别是,函数不属于任何类型,方法属于特定的类型。
指针类型的接收者
指针类型的接收者由一个结构体的指针组成,由于指针的特性,调用方法时修改接收者指针的任意成员变量,在方法结束后,修改都是有效的。这种方式就十分接近于其他语言中面向对象中的this或者self。 例如我们为Person添加一个SetAge方法,来修改实例变量的年龄。
调用该方法:
值类型的接收者
当方法作用于值类型接收者时,Go语言会在代码运行时将接收者的值复制一份。在值类型接收者的方法中可以获取接收者的成员值,但修改操作只是针对副本,无法修改接收者变量本身。
什么时候应该使用指针类型接收者
任意类型添加方法
在Go语言中,接收者的类型可以是任何类型,不仅仅是结构体,任何类型都可以拥有方法。 举个例子,我们基于内置的int类型使用type关键字可以定义新的自定义类型,然后为我们的自定义类型添加方法。
注意事项: 非本地类型不能定义方法,也就是说我们不能给别的包的类型定义方法。
结构体的匿名字段
匿名字段默认采用类型名作为字段名,结构体要求字段名称必须唯一,因此一个结构体中同种类型的匿名字段只能有一个。
嵌套结构体
一个结构体中可以嵌套包含另一个结构体或结构体指针。
嵌套匿名结构体
当访问结构体成员时会先在结构体中查找该字段,找不到再去匿名结构体中查找。
嵌套结构体的字段名冲突
嵌套结构体内部可能存在相同的字段名。这个时候为了避免歧义需要指定具体的内嵌结构体的字段。
结构体的“继承”
Go语言中使用结构体也可以实现其他编程语言中面向对象的继承。
结构体字段的可见性
结构体中字段大写开头表示可公开访问,小写表示私有(仅在定义当前结构体的包中可访问)。
结构体与JSON序列化
JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。JSON键值对是用来保存JS对象的一种方式,键/值对组合中的键名写在前面并用双引号""包裹,使用冒号:分隔,然后紧接着值;多个键值之间使用英文,分隔。
结构体标签(Tag)
Tag是结构体的元信息,可以在运行的时候通过反射的机制读取出来。 Tag在结构体字段的后方定义,由一对反引号包裹起来,具体的格式如下:
`key1:"value1" key2:"value2"`
结构体标签由一个或多个键值对组成。键与值使用冒号分隔,值用双引号括起来。键值对之间使用一个空格分隔。 注意事项: 为结构体编写Tag时,必须严格遵守键值对的规则。结构体标签的解析代码的容错能力很差,一旦格式写错,编译和运行时都不会提示任何错误,通过反射也无法正确取值。例如不要在key和value之间添加空格。
例如我们为Student结构体的每个字段定义json序列化时使用的Tag: