字符串

 

对于字符串比较,编译器有两个优化:

若长度不相等,则字符串不相等,O(1)

若指针相等,长度大的字符串大,O(1)

 

 

slice

slice由指针、长度、容量三部分组成

对 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里继续寻找。

 

 

 

channel

疑问:复制一个channel,是复制了hchan结构体吗?如果是,那么对一个channel操作,另一个hchan里的qcount等属性,是怎么被更新的?

对hchan内部的修改,都要获取lock吗?

 

目前的 Channel 收发操作均遵循了先入先出(FIFO)的设计,具体规则如下:

  • 先从 Channel 读取数据的 Goroutine 会先接收到数据;
  • 先向 Channel 发送数据的 Goroutine 会得到先发送数据的权利;

 

 

recvq和sendq两个链表,一个是因读这个通道而导致阻塞的goroutine,另一个是因为写这个通道而阻塞的goroutine。WaitQ是链表的定义,包含一个头结点和一个尾结点:

队列中的每个成员是一个SudoG结构体变量。

 

该结构中主要的就是一个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做以下几件事:

  1. 调用 runtime.getg 获取发送数据使用的 Goroutine;
  2. 执行 runtime.acquireSudog 函数获取 runtime.sudog 结构体并设置这一次阻塞发送的相关信息,例如发送的 Channel、是否在 Select 控制结构中和待发送数据的内存地址等;
  3. 将刚刚创建并初始化的 runtime.sudog 加入发送等待队列,并设置到当前 Goroutine 的 waiting 上,表示 Goroutine 正在等待该 sudog 准备就绪;
  4. 调用 runtime.goparkunlock 函数将当前的 Goroutine 陷入沉睡等待唤醒;
  5. 被调度器唤醒后会执行一些收尾工作,将一些属性置零并且释放 runtime.sudog 结构体;

 

读channel

读channel对应runtime.chanrecv函数。

 

从nil值的channel接收,会调用gopark让出处理器的使用权

如果channel已经关闭,且缓冲区不存在数据,则清除ep指针中的数据并立即返回。ep指针应该指向接收方变量。

 

当 Channel 的 sendq 队列不为空,调用 runtime.recv 函数:

  • 如果 Channel 不存在缓冲区;
  1. 调用 runtime.recvDirect 函数将 sendq 队列中 Goroutine 存储的 elem 数据拷贝到目标内存地址中;
  • 如果 Channel 存在缓冲区;
  1. 将缓冲区队列头的数据拷贝到接收方的内存地址;
  2. 将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?

 

 

 

接口

接口是一个结构体,包含两个成员:类型,和指向数据的指针

 

 

类型断言会比较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函数分别做这个事情。