前置知识: Go

切片原理

3 minIntermediate2026/6/14

Go切片底层实现与扩容

概述

切片(slice)是 Go 语言中最常用的数据结构之一,它是对数组的抽象封装,提供了动态扩容的能力。与固定长度的数组不同,切片的长度可以在运行时动态增长。理解切片的底层实现原理,有助于编写高效的代码并避免常见的内存陷阱。

基础概念

切片与数组的区别

数组是固定长度的,长度是型的一部分。切片是动态的,底层引用一个数组。切片本身只是一个小的结构体,包含指向底层数组的指针、长度和容量。

// 数组:长度是类型的一部分
var arr1 [5]int     // 长度为 5 的数组
var arr2 [3]int     // 长度为 3 的数组,与 arr1 类型不同

// 切片:长度可变
var s1 []int        // nil 切片
s2 := []int{1, 2, 3} // 字面量创建

切片的底层结构

切片在运行时由以下结构体表示:

type slice struct {
    array unsafe.Pointer  // 指向底层数组的指针
    len   int             // 当前元素个数
    cap   int             // 底层数组的容量
}

这三个字段的大小在 64 位系统上共占 24 字节。切片本身很小,复制切片只是复制这三个字段,底层数组不会被复制。

快速上手

创建切片

// 方式一:字面量
s1 := []int{1, 2, 3}

// 方式二:make 函数
s2 := make([]int, 5)       // 长度 5,容量 5
s3 := make([]int, 3, 10)   // 长度 3,容量 10

// 方式三:从数组切片
arr := [5]int{1, 2, 3, 4, 5}
s4 := arr[1:3]  // [2, 3],len=2, cap=4

// 方式四:new(不推荐,得到的是指向切片的指针)
s5 := *new([]int)  // nil 切片

基本操作

s := []int{1, 2, 3}

// 获取长度和容量
fmt.Println(len(s))  // 3
fmt.Println(cap(s))  // 3

// 追加元素
s = append(s, 4)     // [1, 2, 3, 4]

// 追加多个元素
s = append(s, 5, 6)  // [1, 2, 3, 4, 5, 6]

// 复制切片
dst := make([]int, len(s))
copy(dst, s)

详细用法

扩容策略

当 append 操作导致切片长度超过容量时,Go 运行时会分配新的底层数组并将旧数据复制过去。扩容策略如下:

  • 旧容量小于 256:新容量 = 旧容量 * 2(翻倍)
  • 旧容量大于等于 256:新容量 = 旧容量 * 1.25 + 192(渐进增长)
  • 最终容量还需要根据元素大小进行内存对齐
// 观察扩容过程
func observeGrow() {
    var s []int
    for i := 0; i < 1000; i++ {
        s = append(s, i)
        if i%100 == 0 {
            fmt.Printf("len=%d cap=%d\n", len(s), cap(s))
        }
    }
}

切片表达式

Go 提供了简单切片表达式和完整切片表达式两种语法:

arr := [5]int{1, 2, 3, 4, 5}

// 简单切片表达式:s[low:high]
s1 := arr[1:3]    // [2, 3], len=2, cap=4

// 省略下界
s2 := arr[:3]     // [1, 2, 3], len=3, cap=5

// 省略上界
s3 := arr[2:]     // [3, 4, 5], len=3, cap=3

// 完整切片表达式:s[low:high:max]
// 限制容量为 max-low
s4 := arr[1:3:4]  // [2, 3], len=2, cap=3

完整切片表达式可以限制新切片的容量,防止 append 操作意外覆盖原数组的后续元素。

切片共享底层数组

这是切片最常见的陷阱:多个切片可能共享同一个底层数组,修改一个切片会影响其他切片。

// 切片共享底层数组
a := []int{1, 2, 3, 4, 5}
b := a[1:3]      // b = [2, 3]
b[0] = 99        // a[1] 也变成 99

fmt.Println(a)   // [1, 99, 3, 4, 5]
fmt.Println(b)   // [99, 3]

使用 copy 避免共享:

a := []int{1, 2, 3, 4, 5}
b := make([]int, 2)
copy(b, a[1:3])  // b 拥有独立的底层数组
b[0] = 99        // 不影响 a

使用完整切片表达式限制容量:

a := []int{1, 2, 3, 4, 5}
b := a[1:3:3]    // len=2, cap=2
b = append(b, 10) // 触发扩容,b 获得新底层数组
// a 不受影响

nil 切片与空切片

