Slice(切片)是 Go 语言内置的可变长度序列,属于引用类型,基于底层数组封装实现,解决数组长度固定、数组传参完整拷贝开销大的问题,是 Go 开发最常用的数据结构之一。

1. Slice 的基础特性

  • 可变长度:长度不固定,可通过 append 动态追加元素。
  • 底层数组共享:切片本身不存储实际元素,元素存放在底层数组;多个切片可以指向同一个底层数组,修改会互相影响。
  • len 与 cap 分离:len代表切片当前可用元素个数;cap代表底层数组允许的最大容量。
  • 零值特性:切片零值为 nil,nil slice len=0、cap=0;nil slice 和空切片 []int{} 不相等。
  • 引用语义:切片变量存储的是结构体头信息,赋值、传参拷贝的是切片头,不是底层数组。
  • 不可直接比较:切片不能使用 == 判断两个切片内容是否相等,仅允许和 nil 做相等判断。

2. Slice 的四种初始化方式

切片必须初始化,nil切片可以读取、append,但不能直接对索引位置赋值。

2.1 声明未初始化(nil slice)

仅定义变量,底层数组指针为 nil,len=0、cap=0。

// 声明nil切片,未分配底层数组
var a []string
fmt.Println(a == nil) // true
fmt.Println(len(a), cap(a)) // 0 0

// a[0] = "hello"
// 未初始化的切片直接赋值会引发 panic
// panic: index out of range

2.2 字面量初始化(空切片/带初始值)

直接初始化,分配底层数组,支持空切片和预设元素。

// 1. 空切片,不为nil
s1 := []int{}

// 2. 带初始元素
s2 := []int{11,22,33}

fmt.Println(s1 == nil) // false
fmt.Println(s2) // [11 22 33]

2.3 make 函数初始化

使用内置函数make([]T, len, cap)进行初始化操作,cap 可省略,省略时 cap 等于 len。适合预先知道数据量级的场景,可以减少扩容开销。

// len=3,cap=5
s := make([]int, 3, 5)
fmt.Println(len(s), cap(s)) //3 5

// 未赋值位置为元素零值
fmt.Println(s) // [0 0 0]

2.4 从数组/切片表达式生成切片

基于已有数组或者切片,通过切片表达式生成新切片,新切片和原数据共享底层数组。

arr := [10]int{1,2,3,4,5,6,7,8,9,10}
// low:high,左闭右开
s1 := arr[1:6]
fmt.Println(s1) // [2 3 4 5 6]

// low:high:max,max 用来限制新切片的最大 cap
s2 := arr[1:6:8]
fmt.Println(len(s2), cap(s2)) // 5 7

3. Slice 常用操作

切片常用操作:索引读写、追加、切片表达式、拷贝、删除元素、获取长度容量。

3.1 索引读写

可以通过下标直接读写切片元素,下标越界会触发 panic。

s := []int{10,20,30}

// 读
v := s[0]
fmt.Println(v) // 10


// 写
s[1] = 200
fmt.Println(s) // [10 200 30]

3.2 append 追加元素

Go 内置append(slice, elem...)函数,用来向切片尾部追加元素,必须接收返回值。

  • 如果底层数组剩余容量足够,直接复用底层数组,修改会影响同源切片。
  • 如果容量不足,触发自动扩容,分配全新底层数组,和旧底层数组解绑。
numbers := []int{}
numbers = append(numbers, 1,2,3)
numbers = append(numbers, []int{4,5}...) //追加切片需要解包...
fmt.Println(numbers) // [1 2 3 4 5]

3.3 切片表达式

语法 s[low:high:max]

  • low:起始下标,包含;省略默认为 0。
  • high:结束下标,不包含;省略默认为 len(s)。
  • max:可选,控制新切片的容量 cap;省略则 cap 继承原切片的 cap。
  • 新切片:len = high‑low,cap = max‑low
