数组
数组是由相同类型元素的集合组成的数据结构,计算机会为数组分配一块连续的内存来保存其中的元素,我们可以利用数组中元素的索引快速访问特定元素
存储元素类型相同、但是大小不同的数组类型在 Go 语言看来也是完全不同的,只有两个条件都相同才是同一类型。
对于一个由字面量组成的数组,当元素数量小于或者等于 4 个时,会直接将数组中的元素放置在栈上,当元素数量大于 4 个时,会将数组中的元素放置到静态区并在运行时取出
切片
动态数组,其长度并不固定,我们可以向切片中追加元素,它会在容量不足时自动扩容
SliceHeader 结构体:
type SliceHeader struct {
Data uintptr // 是指向数组的指针
Len int // 是当前切片的长度
Cap int // 是当前切片的容量,即 Data 数组的大小
}
Data 是一片连续的内存空间,这片内存空间可以用于存储切片中的全部元素,数组中的元素只是逻辑上的概念,底层存储其实都是连续的,所以我们可以将切片理解成一片连续的内存空间加上长度与容量的标识
切片扩容
如果期望容量大于当前容量的两倍就会使用期望容量
如果当前切片的长度小于 1024 就会将容量翻倍
如果当前切片的长度大于 1024 就会每次增加 25% 的容量,直到新容量大于期望容量
下面两种方式处理不一样:
append(slice, 1, 2, 3)
ptr, len, cap := slice
newlen := len + 3
if newlen > cap {
ptr, len, cap = growslice(slice, newlen)
newlen = len + 3
}
*(ptr+len) = 1
*(ptr+len+1) = 2
*(ptr+len+2) = 3
return makeslice(ptr, newlen, cap)
slice = append(slice, 1, 2, 3)
a := &slice
ptr, len, cap := slice
newlen := len + 3
if uint(newlen) > uint(cap) {
newptr, len, newcap = growslice(slice, newlen)
vardef(a)
*a.cap = newcap
*a.ptr = newptr
}
newlen = len + 3
*a.len = newlen
*(ptr+len) = 1
*(ptr+len+1) = 2
*(ptr+len+2) = 3
如果我们选择覆盖原有的变量,就不需要担心切片发生拷贝影响性能,因为 Go 语言编译器已经对这种常见的情况做出了优化。
切片copy
会通过 runtime.memmove 将整块内存的内容拷贝到目标的内存区域中,需要注意的是,整块拷贝内存仍然会占用非常多的资源,在大切片上执行拷贝操作时一定要注意对性能的影响
map
哈希函数
完美哈希函数要求哈希函数的输出范围大于输入范围,但是不可能,现实中实现尽可能的均匀分布
冲突解决
- 开放寻址法
依次探测和比较数组中的元素以判断目标键值对是否存在于哈希表中
当我们向当前哈希表写入新的数据时,如果发生了冲突,就会将键值对写入到下一个索引不为空的位置,查找时对比key,不相等就对比下一位,直到key相等为止
开放寻址法中对性能影响最大的是装载因子,它是数组中元素的数量与数组大小的比值,随着装载因子的增加,线性探测的平均用时就会逐渐增加,这会影响哈希表的读写性能。当装载率超过 70% 之后,哈希表的性能就会急剧下降,而一旦装载率达到 100%,整个哈希表就会完全失效,这时查找和插入任意元素的时间复杂度都是 O(n) 的,这时需要遍历数组中的全部元素,所以在实现哈希表时一定要关注装载因子的变化。
- 链表法
实现拉链法一般会使用数组加上链表,不过一些编程语言会在拉链法的哈希中引入红黑树以优化性能,拉链法会使用链表数组作为哈希底层的数据结构
在一个性能比较好的哈希表中,每一个桶中都应该有 01 个元素,有时会有 23 个,很少会超过这个数量
哈希表的每个桶都只能存储 8 个键值对,一旦当前哈希的某个桶超出 8 个,新的键值对就会存储到哈希的溢出桶中。随着键值对数量的增加,溢出桶的数量和哈希的装载因子也会逐渐升高,超过一定范围就会触发扩容,扩容会将桶的数量翻倍,元素再分配的过程也是在调用写操作时增量进行的,不会造成性能的瞬时巨大抖动
数据结构
type hmap struct {
count int // 当前哈希表中的元素数量
flags uint8
B uint8 // 当前哈希表持有的 buckets 数量,len(buckets) == 2^B
noverflow uint16
hash0 uint32 // 哈希的种子,它能为哈希函数的结果引入随机性,这个值在创建哈希表时确定,并在调用哈希函数时作为参数传入
buckets unsafe.Pointer
oldbuckets unsafe.Pointer // 扩容时用于保存之前 buckets 的字段,它的大小是当前 buckets 的一半
nevacuate uintptr
extra *mapextra
}
type mapextra struct {
overflow *[]*bmap
oldoverflow *[]*bmap
nextOverflow *bmap
}
扩容
触发:
- 装载因子已经超过 6.5(翻倍扩容)
- 哈希使用了太多溢出桶(等量扩容)
Go 语言哈希的扩容不是一个原子的过程
字符串
是一片连续的内存空间,我们也可以将它理解成一个由字符组成的数组
type StringHeader struct {
Data uintptr
Len int
}
字符串是一个只读的切片类型
使用双引号声明的字符串和其他语言中的字符串没有太多的区别,它只能用于单行字符串的初始化
使用反引号时,因为双引号不再负责标记字符串的开始和结束,我们可以在字符串内部直接使用双引号,遇到需要手写 JSON 或者其他复杂数据格式的场景下非常方便
字符串的拼接不能过大,字符串与切片转换也有一定的消耗