santisify Site

Back

go-collections

July 15, 2026

第4章:数组、切片与Map#

学习目标#

  • 掌握数组、切片、map 核心特性
  • 理解切片容量/扩容与底层数组
  • 安全遍历与初始化习惯
  • 避免 nil 切片/空 map 的常见坑

章节提纲#

  • 数组与切片差异、make 使用
  • 切片容量、append、切片截取
  • map 创建、读取、删除与并发限制
  • range 遍历与拷贝语义
  • 常见陷阱:共享底层数组、nil map

数组与切片的区别#

  • 数组:[N]int 长度是类型的一部分,值语义,赋值/传参会整体拷贝;很少直接作为参数,多用于固定尺寸的底层缓冲。
  • 切片:[]int 是一个描述符,包含指向底层数组的指针、长度、容量,引用语义。零值 nil 切片可安全 len/cap/append(但读写元素会 panic)。
  • 字面量:数组 [3]int{1,2,3},切片 []int{1,2,3};切片更常用。

slice 变量 s
┌───────────────┐
│ ptr  ─────────┼─────► 底层数组
│ len = 3       │       ┌───┬───┬───┬───┬───┐
│ cap = 5       │       │ a │ b │ c │ d │ e │
└───────────────┘       └───┴───┴───┴───┴───┘

                          len=3  cap=5
plaintext

切片创建与 make#

  • var s []int 得到 nil 切片;make([]int, 3) 申请长度=容量=3 的底层数组并填零值。
  • 预分配:make([]int, 0, 10) 用于先占容量再 append,减少扩容。
  • 空切片与 nil 切片:[]int{} 长度0但非 nil,JSON 序列化会得到 []var s []int 序列化可能是 null,根据接口需求选择。

切片截取与共享底层#

  • 基本截取:s[a:b],长度 b-a,容量 cap(s)-a,与原切片共享底层数组,修改会互相影响。
  • 全切:s[:] 保持原长度/容量;缩短容量可用 full slice 表达式 s[a:b:b],限制后续 append 影响原切片。
  • 与 Python 不同:Go 不支持负索引或步长,也不支持 b 超过 len(s)(full slice 时不超过 cap)。省略 a/b 时分别默认为 0 和 len(s)
nums := []int{1, 2, 3, 4}
sub := nums[1:3]   // [2 3] len=2 cap=3
sub[0] = 20        // nums 变为 [1 20 3 4]
safe := nums[1:3:3] // cap=2,后续 append 不影响 nums
go

append、容量与扩容#

  • append 可能返回指向同一底层数组的切片,若容量不足会分配新数组并拷贝旧数据,所以务必使用返回值s = append(s, v)
  • 预估大小时用 make 预分配容量,能减少扩容开销;append(s, t...) 可连接两个切片。
  • 复制:copy(dst, src) 返回复制元素个数,长度取两者较小。
  • 扩容规则(Go 1.21 起):当 len+需要元素 超过当前 cap,运行时会创建新数组:旧容量 < 1024 时通常按 2 倍增长;≥ 1024 时按约 1.25 倍递增(cap += cap/4),直到可容纳新元素,再将旧数据 memmove 过去。扩容会导致切片指向新底层数组,原底层数组保持不变。

切片作为函数参数#

  • 切片参数是“值传递”,但值里包含指向底层数组的指针;因此修改元素会影响调用方
  • 若函数内 append 触发扩容,会拿到新底层数组;此时对新切片的修改不再影响调用方,所以需要返回新的切片。
  • 想隔离修改:在函数内 copy 到新切片或用 append([]T(nil), s...) 克隆。
func mutate(s []int) {
	s[0] = 99           // 影响调用方
	s = append(s, 100)  // 可能扩容
	s[1] = 88           // 若扩容,调用方不受影响
}

func main() {
	a := []int{1, 2, 3}
	mutate(a)
	fmt.Println(a) // [99 2 3] 或 [99 88 3] 取决于是否扩容,像这里会发生扩容,所以是[99 2 3]
}
go

map 创建、读取与删除#

  • 零值 map 是 nil,读返回零值,写会 panic;需要 make(map[string]int) 或字面量 map[string]int{"a":1}
  • 取值:v, ok := m["key"],用 ok 区分“键不存在”与“零值”;删除用 delete(m, "key")(不存在时安全)。
  • 遍历:for k, v := range m {};遍历顺序不保证,每次随机;len(m) 返回键数量。
  • 并发:内置 map 不是并发安全的,写操作需加锁或使用 sync.Map

range 语义与注意点#

  • range over 切片/数组时,k 是索引,v 是元素的拷贝;修改 v 不会写回原切片,需用索引写:s[i] = ...
  • range over map 时,键和值都是拷贝,且顺序不稳定;不要依赖迭代顺序做逻辑。

常见陷阱#

  • 切片共享底层导致“误修改”:截取后修改或 append 可能影响原切片,必要时用 full slice 限制容量或 copy 复制。
  • nil 切片可 append,但 nil map 不能写;初始化 map 时用 make 或字面量。
  • 长期保存大切片的子切片可能导致底层大数组无法回收,可用 copy 到新切片以释放多余内存。
  • map 键必须可比较(用于哈希),不能直接使用切片/函数等不可比较类型。
  • map 遍历顺序随机:Go 在 map 初始化/扩容时随机化桶迭代起点并扰动哈希,避免代码依赖顺序并降低哈希碰撞 DoS 风险(可预测哈希会被构造碰撞键让查找退化为线性)。

小练习#

  1. 写一个函数 dedup 接收 []string 返回去重后的新切片,保持原顺序。
  2. 编写 countWords 使用 map 统计字符串切片中每个单词出现次数,并打印排序后的键值。

实际开发#

  • 标准库 slices(Go 1.21 起)提供常见操作:slices.Cloneslices.Equalslices.Sort/SortFuncslices.BinarySearchslices.Indexslices.Delete 等,避免手写循环;多用 Clone/Delete 生成新切片,减少共享底层的意外副作用。
  • 第三方 github.com/samber/lo 提供更高阶的泛型工具(Map/Filter/Reduce/GroupBy/Compact/Unique/Chunk 等);适合偏函数式风格。