golang底层 数据结构
字符串
type _string struct { elements *byte // 引用着底层的字节 len int // 字符串中的字节数,获取长度O(1) }
对于字符串比较,编译器有两个优化:
若长度不相等,则字符串不相等,O(1)
若指针相等,长度大的字符串大,O(1)
slice
slice由指针、长度、容量三部分组成
type SliceHeader struct { Data uintptr Len int Cap int }
对 slice 和 array 做 len() 和 cap() 操作,会被直接展开为 sl->len 和 sl->cap 。
slice扩容规则是:
- 如果新的大小是当前大小2倍以上,则大小增长为新大小
- 否则循环以下操作:如果当前大小小于1024,按每次2倍增长,否则每次按当前大小1.25倍增长。直到增长的大小超过或等于新大小。
var x []int
go func(){ x=make([]int, 10) } ()
go func(){ x=make([]int, 10000) } ()
可能会出现 x的指针指向第一个make创建的底层数组,而x的长度为第二个make里的10000
map
map是用哈希表实现的,
for range遍历是随机的,应该是先随机找到一个桶,遍历该桶和溢出桶内的元素,然后按顺序遍历其他的所有桶
加载因子超过0.65或者使用了过多的溢出桶会扩容,当哈希函数选的不好,或者频繁的添加然后删除,就会导致加载因子小,却使用了过多的溢出桶,这种情况下扩容(大概)不会增加哈希表的大小,而是新建相同大小的哈希表,并整理桶内的数据。加载因子超过0.65而引发的扩容会扩大到上次大小的2倍
采取增量扩容,每次添加或删除时,将当前的桶扩展成两个桶,并转移桶内的元素
哈希值对2^B取模,低B位作为buckets数组的index,找到对应的桶。将哈希值的高8位和桶内的tophash数组里的值依次比较,若和tophash[i]相等,则比较第i个key与给定的key是否相等,若相等,返回第i个value。桶内的8个元素都不相同则去overflow里继续寻找。
type hmap struct { count int // 当前元素数量 flags uint8 B uint8 // bucket数组的长度为2^B noverflow uint16 hash0 uint32 // 哈希种子 buckets unsafe.Pointer // bucket数组,数组长度为2^B // 老的buckets,长度为新buckets的一半,只有当正在扩容时才不为空 oldbuckets unsafe.Pointer nevacuate uintptr extra *mapextra } type bmap struct { // 桶,存储8个键值对 tophash [bucketCnt]uint8 // hash值的高8位 bucketCnt默认为8 topbits [8]uint8 keys [8]keytype values [8]valuetype pad uintptr overflow uintptr }
channel
疑问:复制一个channel,是复制了hchan结构体吗?如果是,那么对一个channel操作,另一个hchan里的qcount等属性,是怎么被更新的?
对hchan内部的修改,都要获取lock吗?
目前的 Channel 收发操作均遵循了先入先出(FIFO)的设计,具体规则如下:
- 先从 Channel 读取数据的 Goroutine 会先接收到数据;
- 先向 Channel 发送数据的 Goroutine 会得到先发送数据的权利;
type hchan struct { qcount uint //队列中目前的元素数量 dataqsiz uint //环形队列的总大小,make(chan int, 10) 里面的 10 buf unsafe.Pointer // 指向大小为 dataqsiz 的数组 elemsize uint16 // 元素大小 closed uint32 // 是否已被关闭 elemtype *_type // runtime._type,代表 channel 中的元素类型的 runtime 结构体 sendx uint // send index recvx uint // receive index recvq waitq // 接收 goroutine 对应的 sudog 队列 sendq waitq // 发送 goroutine 对应的 sudog 队列 lock mutex }
recvq和sendq两个链表,一个是因读这个通道而导致阻塞的goroutine,另一个是因为写这个通道而阻塞的goroutine。WaitQ是链表的定义,包含一个头结点和一个尾结点:
type waitq struct { // 等待队列 sudog 双向队列 first *sudog last *sudog }
队列中的每个成员是一个SudoG结构体变量。
struct SudoG { G* g; // g and selgen constitute uint32 selgen; // a weak pointer to g SudoG* link; int64 releasetime; byte* elem; // data element };
该结构中主要的就是一个g和一个elem。elem用于存储goroutine的数据。读通道时,数据会从Hchan的队列中拷贝到SudoG的elem域。写通道时,数据则是由SudoG的elem域拷贝到Hchan的队列中。
写channel
写channel对应runtime.chansend函数。
recvq不为空时,
调用 runtime.sendDirect 函数将发送的数据直接拷贝到 x = <-c 表达式中变量 x 所在的内存地址上;
调用 runtime.goready 将等待接收数据的 Goroutine 标记成可运行状态 Grunnable 并把该 Goroutine 放到发送方所在的P的 runnext 上等待执行,该P在下一次调度时就会立刻唤醒数据的接收方;
recvq为空时,
缓冲区不满时不会阻塞写者,而是将数据放到channel的缓冲区中,调用者返回。
阻塞的情况下,chansend做以下几件事:
- 调用 runtime.getg 获取发送数据使用的 Goroutine;
- 执行 runtime.acquireSudog 函数获取 runtime.sudog 结构体并设置这一次阻塞发送的相关信息,例如发送的 Channel、是否在 Select 控制结构中和待发送数据的内存地址等;
- 将刚刚创建并初始化的 runtime.sudog 加入发送等待队列,并设置到当前 Goroutine 的 waiting 上,表示 Goroutine 正在等待该 sudog 准备就绪;
- 调用 runtime.goparkunlock 函数将当前的 Goroutine 陷入沉睡等待唤醒;
- 被调度器唤醒后会执行一些收尾工作,将一些属性置零并且释放 runtime.sudog 结构体;
读channel
读channel对应runtime.chanrecv函数。
从nil值的channel接收,会调用gopark让出处理器的使用权
如果channel已经关闭,且缓冲区不存在数据,则清除ep指针中的数据并立即返回。ep指针应该指向接收方变量。
当 Channel 的 sendq 队列不为空,调用 runtime.recv 函数:
- 如果 Channel 不存在缓冲区;
- 调用 runtime.recvDirect 函数将 sendq 队列中 Goroutine 存储的 elem 数据拷贝到目标内存地址中;
- 如果 Channel 存在缓冲区;
- 将缓冲区队列头的数据拷贝到接收方的内存地址;
- 将sendq队列头的数据拷贝到缓冲区中,释放一个阻塞的发送方;
无论发生哪种情况,运行时都会调用 runtime.goready 函数将当前处理器的 runnext 设置成发送数据的 Goroutine,在调度器下一次调度时将阻塞的发送方唤醒。
sendq队列为空,缓冲区不为空时,直接获取缓冲区内的数据
sendq队列为空,且缓冲区无数据或不存在缓冲区时,接收方会阻塞,并使用 runtime.sudog 结构体将当前 g 包装成一个处于等待状态的 g 并将其加入到接收队列中。然后调用 runtime.goparkunlock 函数触发 Goroutine 的调度,让出处理器的使用权
close
close channel时,会锁channel,然后将阻塞在channel上的g添加到一个gList上,然后释放锁,最后唤醒所有reader和writer。唤醒的reader会返回零值,唤醒的writer会panic?
接口
接口是一个结构体,包含两个成员:类型,和指向数据的指针
type eface struct { // 不包含方法的接口 _type *_type // 动态类型 data unsafe.Pointer // 接口所指向的具体类型值的地址 } type _type struct { size uintptr // 类型大小 kind uint8 // 所代表的具体类型 hash uint32 // 类型的哈希,可快速判断类型是否相等 ... } type iface struct { // 包含方法的接口 tab *itab // 包含接口的静态类型信息、数据的动态类型信息、函数表的结构 data unsafe.Pointer // 接口所指向的具体类型值的地址 } type itab struct { inter *interfacetype // 接口类型 _type *_type // 动态类型 hash uint32 // _type.hash 的 copy,用于类型的判断 _ [4]byte fun [1]uintptr // 可变大小,func[0]==0 意味着 _type 没有实现相关接口函数 }
类型断言会比较itab里的hash和目标类型_type里的hash,hash相同则是同一个类型
接口的方法调用
对象的方法调用,等价于普通函数调用,函数地址是在编译时就可以确定的。而接口的方法调用,函数地址要在运行时才能确定。将具体值赋值给接口时,会将Type中的方法表复制到接口的方法表中,然后接口方法的函数地址才会确定下来。因此,接口的方法调用的代价比普通函数调用和对象的方法调用略高,多了几条指令。
将具体类型转换为空接口类型,过程比较简单,就是返回一个Eface,将Eface中的data指针指向原型数据,type指针会指向数据的Type结构体。
将具体类型转换为带方法的接口时,会在编译期比较具体类型的方法表和接口类型的方法表,这两处方法表都是排序过的,只需要一遍顺序扫描,就可以知道Type中否实现了接口中声明的所有方法。最后会将Type方法表中的函数指针,拷贝到Itab的fun字段中。
这里提到了三个方法表,有点容易把人搞晕,所以要解释一下。
Type的UncommonType中有一个Method方法表,某个具体类型实现的所有方法都会被收集到这张表中。reflect包中的Method和MethodByName方法都是通过查询这张表实现的。
Iface的Itab的InterfaceType中也有一张方法表,这张方法表中是接口所声明的方法。其中每一项是一个IMethod,里面只有声明没有实现。
Iface中的Itab的func域也是一张方法表,这张表中的每一项就是一个函数指针,也就是只有实现没有声明。
类型转换时的检测就是看Type中的方法表是否包含了InterfaceType的方法表中的所有方法,并把Type方法表中的实现部分拷到Itab的func那张表中。
reflect
reflect就是给定一个接口类型的数据,得到它的具体类型的类型信息,它的Value等。reflect包中的TypeOf和ValueOf函数分别做这个事情。