前言
Go 的日常编码,八成时间在和 slice 与 map 打交道。这篇把它们的核心语义和著名"陷阱"讲透。
一、数组:定长的值类型
// 长度是类型的一部分: [3]int 和 [4]int 是不同类型!
var a [3]int // {0, 0, 0}(零值)
b := [3]int{1, 2, 3}
c := [...]int{1, 2, 3} // 编译器数长度, 仍是 [3]int
// 数组是值类型: 赋值和传参都是完整拷贝!
d := b
d[0] = 99
fmt.Println(b[0]) // 1 —— b 不受影响
实际开发中很少直接用数组(定长 + 值拷贝太死板),主力是切片。
二、切片:数组的"动态视图"
s := []int{1, 2, 3} // 注意: 没有⻓度, 这是切片
s = append(s, 4) // 追加: [1 2 3 4]
s = append(s, 5, 6) // 可以一次追加多个
s2 := append(s, []int{7, 8}...) // 追加另一个切片
sub := s[1:4] // 切片截取: [2 3 4]
len(s), cap(s) // 长度 与 容量
// make 初始化
buf := make([]int, 0, 16) // len=0, cap=16(预分配, 性能友好)
matrix := make([][]int, 3) // 二维
底层结构(理解陷阱的关键)
切片是一个三元组结构体:
slice ──► [指针 ptr]──► 底层数组
[长度 len]
[容量 cap]
由此衍生三大陷阱:
陷阱 1:切片是引用语义(共享底层数组)
a := []int{1, 2, 3, 4, 5}
b := a[1:3] // [2 3]
b[0] = 99
fmt.Println(a[1]) // 99 —— 改 b 动到了 a 的底层数组!
陷阱 2:append 可能换数组
a := make([]int, 1, 2) // len=1 cap=2
b := append(a, 10) // cap 够, 复用数组
b[0] = 99
fmt.Println(a) // [99] —— a 也变了
c := append(a, 1, 2, 3) // 超过 cap, 分配新数组
c[0] = 100
fmt.Println(a) // [99] —— a 不再受影响
想彻底独立拷贝?
dst := make([]int, len(src))
copy(dst, src)
// 或
dst2 := append([]int(nil), src...)
陷阱 3:从大切片截小片导致内存泄漏
data := make([]byte, 100<<20) // 100MB
small := data[:10] // 只用 10 字节
// 只要 small 活着, 100MB 底层数组就无法回收!
// 正确: 拷贝出需要的部分
smallSafe := make([]byte, 10)
copy(smallSafe, data[:10])
三、常用操作速查
// 遍历
for i, v := range s { ... }
// 查找
idx := slices.Index(s, 3) // 标准库 slices(go1.21+)
// 排序
sort.Ints(s)
slices.Sort(s)
sort.Slice(peoples, func(i, j int) bool {
return peoples[i].Age < peoples[j].Age // 按字段排序
})
// 删除下标 i 的元素
s = append(s[:i], s[i+1:]...)
// 去重(借助 map)
seen := map[string]bool{}
for _, v := range items {
if !seen[v] {
seen[v] = true
uniq = append(uniq, v)
}
}
// 判断相等(go1.21+ 前不能直接 ==)
slices.Equal(a, b)
四、映射 map
// 创建
m := map[string]int{"go": 1, "py": 2}
m2 := make(map[string][]string) // 值是切片
// 增删改查
m["sh"] = 3
delete(m, "py")
v := m["go"] // 不存在时返回零值: 0
// 判断 key 是否存在(comma ok 惯用法)
if v, ok := m["rust"]; ok {
fmt.Println("存在", v)
} else {
fmt.Println("不存在") // ← 走这里
}
// 遍历(注意: 顺序是故意随机化的!)
for k, v := range m {
fmt.Println(k, v)
}
map 的值是不可寻址的:
type Counter map[string]int
c := Counter{}
c["a"]++ // ✅ 这样可以
type User struct{ Name string }
users := map[string]User{}
users["zy"].Name = "x" // ❌ 编译错误: cannot assign to struct field
users["zy"] = User{Name: "x"} // ✅ 整体替换
// 或者值用指针
users2 := map[string]*User{}
users2["zy"].Name = "x" // ✅
map 不是并发安全的,并发读写直接 panic:
var mu sync.Mutex
mu.Lock()
m[k] = v
mu.Unlock()
// 或用标准库并发安全版
var sm sync.Map // 读多写少场景
五、函数传参:Go 全是值传递
func modify(s []int) {
s[0] = 99 // 改的是"拷贝的切片头"指向的同一数组 → 外部可见
s = append(s, 100) // 但重新赋值只影响局部变量 → 外部不可见
}
func main() {
s := []int{1, 2, 3}
modify(s)
fmt.Println(s) // [99 2 3]
}
切片头(ptr/len/cap 结构体)被拷贝,但指向的数组是共享的——这就是"值传递却像引用"的真相。
小结
| 要点 | 一句话 |
|---|---|
| 数组 | 值类型、定长,基本不用 |
| 切片 | 三元组;截取共享底层数组 |
| append | 超 cap 换新数组,改老的不影响 |
| copy | 想要真拷贝 |
| map | comma-ok 判存在;遍历随机;非并发安全 |
| 传参 | 永远是值传递,切片共享底层数组 |
本文是「Golang」系列第 3 篇。