s1 := []int{1,2,3,4,5,6,7,8,9,10}
s2 := s1[1:6:8]
fmt.Println("len:",len(s2),"cap:",cap(s2)) // len=5 cap=7

这里有两个坑需要注意:

  1. 由于切片表达式产生的切片和原切片共享底层数组,修改其中一个会影响另一个。
  2. append 触发扩容后,新切片会生成一个新数组,此时新旧切片解绑,又互不影响。

3.4 copy 拷贝切片

直接赋值切片只是拷贝切片头,共享底层数组。想要创建一个完全独立的副本,需要使用内置函数copy(dst, src []T)。

  • dst:目标 Slice
  • src:数据来源 Slice
a := []int{1,2,3,4,5}
b := make([]int, 5)
copy(b, a)

b[0] = 1000
fmt.Println(a) // [1 2 3 4 5],a不受b修改影响
fmt.Println(b) // [1000 2 3 4 5]

另外,copy 函数会返回实际拷贝对象的元素个数,拷贝数量取min(len(dst), len(src))。除非你知道你在做什么,否则最好预先给目标进行初始化且长度也保持一致。

a := []int{1, 2, 3, 4, 5}
b := make([]int, 7)
c := make([]int, 3)
var d []int
copy(b, a)
copy(c, a)
copy(c, d)

fmt.Println(b) // [1 2 3 4 5 0 0]
fmt.Println(c) // [1 2 3]
fmt.Println(d) // []

3.5 删除切片元素

Go 没有内置函数用于从 Slice 中删除元素,想要删除元素,可以使用 append 函数配合切片表达式来新建一个仅包含所需元素的 Slice 来实现。

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

// 删除下标为2的元素
a = append(a[:2], a[3:]...)

fmt.Println(a) // [1 2 4 5]

3.6 获取长度与容量

  • len(slice):获取切片当前有效元素数量
  • cap(slice):获取切片底层数组容量
s := make([]int,3,8)
fmt.Println(len(s), cap(s)) //3 8

4. Slice 底层实现原理

4.1 运行时切片结构体

切片在 runtime 中是结构体,源码位于runtime/slice.go

type slice struct {
    array unsafe.Pointer // 指向底层数组的指针
    len  int             // 当前切片长度
    cap  int             // 底层数组容量
}
  • array:指向底层数组第一个元素地址;nil slice 该指针为 nil。
  • len:程序可以访问的元素个数,超过索引长度会触发 panic。
  • cap:底层数组总空间,append 在不超过 cap 时复用数组。

切片运行时示意图

上图为一个 Slice 的初始化过程,可以看到,Go 运行时为 Slice 创建了一个底层数组用来容纳 Slice 的值。Slice 可以理解成一个对底层数组的包装,操作 Slice,实际上就是对底层的数组进行操作。

4.2 切片寻址与读写流程

  1. 通过 slice 结构体拿到底层数组指针 array;
  2. 访问s[i]等价于 *(array + i *元素类型大小);
  3. 索引 i 必须小于 len,否则张引发越界 panic。

4.3 append 底层流程

  1. 判断len(s) + 新增元素个数 <= cap(s)
    • 条件成立:直接将新元素写到底层数组 len 位置后,修改 slice 的 len,返回新 slice 头;底层数组不变。
  2. 条件不成立:触发扩容
    • 按扩容规则计算新 cap;
    • 在堆上分配全新底层数组;
    • 将旧数组全部元素拷贝到新数组;
    • 把待追加元素写入新数组;
    • 返回指向新数组的 slice 结构体,旧 slice 仍然指向旧数组。

切片扩容规则:

  • 旧数组容量小于等于 256 时,每次扩容时容量翻倍。
  • 旧数组容量大于 256 后,会通过一个公式进行计算,扩容倍数会慢慢由 1.63 降低到原容量的约 1.25 倍,减少内存浪费。

具体细节参见源码src/runtime/slice.go中的nextslicecap函数。

5. Slice 常见错误

5.1 错误1:nil 切片直接索引赋值

