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] 产生一个新切片,包含索引 low 到 high-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,都可以直接 range 和 append。但只有 unset == nil 为 true。切片只能与 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空切片在部分接口中可被区分。- 长期保存小子切片、并发修改切片时,需要额外考虑内存保留和同步。