第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 风险(可预测哈希会被构造碰撞键让查找退化为线性)。
小练习#
写一个函数 dedup 接收 []string 返回去重后的新切片,保持原顺序。
编写 countWords 使用 map 统计字符串切片中每个单词出现次数,并打印排序后的键值。
实际开发#
标准库 slices(Go 1.21 起)提供常见操作:slices.Clone、slices.Equal、slices.Sort/SortFunc、slices.BinarySearch、slices.Index、slices.Delete 等,避免手写循环;多用 Clone/Delete 生成新切片,减少共享底层的意外副作用。
第三方 github.com/samber/lo 提供更高阶的泛型工具(Map/Filter/Reduce/GroupBy/Compact/Unique/Chunk 等);适合偏函数式风格。