跳到主要内容

Slice(切片)

切片(slice)是对一段连续数组元素的动态视图。它记录可访问区域的长度和容量,并引用一个底层数组;切片本身不是数组,也不会直接保存全部元素。

创建切片

切片类型写作 []T,长度不属于类型。可以通过切片字面量直接创建并初始化:

package main

import "fmt"

func main() {
scores := []int{88, 92, 76}
fmt.Println(scores)
fmt.Println(len(scores))
}

也可以使用 make 指定长度,以及可选的容量:

names := make([]string, 3) // len=3, cap=3
buffer := make([]byte, 0, 64) // len=0, cap=64

make([]T, length, capacity) 会为元素准备底层存储,且必须满足 0 <= length <= capacity。新元素都取得其类型的零值。

数组与切片的类型不同:[3]int 是长度固定的数组类型,[]int 是切片类型。 数组的复制、比较和切片转换规则见 Array(数组)

长度与容量

  • len(slice) 返回当前可以访问的元素数量。
  • cap(slice) 返回从切片起点到底层数组末尾的元素容量。

切片表达式 values[low:high] 产生一个新切片,包含索引 lowhigh-1 的元素:

package main

import "fmt"

func main() {
values := []int{10, 20, 30, 40, 50}
window := values[1:4]

fmt.Println(window) // [20 30 40]
fmt.Println(len(window)) // 3
fmt.Println(cap(window)) // 4
}

省略 low 时默认为 0,省略 high 时默认为当前切片长度。索引或切片边界超出允许范围会在运行时触发 panic。

切片共享底层数组

从数组或另一个切片得到的新切片通常共享底层数组。通过任一切片修改元素,其他覆盖该元素的切片都能观察到变化:

package main

import "fmt"

func main() {
all := []string{"Go", "TypeScript", "Rust"}
selected := all[:2]
selected[0] = "Golang"

fmt.Println(all) // [Golang TypeScript Rust]
}

把一个切片赋给另一个变量,只复制切片值,不复制元素:

copyOfHeader := selected
copyOfHeader[1] = "TS"

此时两个切片仍可能引用同一底层数组。需要独立元素副本时,应显式复制元素。

使用 append 追加元素

append 返回追加后的新切片,必须接收返回值:

numbers := []int{1, 2}
numbers = append(numbers, 3)
numbers = append(numbers, 4, 5)

容量足够时,append 可以复用原底层数组;容量不足时,会分配新的底层数组并复制原有元素。容量的具体增长策略属于实现细节,不应依赖。

这种行为会使共享关系发生变化:

package main

import "fmt"

func main() {
base := []int{1, 2, 3}
left := base[:2]
right := append(left, 9)

fmt.Println(base) // [1 2 9]
fmt.Println(right) // [1 2 9]
}

left 的容量仍覆盖 base[2],所以这次追加复用了底层数组并覆盖原值。若调用方不应写入切片尾部,可以使用完整切片表达式限制容量:

left := base[:2:2]
right := append(left, 9) // 必须使用新的底层数组

复制切片

内置 copy(destination, source) 复制两者长度中较小数量的元素,并返回复制数量。源和目标区域重叠时也可以安全复制:

package main

import "fmt"

func main() {
source := []int{2, 4, 6}
cloned := make([]int, len(source))
count := copy(cloned, source)

cloned[0] = 100
fmt.Println(count) // 3
fmt.Println(source) // [2 4 6]
fmt.Println(cloned) // [100 4 6]
}

复制是浅复制。如果元素本身包含切片、映射或指针,复制后的元素仍可能引用相同数据。

nil 切片与空切片

切片的零值是 nil

var unset []int
empty := []int{}
allocated := make([]int, 0)

三者的长度都是 0,都可以直接 rangeappend。但只有 unset == niltrue。切片只能与 nil 比较,不能使用 == 比较两个切片的元素。

在 JSON 等序列化场景中,nil 切片与空切片可能产生不同结果;API 是否区分两者应由接口契约决定,而不是仅凭个人习惯。

删除与清空元素

切片没有内置的删除操作。删除索引 index 处的元素,可以拼接前后两段:

values = append(values[:index], values[index+1:]...)

这会移动后续元素,并保留原底层数组。若元素包含指针且底层数组会长期存活,可先把不再使用的位置设为零值,避免它继续引用对象。

Go 1.21 起,内置 clear(values) 会把现有元素全部设为零值,但不会改变切片的长度或容量。若只想重置长度,可以使用 values = values[:0];这仍保留底层数组以便复用。

避免意外保留大数组

一个很小的子切片仍会引用原来的整个底层数组,可能使大块内存无法被垃圾回收。需要长期保存少量数据时,创建独立副本:

small := append([]byte(nil), large[start:end]...)

是否值得复制取决于数据大小、存活时间和调用频率。性能判断应通过实际测量,而不是把所有子切片都机械地复制。

并发访问

切片本身不提供并发同步。多个 goroutine 并发读写共享元素,或在共享切片上执行可能改变长度和底层数组的操作,必须由程序建立同步关系。可以使用互斥锁、通道或明确的所有权转移,并用 go test -race 检查数据竞态。

与 TypeScript 数组的关键差异

TypeScript 的 Array<T> 是 JavaScript 对象;Go 切片是带有长度、容量并引用底层数组的值。两者都支持按索引访问和动态追加,但 Go 切片的赋值、再切片和 append 会形成特有的底层数组共享关系,不能直接套用 JavaScript 数组的复制经验。

需要验证多个 goroutine 是否错误共享切片时,结合 Sync(同步原语) 学习,并运行 go test -race ./...

小结

  • 切片是对底层数组一段区域的动态视图,长度和容量含义不同。
  • 赋值和再切片通常共享元素,独立副本需要显式复制。
  • append 可能复用原数组,也可能重新分配;始终接收其返回值。
  • nil 切片可正常遍历和追加,但它与非 nil 空切片在部分接口中可被区分。
  • 长期保存小子切片、并发修改切片时,需要额外考虑内存保留和同步。