Skip to content
All notes

语言基础

归纳 Go 切片、接口等语言机制及其常见边界。

语言基础

切片

切片是对底层数组一段连续区间的描述,由起始指针、长度 len 和容量 cap 组成。多个切片可以共享同一个数组,修改已有元素会互相影响。

  • len 决定可访问的下标:0 <= i < len(s)。
  • cap 决定从切片起点最多能扩展到哪里,重新切片可以超过 len,但不能超过 cap。
  • make([]int, 3, 5) 创建长度为 3、容量为 5 的切片;省略容量时,容量等于长度。
a := [5]int{1, 2, 3, 4, 5}
s := a[1:4]       // len=3,cap=4,与 a 共享数组
s[0] = 20         // a[1] 也变为 20
s = s[:cap(s)]    // 合法:扩展到容量范围
sub := s[0:2:2]   // len=2,cap=2
sub = append(sub, 99) // 容量不足,分配新数组,不覆盖 a 后面的元素

三下标切片 s[low:high:max] 的长度是 high-low,容量是 max-low。它只限制后续追加,不会复制已有元素。

append 与扩容

append 返回更新后的切片,需要保存返回值:s = append(s, v)。容量足够时复用数组;容量不足时分配新数组并复制已有元素。

Go 1.18 引入的扩容策略可概括为:

  1. 新长度超过旧容量的两倍,目标容量直接取新长度。
  2. 否则,旧容量小于 256 时翻倍。
  3. 容量较大时,用 newcap += (newcap + 3*256) >> 2 逐步增长,直到满足新长度;增长倍率趋近 1.25。

实际容量还会受元素大小和内存分配粒度影响。这是运行时实现细节,业务代码不要依赖精确倍率。已知规模时,用 make([]T, 0, n) 预分配,或在 Go 1.21+ 用 slices.Grow 预留空间。

小切片保留大数组

从大数组截出一个小切片并长期保存,会让整个底层数组继续存活。只需要那一小段时,复制到独立数组:

tail := large[len(large)-10:]
small := make([]byte, len(tail))
copy(small, tail)

也可以用 append([]byte(nil), tail...) 复制。这里是引用造成的内存滞留,GC 仍在按可达性工作。

map

语言语义

  • key 必须可比较;slice、map、函数不能直接作 key。
  • nil map 可以查询、遍历和删除,写入会 panic;不存在的 key 返回 value 类型的零值,用 v, ok := m[k] 区分是否存在。
  • 遍历顺序未指定。
  • make(map[K]V, hint) 的 hint 是容量提示,没有对应的 cap 操作。
  • 可以并发只读;只要访问中包含写入,就需要同步。运行时未报并发错误,也不能证明没有数据竞争。

Swiss Table(Go 1.24 引入)

内建 map 使用开放寻址哈希表。每个 group 有 8 个槽位和控制字;控制字记录空位、删除标记或哈希低 7 位(H2)。

查找时,先用哈希定位 table 和起始 group,再用 H2 筛出候选槽位,逐一比较 key。未命中时按探测序列继续;遇到可终止查找的空位,说明 key 不存在。

  • 冲突:通过 group 间的二次探测解决。
  • 删除:能安全标空时直接标空,否则留下墓碑,避免截断探测链。
  • 增长:单个 table 扩容时重排;达到大小上限后拆成两张 table,由 directory 定位。只处理部分 table,避免一次重排整张大 map。
  • 小 map:可以直接使用单个 group,省去完整的目录结构。

image.png

sync.Map

sync.Map 是并发安全的键值容器,零值可用,首次使用后不能复制。适合写一次读多次,或多个 goroutine 操作不同 key 集合的场景;需要维护跨 key、跨字段的一致性时,普通 map 配合锁更直接。

Go 1.24 引入的实现基于并发 hash-trie:

  • 每层按哈希的 4 bit 选择 16 个子槽位之一,读路径主要通过原子指针遍历。
  • 写入会锁住目标内部节点并重新检查状态;哈希前缀冲突时向下扩展,完整哈希冲突时用叶子链处理。
  • 删除后可以剪掉空的内部节点。整个结构并非完全无锁。

常用方法的边界:

方法语义与注意点
LoadOrStore已存在则返回旧值和 loaded=true,否则存入新值。保证存入的竞争结果,不保证构造 value 的代码只执行一次
CompareAndSwap / CompareAndDelete比较成功才更新或删除,参与比较的旧值必须可比较
Range不是一致性快照;每个 key 最多访问一次,值可能来自遍历期间不同时间点;可能需要 O(N) 时间
Clear清空所有键值

一次写操作对观察到该写入的读操作具有同步保证,但容器不会自动保护 value 内部的可变对象,也不会把多次调用合成事务。

defer

defer 在当前函数退出前执行,常用于关闭文件、释放连接、解锁和记录耗时。多个 defer 按后进先出的顺序执行。

mu.Lock()
defer mu.Unlock()
// 访问受保护的数据

参考