slice
约 1692 字大约 6 分钟
2025-12-06
slice 的底层结构
在 Go 中,slice 并不是数组或数组指针。slice 本质上是对底层数组某一段的引用,由指针、长度、容量三个字段组成,从而支持变长
底层结构(runtime/slice.go):
type slice struct {
array unsafe.Pointer
len int
cap int
}切片增长(append)
切片有三个属性,指针(array)、长度(len) 和容量(cap)。append 时有两种场景:
- 当 append 之后的长度小于等于 cap,将会直接利用原底层数组剩余的空间。
- 当 append 后的长度大于 cap 时,则会分配一个更大的新底层数组,并将原数据拷贝过去
因此,为了避免内存发生拷贝,如果能够知道最终的切片的大小,预先设置 cap 的值能够获得最好的性能。
扩容规则
Go 的扩容策略在 runtime.growslice 中实现(不同版本略有调整),以下源码基于 Go 1.21+
slice 扩容时,首先判断:若一次追加后所需长度超过原容量的 2 倍(newLen > 2*oldCap),则直接使用所需长度作为逻辑容量,跳过后续增长策略。否则按以下规则逐步扩容:当原容量小于 256 时按 2 倍增长;当原容量不小于 256 时,采用 newcap += (newcap + 3*threshold) >> 2 公式循环扩容,直到满足所需容量。该公式的增长率随 newcap 增大而渐进趋近 1.25 倍,但在 256~1024 范围内实际增长率明显高于 1.25 倍。
注意:
nextslicecap返回的只是逻辑容量,growslice最终还会通过roundupsize做内存对齐,因此实际分配的 cap 往往略大于计算值。
// nextslicecap computes the next appropriate slice length.
func nextslicecap(newLen, oldCap int) int {
newcap := oldCap
doublecap := newcap + newcap
if newLen > doublecap {
return newLen
}
const threshold = 256
if oldCap < threshold {
return doublecap
}
for {
// Transition from growing 2x for small slices
// to growing 1.25x for large slices. This formula
// gives a smooth-ish transition between the two.
newcap += (newcap + 3*threshold) >> 2
// We need to check `newcap >= newLen` and whether `newcap` overflowed.
// newLen is guaranteed to be larger than zero, hence
// when newcap overflows then `uint(newcap) > uint(newLen)`.
// This allows to check for both with the same comparison.
if uint(newcap) >= uint(newLen) {
break
}
}
// Set newcap to the requested cap when
// the newcap calculation overflowed.
if newcap <= 0 {
return newLen
}
return newcap
}扩容导致的陷阱
// 传参时拷贝了 slice header(array/len/cap 三个字段),
// 因此函数内对 len 的修改不会反映到调用方。
func f(a []int) {
a = append(a, 6, 7, 8, 9, 10)
// 因为发生了扩容,函数内切片和原切片指向的底层数组已经不是同一个了。
// 因此,对函数内切片第 0 个元素的修改,并不会影响原切片的第 0 个元素。
a[0] = 100
}
func main() {
a := []int{1, 2, 3, 4, 5}
f(a)
fmt.Println(a) // [1 2 3 4 5]
}如果希望 f 函数的操作能够影响原切片呢?
- 设置返回值,将新切片返回并赋值给 main 函数中的变量 a。
- 切片也使用指针方式传参。
- 提前分配好空间,避免 slice 扩容。(注意:这只能让元素修改影响原切片,
len的变化仍不会反映到调用方。)
切片截取与共享底层数组
通过 a[low:high] 截取得到的新切片,与原切片共享同一个底层数组。修改其中一个会影响另一个:
func main() {
a := []int{1, 2, 3, 4, 5}
b := a[1:3] // b == [2, 3],与 a 共享底层数组
b[0] = 100
fmt.Println(a) // [1 100 3 4 5],a 也被修改了
}如果希望截取后互不影响,可以使用 copy 进行深拷贝,或者在截取时通过三下标语法限制容量 a[low:high:max](其中 max 限定新切片的容量为 max - low),使后续 append 触发扩容从而分离底层数组。注意:三下标本身不会立即分离底层数组,在扩容发生前,对元素的修改仍会影响原切片。
func main() {
a := []int{1, 2, 3, 4, 5}
// 方式一:copy
b := make([]int, 2)
copy(b, a[1:3])
// 方式二:三下标限制容量,append 时触发扩容
c := a[1:3:3] // len=2, cap=2
c = append(c, 99) // 扩容,c 与 a 底层数组分离
b[0] = 100
c[0] = 101
fmt.Println(a) // [1 2 3 4 5],a 不受影响
fmt.Println(b) // [100 3]
fmt.Println(c) // [101 3 99]
}nil slice 与 empty slice
var a []int // nil slice: a == nil, len=0, cap=0
b := []int{} // empty slice: b != nil, len=0, cap=0两者在 len、cap、append、for range 上行为一致,但在以下场景表现不同:
== nil判断:a == nil为 true,b == nil为 false。- JSON 序列化:
a序列化为null,b序列化为[]。 reflect.DeepEqual(a, b)为 false。
实践建议:如果需要返回空集合给前端/API 调用方,优先使用 []int{} 或 make([]int, 0) 以避免返回 null。
copy
copy 是内置函数,用于在两个切片之间拷贝元素,返回实际拷贝的元素个数(取 src 和 dst 长度的较小值):
func main() {
src := []int{1, 2, 3, 4, 5}
dst := make([]int, 3)
n := copy(dst, src)
fmt.Println(dst, n) // [1 2 3] 3
}copy 不会自动扩容 dst,因此需要提前分配好目标切片的长度。
slice 的 GC 陷阱
当从一个大数组上截取一小段 slice 时,只要这个小 slice 还存活,整个底层大数组都无法被 GC 回收,容易造成内存泄漏:
func getHeader(data []byte) []byte {
// 虽然只需要前 512 字节,但底层的整个 data 数组都不会被 GC
return data[:512]
}解决方式是用 copy 将所需数据拷贝到一个新的切片,使原底层数组可以被回收:
func getHeader(data []byte) []byte {
header := make([]byte, 512)
copy(header, data[:512])
return header
}clear(Go 1.21+)
clear 是内置函数,可以将 slice 的所有元素置为零值,但不改变其 len 和 cap:
func main() {
a := []int{1, 2, 3, 4, 5}
clear(a)
fmt.Println(a) // [0 0 0 0 0]
fmt.Println(len(a)) // 5
}slices 标准库(Go 1.21+)
Go 1.21 引入了 slices 包,提供了泛型的切片操作函数,避免手写循环:
import "slices"
// 排序
slices.Sort(a)
// 查找
i, found := slices.BinarySearch(a, 3)
// 删除索引 1~2 的元素
a = slices.Delete(a, 1, 3)
// 去除连续重复元素;如需完全去重,需先排序后再 Compact
a = slices.Compact(a)
// 判断是否包含
ok := slices.Contains(a, 42)数组和切片
- 在 Go 语言中,数组是一种值类型,而且不同长度的数组属于不同的类型。例如 [2]int 和 [20]int 属于不同的类型。
- 当值类型作为参数传递时,参数是该值的一个拷贝,因此更改拷贝的值并不会影响原值。
func f(a [5]int) {
a[0] = 100
}
func main() {
a := [5]int{1, 2, 3, 4, 5}
f(a)
fmt.Println(a) // [1 2 3 4 5]
}