Go语言常见数据结构 | 豆包MarsCode AI 刷题

111 阅读5分钟

Go语言常见数据结构

空结构体

  • 空结构体没有空间大小,有位置(独立出现的时候指向同一个位置
  • 空结构体主要是为了节省空间
    • 可以和 map 结合实现 set 的功能,map[string]struct{}{}
    • 和 channel 结合用于只发送信号,不包含信息, make(chan struct{})

字符串

  • 字符串本质是一个结构体,大小为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 整体结构:

image-20240112130800314

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