首页 / Go 语言入门教程 / 切片扩容与内存

Go 语言入门教程

切片扩容与内存

本教程共 80 篇 · 第 30 篇 · 更新于 2026-07-27 · 约 8 分钟阅读

GoGo 入门教程切片Slice扩容容量内存共享

30. 切片扩容与内存

本节目标:搞懂切片 append 时底层是怎么扩容的,容量增长有什么规律,以及内存共享会踩什么坑。

什么时候会扩容

append 往切片加元素时,如果当前 len 没超过 cap,直接加就行,不用扩容。一旦 len 要超过 cap 了,Go 就会分配一个更大的底层数组,把旧数据搬过去。

来看个直观的例子:

package main

import "fmt"

func main() {
    s := make([]int, 0, 3)

    for i := 1; i <= 5; i++ {
        s = append(s, i)
        fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
    }
}

输出:

len=1 cap=3 [1]
len=2 cap=3 [1 2]
len=3 cap=3 [1 2 3]
len=4 cap=6 [1 2 3 4]
len=5 cap=6 [1 2 3 4 5]

前三次 append 都没超 cap(3),cap 不变。第四次超了,cap 翻倍变成 6。

容量增长规律

切片扩容的策略在 Go 1.18 前后有变化。

Go 1.18 之前的扩容规则比较简单:

  • 如果旧容量小于 1024,新容量翻倍
  • 如果旧容量大于等于 1024,新容量每次加 1.25 倍

Go 1.18 开始,扩容策略改了。新策略不再简单翻倍,而是根据元素大小和当前容量做更精细的计算。核心思路是:小切片翻倍多一点,大切片增长慢一点,避免浪费太多内存。

大致规律是这样的:

旧容量新容量(约)
12
24
48
816
1632
翻倍
256512
512848
8481280
大容量约 1.25 倍
Note

具体的新容量还和元素类型的大小有关。元素越大,扩容时越保守。这个细节不需要死记,知道「小切片翻倍,大切片放缓」就够了。

扩容后是新的底层数组

扩容时 Go 会分配一个全新的底层数组,把旧数组的数据拷过去。这意味着扩容后的切片和旧切片不再共享内存

package main

import "fmt"

func main() {
    s := make([]int, 3, 3) // len=3, cap=3
    copy(s, []int{1, 2, 3})

    // s2 和 s 共享底层数组
    s2 := s
    fmt.Println(s, s2) // [1 2 3] [1 2 3]

    // 扩容后 s 指向新数组
    s = append(s, 4)
    fmt.Println(s, s2) // [1 2 3 4] [1 2 3]

    // 改 s 不影响 s2 了
    s[0] = 100
    fmt.Println(s, s2) // [100 2 3 4] [1 2 3]
}

这个例子值得多看几遍。扩容前 s 和 s2 指向同一个底层数组,改一个另一个也变。append 触发扩容后,s 换了新数组,s2 还指着旧的,两者就分道扬镳了。

内存共享的坑

切片共享底层数组是个好特性,但也容易踩坑。看这个经典案例:

package main

import "fmt"

func main() {
    s := []int{1, 2, 3, 4, 5}

    // 截取前3个元素
    sub := s[:3]
    fmt.Println(sub) // [1 2 3]

    // 原切片后面的元素其实还在内存里
    fmt.Println(s) // [1 2 3 4 5]

    // sub 虽然只看到3个,但底层数组有5个
    fmt.Println(cap(sub)) // 5
}

sub := s[:3] 只截取了前 3 个元素,但底层数组还是那个 5 元素的数组。只要你不去扩容 sub,它和 s 就共享同一块内存。

更大的坑在这里:如果你有一个很大的切片,只截了一小部分用,但那个大切片一直被引用着,整个大数组都释放不了:

// 一个100万元素的切片
big := make([]int, 1000000)

// 只用前10个
small := big[:10]

// big 不用了,但 small 还在
// 底层的100万元素数组无法被GC回收!
Tip

解决办法:用 copy 把需要的小部分复制到一个新的独立切片,让大切片可以被回收。

small := make([]int, 10)
copy(small, big[:10])
// 现在 big 可以被回收了

预分配容量避免频繁扩容

如果你知道最终大概要放多少元素,用 make 预分配容量,可以避免多次扩容和拷贝:

// 不好:从空切片开始,多次扩容
var s []int
for i := 0; i < 10000; i++ {
    s = append(s, i)
}

// 好:预分配,一次到位
s := make([]int, 0, 10000)
for i := 0; i < 10000; i++ {
    s = append(s, i)
}

预分配的性能差异在大数据量下很明显。养成这个习惯,代码质量会提升一截。

小结

扩容这块记住三点:

  • append 超过 cap 就扩容,扩容后是全新的底层数组
  • 小切片翻倍增长,大切片约 1.25 倍(Go 1.18 后更精细)
  • 切片共享底层数组,小切片引用大数组可能导致内存泄漏

下一章介绍 Go 1.21 新增的 slices 标准库,排序、查找、克隆一行搞定。