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做以下几件事:

  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?

接口

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

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函数分别做这个事情。

猜你喜欢

转载自www.cnblogs.com/ts65214/p/12977194.html