var s []int
s[0] = 10 // panic: index out of range

原因分析:nil 切片没有底层数组,len=0,没有可访问下标。 正确做法:使用 make函数或字面量初始化切片。

5.2 错误2:切片共享底层数组,修改互相污染

s1 := []int{1,2,3,4}
s2 := s1[:3]

s2[0] = 99
fmt.Println(s1) // s1也被修改 [99 2 3 4]

原因分析:切片表达式生成切片共享底层数组。 正确做法:需要独立数据,使用 copy 函数拷贝一份新切片。

5.3 错误3:append 不接收返回值

var s []int
append(s,1,2,3) // 丢弃返回值,s没有变化

fmt.Println(s) // []

原因分析:append 返回新 slice 结构体;扩容时会产生全新切片头,原变量不会自动更新。 正确做法:s = append(s,1,2,3)。

5.4 错误4:在 for range 中修改切片元素

s := []int{10,20,30}
for _,v := range s {
    v = v*2 //v是拷贝副本,不会修改原切片
}
fmt.Println(s) // [10 20 30]

原因分析:for range 遍历出来的元素值赋值给 v,这里的 v 是值副本,并不是指针。 正确做法:使用下标访问原切片

for i := range s {
    s[i] = s[i]*2
}

5.5 错误5:用==直接比较两个切片

a := []int{1,2}
b := []int{1,2}

fmt.Println(a == b) // 编译报错,invalid operation: a == b (slice can only be compared to nil)

原因分析:Go 语言中切片只能和 nil 进行比较,切片之间不支持使用==进行比较。 正确做法:Go 1.21 前需要自己循环逐个对比元素,1.21m之后可以使用 slices 标准库的 slices.Equal 函数。

package main

import (
	"fmt"
	"slices"
)

func equalSlice[T comparable](a, b []T) bool {
	if len(a) != len(b) {
		return false
	}
	for i := range a {
		if a[i] != b[i] {
			return false
		}
	}
	return true
}

func main() {
	a := []int{1, 2, 3}
	b := []int{1, 2, 3}
	c := []int{1, 2}

	fmt.Println(slices.Equal(a, b)) // true
	fmt.Println(slices.Equal(a, c)) // false
	fmt.Println(equalSlice[int](a, b)) // true
}

5.6 错误6:切片删除元素产生内存泄漏

删除中间元素使用append(a[:i],a[i+1:]...),被删掉的元素还存在底层数组,切片头只是缩短 len,如果底层数组持有大对象指针,对象不会被 GC。

package main

import "fmt"

type BigData struct {
    Data []byte // 假设占用大量内存
}

func main() {
    // 创建一个指针切片
    items := make([]*BigData, 1000)
    for i := range items {
        items[i] = &BigData{Data: make([]byte, 1<<20)} // 每个 1MB
    }

    // 删除前 990 个元素,只保留最后 10 个
    items = items[990:]

    fmt.Println("len:", len(items), "cap:", cap(items))
    // 输出: len: 10, cap: 1000
}

原因分析:

  1. items[990:] 只改变了切片的起始偏移和长度,底层数组仍完整保留,cap 仍为 1000。
  2. 前 990 个 *BigData 指针看似被“删除”,实际仍存在于底层数组的前 990 个槽位,GC 仍能通过这些指针找到对应的 BigData 对象,因此不会回收它们。
  3. 若该切片长期存活(如全局变量或长生命周期结构体字段),这 990 个大对象将一直占用内存。

正确做法:删除后手动置空被遗弃位置元素,帮助 GC 回收。

// 方法1:手动置 nil 再截断(经典做法)
for i := 0; i < 990; i++ {
    items[i] = nil
}
items = items[990:]

// 方法2:复制到新切片,彻底切断引用
items = append([]*BigData{}, items[990:]...)

// 方法3:使用 Go 1.22+ 的 slices.Delete(内部已用 clear 处理尾部)
import "slices"
items = slices.Delete(items, 0, 990)