Go 数组与切片 Slice — 长度、容量与扩容
要把"一组数据"装进一个变量,Go 提供了两种东西:数组(array)和切片(slice)。新手一开始最容易把它们搞混,但其实只要记住一句话——数组是固定长度的值类型,切片是变长的引用类型——剩下的事情就水到渠成了。日常开发里 99% 用的是切片,数组大多出现在底层或固定大小的场景。
一、数组:固定长度,值类型
Go 的数组在声明时必须给出长度,而且长度是类型的一部分——[3]int 和 [5]int 是两个完全不同的类型,不能互相赋值。数组是值类型,赋值或传参都会完整拷贝一份,这一点和 C 大不一样(C 里数组名其实是指针)。
package main
import "fmt"
func main() {
// 数组:固定长度,值类型
var a [5]int // 长度 5,元素自动初始化为 0
b := [3]int{1, 2, 3} // 指定长度并赋值
c := [...]int{10, 20, 30} // ... 让编译器自己数长度
fmt.Println(a, b, c)
// [0 0 0 0 0] [1 2 3] [10 20 30]
// 遍历:for + range,同时拿到索引和值
for i, v := range b {
fmt.Printf("索引 %d = %d\n", i, v)
}
// 值类型:赋值即拷贝,改副本不影响原数组
b2 := b
b2[0] = 99
fmt.Println(b[0]) // 还是 1
}注意三种写法:var a [5]int 得到全零数组;b := [3]int{1,2,3} 指定长度并赋值;最方便的是 c := [...]int{10,20,30},用 ... 让编译器自己数长度。for i, v := range b 是 Go 遍历数组/切片的标准姿势,同时拿到索引和值,不需要的时候可以用 _ 丢掉其中一个。
二、切片:变长,引用类型
切片可以理解为"数组的一个视图"——它本身不存数据,而是指向底层某段数组,并记录三个东西:指针(指向起始位置)、长度 len(当前可见元素数)、容量 cap(从指针到底层数组末尾还能用多少)。正因为只是视图,切片之间共享底层数组,改一个会影响另一个。
package main
import "fmt"
func main() {
// 切片:变长,引用类型,底层是某个数组的一段
var s []int // nil 切片(还没指向任何数组)
s2 := []int{1, 2, 3} // 字面量创建
s3 := make([]int, 5, 10) // make(类型, 长度, 容量)
fmt.Println(s, s2, s3) // [] [1 2 3] [0 0 0 0 0]
fmt.Println(len(s3), cap(s3)) // 5 10
// 切片操作 s[start:end],左闭右开
nums := []int{0, 1, 2, 3, 4, 5}
fmt.Println(nums[1:4]) // [1 2 3]
fmt.Println(nums[:3]) // [0 1 2]
fmt.Println(nums[3:]) // [3 4 5]
fmt.Println(nums[:]) // 整段
}创建切片有三种主流方式:字面量 []int{1,2,3}、make([]int, 5, 10)、以及对已有数组/切片做截取 nums[1:4]。切片操作 s[start:end] 是左闭右开的,包含 start 不包含 end,这一点和 Python 一致。start 或 end 都能省略:nums[:3] 取前三个,nums[3:] 取从下标 3 到末尾,nums[:] 取整段。
三、len 与 cap:长度与容量
len(s) 返回切片当前有多少个元素;cap(s) 返回底层数组从切片起始位置算起,最多能扩展到多少。当 len 还没顶到 cap 时,append 不会分配新数组;一旦 len 超过 cap,Go 就会分配一块更大的底层数组,把旧数据搬过去,通常容量翻倍(小切片时是翻倍,大了之后约 1.25 倍,具体由运行时决定)。
四、append 与 copy
append 是切片最常用的操作,在末尾追加元素,返回新的切片——一定要用 s = append(s, x) 接住返回值,因为它可能已经指向一块全新的底层数组。copy(dst, src) 则用来把一个切片的内容拷贝到另一个,返回实际拷贝的元素个数(取两者长度的较小值)。
package main
import "fmt"
func main() {
s := make([]int, 0, 2) // 长度 0,容量 2
for i := 1; i <= 5; i++ {
s = append(s, i) // 追加元素,可能触发扩容
fmt.Printf("追加 %d 后:len=%d cap=%d\n", i, len(s), cap(s))
}
// copy(dst, src):把 src 拷贝到 dst,返回拷贝的元素个数
src := []int{1, 2, 3}
dst := make([]int, len(src))
n := copy(dst, src)
fmt.Println(dst, n) // [1 2 3] 3
}运行上面这段代码,你会看到 cap 从 2 一路跳到 4、再到 8——这就是扩容的过程。每次扩容都要分配新内存、拷贝旧数据,如果提前知道最终大小,用 make([]int, 0, n) 一次性给够容量,能显著减少开销,这是性能优化的小窍门。
五、make 函数
make 是 Go 专门用来创建切片、map 和 channel 这三种"引用类型"的内建函数。对切片而言,签名是 make([]T, length, capacity),容量参数可以省略(默认等于长度)。它和字面量 []int{} 的区别在于:make 会预分配好底层数组,适合已知大概规模的场景。
六、多维切片
多维切片就是"切片的切片",声明 [][]int。和 C/Java 的二维数组不同,Go 的每一行长度可以不一样——因为每行本身是一个独立的切片。声明时通常用字面量嵌套,遍历时用双层 for range。
package main
import "fmt"
func main() {
// 二维切片:本质是"切片的切片"
grid := [][]int{
{1, 2, 3},
{4, 5, 6},
{7, 8, 9},
}
// 双层遍历
for r, row := range grid {
for c, v := range row {
fmt.Printf("grid[%d][%d]=%d ", r, c, v)
}
fmt.Println()
}
}七、一个常见的陷阱
切片只持有底层数组的一个"窗口",所以如果一个很大的底层数组,你只用 s[:10] 截了一小段并长期持有,整个大数组都不会被垃圾回收——因为切片的指针还指着它。如果你只需要其中几个元素,可以用 copy 把它们拷到一个新的小切片,让大数组释放。这是 Go 内存优化里非常经典的注意点。
小结
- 数组固定长度、值类型;赋值即拷贝。
- 切片变长、引用类型,底层是数组的一段,靠 len/cap 描述。
append记得接住返回值;copy用于独立拷贝。- 预知规模就用
make([]T, 0, n)预分配,避免反复扩容。