数组、切片与 map
arrays、slices、maps、strings、make、comma-ok。
数组、切片与 map
这一课你将能做到
- 说清数组与切片的本质区别,画出切片「指针 + len + cap」的底层结构
- 用 make / append / 切片表达式 / copy 熟练操作切片,并能解释 append 的扩容行为
- 用 map 完成增删改查与 comma-ok 判断,说出哪些类型可以当 map 的 key
为什么要学
Go 日常开发 90% 的数据容器就是切片(slice)和 map。数组反而很少直接用——它只是切片的地基。这两样东西的底层机制是 Go 面试的固定题库:切片怎么扩容、两个切片为什么会被牵连修改、map 为什么遍历无序、map 并发读写会怎样。这一课把这些一次讲透,后面写并发、写服务才不会踩雷。
先建立图像
切片不是数组,它是一个「三件套」结构体,指向一块底层数组:
s := []int{10, 20, 30} (cap 为 3)
slice header(切片头)
┌──────────┬─────┬──────┐
│ 指针 ptr │ len │ cap │
└────┬─────┴─────┴──────┘
│ 指向
▼
底层数组 ┌────┬────┬────┐
│ 10 │ 20 │ 30 │
└────┴────┴────┘
◀—— len=3 ——▶
len:窗口里能看到几个元素(len(s))cap:从切片起点开始,底层数组总共能容纳几个元素(cap(s))append:空间够就在底层数组后面写;不够就换一块更大的数组,指针指向新地址
理解了这张图,本课一半的坑就自动解开了。
跟着做
步骤 1:数组——值类型,很少直接用
package main
import "fmt"
func main() {
a := [3]int{1, 2, 3} // 长度是类型的一部分:[3]int ≠ [4]int
b := a // 整块拷贝!数组是值类型
b[0] = 100
fmt.Println(a) // [1 2 3],a 没变
fmt.Println(b) // [100 2 3]
// 数组长度也可以由编译器数出来
c := [...]string{"x", "y", "z"}
fmt.Println(len(c)) // 3
}
因为赋值/传参会整块拷贝,加上长度写死,实际工程里数组主要作为切片的底层存在。
步骤 2:切片的两种创建方式
package main
import "fmt"
func main() {
// 方式一:字面量,最常用
s := []int{1, 2, 3} // 注意 [] 里没有数字,就是切片
// 方式二:make,预先指定长度和容量
nums := make([]int, 3) // len=3, cap=3,元素是零值 [0 0 0]
buf := make([]int, 0, 8) // len=0, cap=8,空切片但已预留空间
fmt.Println(s, len(s), cap(s))
fmt.Println(nums, len(nums), cap(nums))
fmt.Println(buf, len(buf), cap(buf))
nums[0] = 9 // len=3 的切片可以直接按下标赋值
fmt.Println(nums)
}
已知大概数量时用 make([]T, 0, n) 预分配,能避免多次扩容拷贝,这是性能优化的常识。
步骤 3:append 与扩容
package main
import "fmt"
func main() {
s := make([]int, 0, 3)
fmt.Printf("初始: len=%d cap=%d\n", len(s), cap(s))
for i := 1; i <= 6; i++ {
s = append(s, i) // 必须接收返回值!
fmt.Printf("append %d 后: len=%d cap=%d 底层地址=%p\n",
i, len(s), cap(s), s)
}
}