讲解
Go 的数组长度固定,且长度是类型的一部分:[3]int 和 [4]int 是两种不同类型,不能互相赋值。数组按值传递——函数收到的是完整拷贝,改形参不影响原数组。这些特性让数组在实际开发中很少直接使用,真正的主角是建立在数组之上的切片(slice)。
切片是对底层数组的一个「动态视图」:它记录指向数组的指针、长度(len)和容量(cap)三件事。用 []int{1, 2, 3} 直接创建,或用 make([]int, 长度, 容量) 预分配。append 往切片尾部追加元素,底层数组装不下时 Go 会自动分配更大的数组并把数据搬过去——所以 append 必须把返回值接回来:s = append(s, x)。
理解「共享底层数组」是用好切片的关键:s[1:3] 切出的子切片和原切片指向同一个数组,改子切片的元素会反映到原切片上。想要一份独立拷贝,用 make 加 copy。append 触发扩容后,新旧切片就不再共享数组,行为差异很微妙——知道「扩容即分家」这条规则,就能预判绝大多数切片行为。
示例
数组与切片的基本对比:
package main
import "fmt"
func main() {
// 数组:长度固定,是类型的一部分
arr := [3]int{1, 2, 3}
fmt.Println("数组:", arr, "长度:", len(arr))
// 切片:长度可变的视图
s := []int{1, 2, 3}
s = append(s, 4, 5)
fmt.Println("切片:", s, "长度:", len(s), "容量:", cap(s))
}
append 与容量增长:
package main
import "fmt"
func main() {
s := []int{}
for i := 1; i <= 5; i++ {
s = append(s, i)
fmt.Printf("追加 %d 后: len=%d cap=%d\n", i, len(s), cap(s))
}
// 已知规模时用 make 预分配容量,避免反复扩容
presized := make([]int, 0, 10)
presized = append(presized, 1, 2, 3)
fmt.Println("预分配:", presized, "cap:", cap(presized))
}
子切片共享底层数组,copy 获得独立副本:
package main
import "fmt"
func main() {
s := []int{1, 2, 3, 4, 5}
sub := s[1:3] // 元素 2、3 的视图
fmt.Println("sub:", sub)
sub[0] = 99
fmt.Println("改 sub 后原切片:", s) // 1 99 3 4 5
dst := make([]int, len(s))
copy(dst, s)
dst[0] = 100
fmt.Println("copy 后互不影响:", s, dst)
}
常见坑
- append 不接返回值:append(s, x) 而不写 s = ... 是编译错误(返回值未使用),这个设计反而救了无数人——因为扩容后原切片根本没变,不接返回值等于白追加。
- nil 切片与空切片分不清:var s []int 是 nil 切片,s := []int{} 是空切片。两者 len 都是 0、都能 append,但 JSON 序列化结果不同(null vs []),做 API 时要留意。
- 遍历中 append 期待新元素被遍历:range 开始时容器长度就定了,循环里 append 的元素本轮遍历不到。需要处理新元素用下标式循环。
- 大切片只取一小段导致内存不释放:sub := big[0:2] 仍引用着整个大数组,GC 无法回收其余部分。只留小段时 copy 出来再丢弃原引用。
小结
切片是底层数组的动态视图,append 扩容「即分家」,子切片共享数据,copy 得独立副本。下一节学习键值对结构:map。