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 一致。startend 都能省略: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 内存优化里非常经典的注意点。

小结

下一篇 Go Map 映射 讲键值对。上一篇 Go 函数

✈️💬