Chan数据结构
src/runtime/chan.go:hchan
定义了channel的数据结构:
type hchan struct {
qcount uint // 当前队列中剩余元素个数
dataqsiz uint // 环形队列长度,即可以存放的元素个数
buf unsafe.Pointer // 环形队列指针
elemsize uint16 // 每个元素的大小
closed uint32 // 标识关闭状态
elemtype *_type // 元素类型
sendx uint // 队列下标,指示元素写入时存放到队列中的位置
recvx uint // 队列下标,指示元素从队列的该位置读出
recvq waitq // 等待读消息的goroutine队列
sendq waitq // 等待写消息的goroutine队列
lock mutex // 互斥锁,chan不允许并发读写
}
dataqsiz指示了队列长度为6,即可缓存6个元素;
buf指向队列的内存,队列中还剩余两个元素;
qcount表示队列中还有两个元素;
sendx指示后续写入的数据存储的位置,取值[0, 6);
recvx指示从该位置读取数据, 取值[0, 6);
recvq waitq // 等待读消息的goroutine队列
sendq waitq // 等待写消息的goroutine队列
从channel读数据,如果channel缓冲区为空或者没有缓冲区,当前goroutine会被阻塞。
向channel写数据,如果channel缓冲区已满或者没有缓冲区,当前goroutine会被阻塞。
被阻塞的goroutine将会挂在channel的等待队列中:
- 因读阻塞的goroutine会被向channel写入数据的goroutine唤醒;
- 因写阻塞的goroutine会被从channel读数据的goroutine唤醒;
elemtype *_type // 元素类型
一个channel只能传递一种类型的值,类型信息存储在hchan数据结构中。
lock mutex // 互斥锁,chan不允许并发读写
一个channel同时仅允许被一个goroutine读写
Slice数据结构
源码包中src/runtime/slice.go:slice
定义了Slice的数据结构:
type slice struct {
array unsafe.Pointer //指针
len int //长度
cap int //容量
}
看一下slice的特性
func main() {
//1.slice
var array [10]int
array[5] = 100
array[6] = 101
var slice = array[5:6]//传递的是指针
slice[0] = 200
fmt.Println("lenth of slice: ", len(slice))
fmt.Println("capacity of slice: ", cap(slice))
fmt.Println(&slice[0] == &array[5])
fmt.Println("array: ", array)
//2.slice
var slice []int
slice = append(slice, 1, 2, 3, 4)
fmt.Println("cap of slice :", cap(slice))
newSlice := append(slice, 5)
fmt.Println(&slice[0] == &newSlice[0])
//================================
var slice []int
slice = append(slice, 1, 2, 3, 4, 5)
fmt.Println("cap of slice :", cap(slice))
newSlice := append(slice, 6)
fmt.Println(&slice[0] == &newSlice[0])
}
第一个输出的是
lenth of slice: 1
capacity of slice: 5
true //证明slice使用的是array 的地址
array: [0 0 0 0 0 200 101 0 0 0]
第二个输出的是
cap of slice : 4
false
========
cap of slice : 6
true
解释:append函数执行时会判断切片容量是否能够存放新增元素,如果不能,则会重新申请存储空间,新存储空间将是原来的2倍或1.25倍(取决于扩展原空间大小),本例中实际执行了两次append操作,第一次空间增长到4,所以第二次append需要扩容,所以新旧两个切片所用存储空间不一样。程序会输出”false”。
第二个案例,第一次空间增加到6,第二次append时不需要扩容,所以为true
Slice 扩容机制
使用append向Slice追加元素时,如果Slice空间不足,将会触发Slice扩容,扩容实际上是重新分配一块更大的内存,将原Slice数据拷贝进新Slice,然后返回新Slice,扩容后再将数据追加进去。
Slice Copy
使用copy()内置函数拷贝两个切片时,会将源切片的数据逐个拷贝到目的切片指向的数组中,拷贝数量取两个切片长度的最小值。
也就是说,copy过程中不会发生扩容。
下面是一个面试常见的算法题,其中就用到copy
package main
import "fmt"
// 题目:给定两个整数 n 和 k,返回 1 ... n 中所有可能的 k 个数的组合。
// 示例: 输入: n = 4, k = 2 输出: [ [2,4], [3,4], [2,3], [1,2], [1,3], [1,4], ]
var (
path []int
res [][]int
)
func main() {
arr := combine(4, 2)
fmt.Println(arr)
}
func combine(n int, k int) [][]int {
path, res = make([]int, 0, k), make([][]int, 0)
dfs1(n, k, 1)
return res
}
func dfs1(n, k int, start int) {
if len(path) == k {
temp := make([]int, k)
copy(temp, path) //这里需要先进行copy,再添加,否则所有添加的结果统一
res = append(res, temp)
}
for i := start; i <= n; i++ {
path = append(path, i)
dfs1(n, k, i+1)
path = path[:len(path)-1]
}
}
总结:
- 每个切片都指向一个底层数组
- 每个切片都保存了当前切片的长度、底层数组可用容量
- 使用len()计算切片长度时间复杂度为O(1),不需要遍历切片
- 使用cap()计算切片容量时间复杂度为O(1),不需要遍历切片
- 通过函数传递切片时,不会拷贝整个切片,因为切片本身只是个结构体而已
- 使用append()向切片追加元素时有可能触发扩容,扩容后将会生成新的切片
map
map数据结构由runtime/map.go:hmap
定义:
type hmap struct {
count int // 当前保存的元素个数
...
B uint8
...
buckets unsafe.Pointer // bucket数组指针,数组的大小为2^B
...
}
以hmap.B=2
为例, 此时hmap.buckets长度是2^B为4. 元素经过哈希运算后会落到某个bucket中进行存储,也就是经过hash运算之后数据会落在4个桶之中的一个
bucket数据结构
bucket数据结构由runtime/map.go:bmap
定义:
type bmap struct {
tophash [8]uint8 //存储哈希值的高8位
data byte[1] //key value数据:key/key/key/.../value/value/value...
overflow *bmap //溢出bucket的地址
}
每个bucket可以存储8个键值对。
- tophash是个长度为8的数组,哈希值相同的键(准确的说是哈希值低位相同的键)存入当前bucket时会将哈希值的高位存储在该数组中,以方便后续匹配。
- data区存放的是key-value数据,存放顺序是key/key/key/…value/value/value,如此存放是为了节省字节对齐带来的空间浪费.
- overflow 指针指向的是下一个bucket,据此将所有冲突的键连接起来。
下图展示bucket存放8个key-value对
当有两个或以上数量的键被哈希到了同一个bucket时,我们称这些键发生了冲突。Go使用链地址法来解决键冲突。
下图展示产生冲突后的map:
map查询过程
查找过程如下:
- 根据key值算出哈希值
- 取哈希值低位与hmap.B取模确定bucket位置
- 取哈希值高位在tophash数组中查询
- 如果tophash[i]中存储值也哈希值相等,则去找到该bucket中的key值进行比较
- 当前bucket没有找到,则继续从下个overflow的bucket中查找。
- 如果当前处于搬迁过程,则优先从oldbuckets查找
注:如果查找不到,也不会返回空值,而是返回相应类型的0值。