Go语言常见数据结构
空结构体
- 空结构体没有空间大小,有位置(独立出现的时候指向同一个位置)
- 空结构体主要是为了节省空间
- 可以和 map 结合实现 set 的功能,
map[string]struct{}{} - 和 channel 结合用于只发送信号,不包含信息,
make(chan struct{})
- 可以和 map 结合实现 set 的功能,
字符串
- 字符串本质是一个结构体,大小为16字节
- Data 指针指向底层字符数组
- Len 表示字符数组的长度(并非字符数量)
- 对字符串使用
len方法得到的是字节数不是字符数,因此存在多个字节表示一个字符的时候不能直接用下标访问,需要用 range - 字符串被
range遍历的时候,被解码成 rune 类型的字符 - 在切片的时候,可以先转为 rune 数组,再切片后转为 string 类型,例如:
s = string([]rune(s)[:3])
切片
- 切片本质是一个结构体,是对数组的引用 // runtime/slice.go type slice struct { array unsafe.Pointer // 元素指针 len int // 长度 cap int // 容量 } ```
- 可以通过字面量和 make 方式进行创建,能通过
len(),cap()等函数访问 - 底层数组是可以被多个 slice 同时指向的,因此对一个 slice 的元素进行操作是有可能影响到其他 slice 的。
切片扩容
- 使用 append 可以向 slice 追加元素,实际上是往底层数组添加元素。如果容量满了,slice 会迁移到新的内存位置,新底层数组的长度也会增加,这样就可以放置新增的元素。同时,为了应对未来可能再次发生的 append 操作,新的底层数组的长度,也就是新
slice的容量是留了一定的buffer的。 - 在1.18版本更新之前,当原 slice 容量小于
1024的时候,新 slice 容量变成原来的2倍;原 slice 容量超过1024,新 slice 容量变成原来的1.25倍。 - 在1.18版本更新之后,当原slice容量(oldcap)小于256的时候,新slice(newcap)容量为原来的2倍;原slice容量超过256,新slice容量newcap = oldcap+(oldcap+3*256)/4。
- 实际的内存在最后还会进行内存对齐,新 slice 的容量是要
大于等于按照前半部分生成的newcap。 - 并发不安全,需要加锁保证
map
map 结构
hmap 整体结构:
map 扩容
- 装载因子:
loadFactor := count / (2^B),count 就是 map 的元素个数,2^B 表示 bucket 数量。也就是元素个数是 bucket 个数的倍数。 - 在向 map 插入新 key 的时候,会进行条件检测,符合下面这 2 个条件,就会触发扩容: 装载因子超过阈值,源码里定义的阈值是 6.5。 overflow 的 bucket 数量过多:当 B 小于 15,也就是 bucket 总数 2^B 小于 2^15 时,如果 overflow 的 bucket 数量超过 2^B;当 B >= 15,也就是 bucket 总数 2^B 大于等于 2^15,如果 overflow 的 bucket 数量超过 2^15。
- 对于元素太多,而 bucket 数量太少的情况:将 B 加 1,bucket 最大数量(2^B)直接变成原来 bucket 数量的 2 倍。于是,就有新老 bucket 了。注意,这时候元素都在老 bucket 里,还没迁移到新的 bucket 来。
- 对于元素没那么多,但是 overflow bucket 数特别多的情况:说明很多 bucket 都没装满。解决办法就是开辟一个新 bucket 空间,将老 bucket 中的元素移动到新 bucket,使得同一个 bucket 中的 key 排列地更紧密。
- 为了保证搬迁的性能,Go map 的扩容采取了一种称为“渐进式”地方式,原有的 key 并不会一次性搬迁完毕,每次最多只会搬迁 2 个 bucket。插入或修改、删除 key 的时候,先检查 oldbuckets 是否搬迁完毕,根据B的变化计算新的桶号尝试进行搬迁 buckets 的工作。
map 扩容并发问题
- map 在扩容情况下,可能出现并发不一致问题
- 通过加锁解决(mutex)
- sync.Map使用两个map,分离了扩容和读写,不会引发扩容的读写使用 read map,可能引起扩容的新增操作使用 dirty map
接口
普通接口
- 接口数据使用
runtime.iface表示 iface记录了数据的地址,也记录了接口的类型信息和实现的方法- 如果实现了接收者是值类型的方法,会隐含地也实现了接收者是指针类型的方法,反过来不会。
- 在调用方法的时候,值类型既可以调用
值接收者的方法,也可以调用指针接收者的方法;指针类型既可以调用指针接收者的方法,也可以调用值接收者的方法。 - 如果方法的接收者是值类型,无论调用者是对象还是对象指针,修改的都是对象的副本,不影响调用者;如果方法的接收者是指针类型,则调用者修改的是指针指向的对象本身。
- 可以使用类似
var _ io.Writer = (*myWriter)(nil) var _ io.Writer = myWriter{}的语句来检查 myWrite 类型是否实现 io.Writer 接口。
空接口
eface表示不包含任何方法的空接口:interface{}。- 空接口可以承载任何类型,因此可以用作接收任何数据的入参
nil,空结构体和空接口
nil是多个类型的零值,或者空值,不同类型的零值不相等- 空结构体的指针和值都不是
nil - 空接口的零值是
nil,一旦有了类型信息就不是nil