// nil 切片:未初始化
var s1 []int
fmt.Println(s1 == nil)  // true
fmt.Println(len(s1))    // 0
fmt.Println(cap(s1))    // 0

// 空切片:已初始化但无元素
s2 := []int{}
fmt.Println(s2 == nil)  // false
fmt.Println(len(s2))    // 0

// 两者在 JSON 序列化时行为不同
// nil 切片序列化为 null
// 空切片序列化为 []

在大多数使用场景下,nil 切片和空切片可以互换,但在 JSON 序列化和反射中行为不同。

常见场景

场景一:预分配切片提升性能

// 不推荐:多次扩容
func buildSliceBad(n int) []int {
    var s []int
    for i := 0; i < n; i++ {
        s = append(s, i) // 可能多次扩容和复制
    }
    return s
}

// 推荐:预分配容量
func buildSliceGood(n int) []int {
    s := make([]int, 0, n) // 预分配容量
    for i := 0; i < n; i++ {
        s = append(s, i)   // 不会触发扩容
    }
    return s
}

场景二:删除切片元素

// 删除索引 i 处的元素(保持顺序)
func remove(s []int, i int) []int {
    return append(s[:i], s[i+1:]...)
}

// 删除索引 i 处的元素(不保持顺序,性能更好)
func removeFast(s []int, i int) []int {
    s[i] = s[len(s)-1]
    return s[:len(s)-1]
}

场景三:切片去重

func unique(s []int) []int {
    seen := make(map[int]bool)
    result := make([]int, 0, len(s))
    for _, v := range s {
        if !seen[v] {
            seen[v] = true
            result = append(result, v)
        }
    }
    return result
}

场景四:二维切片

// 创建二维切片
matrix := make([][]int, rows)
for i := range matrix {
    matrix[i] = make([]int, cols)
}

// 或者使用一维切片模拟二维(性能更好,内存连续)
flat := make([]int, rows*cols)
// 访问 matrix[i][j] 等价于 flat[i*cols+j]

注意事项

  • 切片作为函数参数时,传递的是 slice 结构体的副本,但底层数组是共享的。函数内 append 不会影响原切片的 len/cap,但修改元素会影响原切片
  • append 返回的新切片必须用原变量接收:s = append(s, x)
  • 大切片复制有性能开销,尽量预分配容量避免频繁扩容
  • 切片引用大数组的一小部分时,原数组无法被 GC 回收。使用 copy 创建独立切片释放原数组
  • 并发读写同一个切片不安全,需要加锁或使用 sync 包

进阶用法

切片与内存泄漏

// 内存泄漏场景:切片引用大数组的尾部
func leak() []int {
    data := make([]int, 1000000) // 大数组
    return data[:10]             // 只返回前 10 个元素
    // 但底层数组仍占 1000000 个 int 的内存
}

// 修复:复制到新切片
func noLeak() []int {
    data := make([]int, 1000000)
    result := make([]int, 10)
    copy(result, data[:10])
    return result // 原大数组可以被 GC 回收
}

切片排序与搜索

import "sort"

s := []int{3, 1, 4, 1, 5, 9}

// 排序
sort.Ints(s)

// 二分搜索(切片必须已排序)
idx := sort.SearchInts(s, 4)

// 自定义排序
sort.Slice(s, func(i, j int) bool {
    return s[i] > s[j] // 降序
})

反射操作切片

import "reflect"

func sliceContains(slice, item interface{}) bool {
    s := reflect.ValueOf(slice)
    for i := 0; i < s.Len(); i++ {
        if reflect.DeepEqual(s.Index(i).Interface(), item) {
            return true
        }
    }
    return false
}

// 使用
found := sliceContains([]string{"a", "b", "c"}, "b") // true

泛型切片工具函数

// Go 1.18+ 泛型实现 Map 函数
func Map[T, U any](s []T, fn func(T) U) []U {
    result := make([]U, len(s))
    for i, v := range s {
        result[i] = fn(v)
    }
    return result
}

// 泛型 Filter 函数
func Filter[T any](s []T, fn func(T) bool) []T {
    result := make([]T, 0, len(s))
    for _, v := range s {
        if fn(v) {
            result = append(result, v)
        }
    }
    return result
}

// 使用示例
nums := []int{1, 2, 3, 4, 5}
doubled := Map(nums, func(n int) int { return n * 2 })    // [2, 4, 6, 8, 10]
evens := Filter(nums, func(n int) bool { return n%2 == 0 }) // [2, 4]