前言

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 篇。