切片即动态数组,可以动态扩容改变数组的容量. golang 的 slice 底层结构如下所示,它是一个结构体,里面包含了指向数组的地址,并通过 len、cap 保存数组的元素数、容量:
切片拷贝:
考虑到切片 slice 的结构,对于切片直接用 = 拷贝,实际上是浅拷贝,只是改变了指针的指向,并没有改变数组中元素的值. 对于深度拷贝的需求,可以借助 copy 内置函数完成. 两种拷贝的方式如下:
- 深度拷贝: copy(sliceA, sliceB)
- 浅拷贝: sliceA = sliceB
切片之间的复制会拷贝数组指针、cap、len 值,但数组指针指向的是同一个地址. 如果想做深度拷贝,即将指针指向的数组内容而不是指针值进行拷贝. 可以使用内置的 copy 函数进行切片拷贝. 如下所示,使用 copy 进行复制,会改变 s2 地址的内存内的数组值.
切片 slice 函数传递
在切片进行复制时,会将切片的值(指针、cap、len)复制了一份. 在函数内部可以改变原切片的值.
但是,当涉及到 append 触发扩容时,原来的指针指向的地址会发生变化,之后再对数组值进行更改,原切片将不受影响.
切片 slice 的扩容
当使用 append(slice,data) 时候,Golang 会检查底层的数组的长度是否已经不够,如果长度不够,Golang 则会新建一个数组,把原数组的数据拷贝过去,再将 slice 中的指向数组的指针指向新的数组。
其中新数组的长度一般是老数组的俩倍,当然,如果一直是俩倍增加,那也会极大的浪费内存. 所以在老数组长度大于 1024 时候,将每次按照不小于 25% 的涨幅扩容.
slice 增加长度的源码在 src/runtime/slice.go 的 growslice 函数中
Mapmap 字典是 golang 中高级类型之一,它提供键值对形式的存储. 它也是引用类型,参数传递时其内部的指针被复制,指向的还是同一个内存地址. 当对赋值后的左值进行修改时,是会影响到原 map 值的.
map 的底层本质上是实现散列表,它解决碰撞的方式是拉链法. map 在进行扩容时不会立即替换原内存,而是慢慢的通过 GC 方式释放.
hmap 结构
以下是 map 的底层结构,其源码位于 src/runtime/map.go 中,结构体主要是 hmap .
上述代码中 buckets、oldbuckets 是指向存储键值的内存地址, 其中 oldbuckets 用于在扩容时候,指向旧的 bucket 地址,再下次访问时不断的将 oldbuckets 值转移到 buckets 中. oldbuckets 并不直接释放内存,而是通过不引用,交由 gc 释放内存.
散列表和 bucket ( a bucket for a go map)
hmap 中核心的结构是 buckets,它是 bucket 数组,其中每个 bucket 是一个链表. 这个结构其实就是散列表的实现,通过拉链法消除 hash 冲突. 使得散列表能够存储更多的元素,同时避免过大的连续内存申请. 如下图 1,是 golang buckets 数组在内存中的形式,buckets 数组的每个元素是链表的头节点.
在哈希表结构中有一个加载因子(即 loadFactor), 它一般是散列包含的元素数除以位置总数. 加载因子越高,冲突产生的概率越高. 当达到一定阈值时,就该为哈希表进行扩容了,否则查询效率将会很低.
当 golang map 的加载因子大于阈值时,len(map) / 2 ^ B > 6.5 时 ,就会对 map 对象进行扩容. 扩容不会立刻释放掉原来的 bucket 内存,而是由 oldbucket 指向,并产生新的 buckets 数组并由指针 buckets 指向. 在再次访问原数据时,再依次将老的 bucket 移到新的 buckets 数组中. 同时解除对老的 bucket 的引用,GC 会统一释放掉这些内存.
哈希函数
哈希函数是哈希表的特点之一,通过 key 值计算哈希,快速映射到数据的地址. golang 的 map 进行哈希计算后,将结果分为高位值和低位值,其中低位值用于定位 buckets 数组中的具体 bucket,而高位值用于定位这个 bucket 链表中具体的 key .
channelhchan 结构
chan 源码位于 src/runtime/chan.go 中,其结构体为 hchan,其中主要包括 buf、sendx、recvx、sendq、recvq 等.
主要结构的作用:
- buf: 有缓冲通道用于存储缓存数据的空间, 它是一个循环链表.
- sendx 和 recvx: 用于记录循环链表 buf 中的发送或者接受的 index.
- sendq 和 recvq: 是俩个双向队列,分别是发送、接受的 goroutine 抽象出来的 sudog 结构体的队列.
- lock: 互斥锁. 在 send 和 recv 操作时锁住 hchan.
make 创建通道
使用 make 可以创建通道,如下示例:
创建通道实际上是创建了一个 hchan 结构体,返回指针 ch0 . chan 在 go 语言中是引用类型, 在参数传递过程是复制的是这个指针值.
send 和 recv
首先会使用 lock 锁住 hchan. 然后以 sendx 或 recvx 序号,在循环链表 buf 指定的位置上找到数据,将数据 copy 到 goroutine 或者时从 goroutine copy 的 buf 上. 然后释放锁.