← [ BACK_TO_NOTES ]

Go Slice

Slice 是底层数组的轻量描述符:指针、长度与容量如何决定共享、append、复制与内存保留;以及 nil 与 empty 的工程差异。

#concept #growing #dev #go

[!info] related notes

Go Slice

这个概念为什么会出现

数组在 Go 里是值类型,长度属于类型:[3]int 与 [4]int 不同。这让数组适合固定大小,却不适合:

  • 长度在运行时才知道
  • 函数间传递大段数据又不想复制整个数组
  • 对同一块数据做“窗口式”观察(前半段、过滤后片段)

于是 Go 提供 slice:不是“可变长数组对象”本身,而是对底层数组某一段的描述符。几乎所有 Go 数据处理、I/O 缓冲、API 列表参数都建立在这个模型上。若把 slice 想成 JavaScript 的 Array 引用对象,后续会在 append、共享修改、内存泄漏上连续踩坑。

[!abstract] 一句话理解 Slice 是 (pointer, len, cap) 描述符:复制 slice 只复制描述符;是否共享数据取决于是否仍指向同一底层数组,append 可能复用也可能换新数组。

最小可运行示例

先把示例放进业务场景,再看代码:

场景:处理订单 ID 列表时的“窗口视图”与“独立拷贝”

后台任务拿到一批订单 ID:[1001, 1002, 1003, 1004]。
某一步只想处理中间一段(分页窗口、批内子任务),会写成 ids[1:3]。

关键问题:

  1. 子切片共享底层数组——改窗口可能改到整批数据
  2. append 在容量够时也可能写穿到原切片后面的元素
  3. 若接收方需要独立修改,或原缓冲会复用,应显式拷贝;只读共享或明确转移所有权时不必复制
package main

import "fmt"

func main() {
	// 整批订单 ID(可理解为一次拉取的 buffer)
	ids := []int{1001, 1002, 1003, 1004}

	// window:只关注中间两单,像“本 worker 的分片视图”
	// len=2;cap=3,从索引 1 延伸到原切片容量边界
	window := ids[1:3]
	// 改窗口第 0 个元素 → 实际改的是底层数组里原来的 1002
	window[0] = 9999
	fmt.Println("after in-place edit:", ids) // [1001 9999 1003 1004]

	// window 的 len=2、cap=3,追加一个元素会复用原数组。
	// 新元素写到 ids[3],覆盖原来的 1004。
	window = append(window, 7)
	fmt.Println("ids after append on window:", ids)
	fmt.Println("window:", window)

	// 需要独立所有权时:拷贝一份再改(交给缓存/别的请求互不影响)
	// append([]int(nil), ids...) 是常见的“新底层数组”写法
	owned := append([]int(nil), ids...)
	owned[0] = 42
	fmt.Println("original ids:", ids)
	fmt.Println("owned copy:", owned)
}

建议运行:

go run .

期望输出(本例容量足够,append 会改写原 ids 尾部):

after in-place edit: [1001 9999 1003 1004]
ids after append on window: [1001 9999 1003 7]
window: [9999 1003 7]
original ids: [1001 9999 1003 7]
owned copy: [42 9999 1003 7]

本例输出由 len/cap 规则确定;若改成 ids[1:3:3] 限制窗口容量,追加时就会分配新数组。

结合场景再看三个关注点

  1. 子切片是窗口,不是深拷贝
    改 window[0] 等于改整批 ids 里对应元素——分片处理时很容易误伤。

  2. append 可能复用底层数组
    容量足够时继续写同一块内存,可能覆盖原切片“窗口外”的元素。

  3. 要独立所有权就显式复制
    append([]int(nil), src...) 或 copy 到新切片可分离本例的整数元素;元素含指针、slice 或 map 时,仍须考虑其引用的共享数据。

核心概念与准确模型

数组 vs slice

数组slice
类型例子[3]int[]int
长度类型的一部分运行时 len
传递复制全部元素复制描述符
零值元素全零nil

描述符模型

语言与官方博文将 slice 理解为包含:

  1. 指向底层数组某元素的指针
  2. 长度 len:可见元素个数
  3. 容量 cap:从当前起点可重新切片到的最大长度;完整切片表达式可将它限制在底层数组末尾之前

这是语义模型。当前 Runtime 的具体结构体布局属于实现细节,但 len/cap/共享关系由规范与官方文档保证其使用语义。

flowchart LR
    S["slice 描述符"] --> P["ptr"]
    S --> L["len"]
    S --> C["cap"]
    P --> A["底层数组"]

参考:Go Slices: usage and internals、Spec — Slice types。

创建方式

var s []int                 // nil slice
t := []int{1, 2, 3}         // 字面量
u := make([]int, 3)         // len=3, cap=3,元素为零值
v := make([]int, 0, 8)      // len=0, cap=8
w := a[1:4]                 // 从数组或 slice 切片

切片表达式

对长度 n 的数组或 slice:

s[low:high]      // len = high-low,cap 到原 cap 末尾
s[low:high:max]  // 完整切片形式,cap = max-low

完整形式用于截断容量,防止 append 误写原数组后续元素:

head := s[:n:n]
head = append(head, x) // len==cap,追加一个元素会分配新数组,不覆盖 s[n]

append

s = append(s, elem)
s = append(s, elems...)

规则直觉:

  1. 若 len+新增 <= cap,复用底层数组,可能覆盖原 cap 范围内旧数据的“可见之外”槽位。
  2. 若容量不足,分配新数组、复制旧元素、返回新描述符。
  3. 必须接收返回值:s = append(s, x)。
  4. 扩容策略是实现细节,不可依赖固定倍数写生产逻辑。

copy

n := copy(dst, src) // n = 复制的元素个数 = min(len(dst), len(src))

copy 只按 len 复制,不改变 cap。

nil 与 empty

见 nil 的多种形态。工程上:

  • 无元素判断常用 len(s)==0
  • 序列化是否区分 null/[] 要显式约定

从共享到底层:所有权

a := []int{1, 2, 3, 4, 5}
b := a[1:4]
b[0] = 9
// a[1] 也是 9

当多个 slice 共享数组时,要明确:

  • 谁拥有写入权
  • 函数返回子切片是否仍引用大正文/大缓冲(内存保留问题)

典型泄漏模式:从巨大 buffer 切出短字符串/切片并长期保存,导致整个底层数组无法回收。缓解:

small := append([]byte(nil), big[i:j]...)
// 或 strings.Clone (Go 1.18+) 对 string 场景

设计动机

  1. 数组太硬,引用类型又太藏
    slice 把“视图”做一等公民,同时复制描述符保持传值一致性。
  2. 保持 Go 的值传递规则 传 slice 仍是值复制,只是值里有指针字段。
  3. 标准库统一序列抽象
    io、sort、bytes、slices 包都以 slice 为中心。

边界情况与反直觉行为

1. 越界索引 panic,越界切片也可能 panic

s := []int{1, 2}
// _ = s[2]     // panic
// _ = s[:3]    // panic:本例 cap=2,high=3 超过容量

2. append 可能悄悄共享或断开

容量足够时修改新 slice 可能改写旧 slice 视野外的底层槽;容量不足则断开。不要假设“append 后一定独立”。

3. 三索引切片截断 cap

s := []int{1, 2, 3, 4}
t := s[:2:2]
t = append(t, 9)
fmt.Println(s) // [1 2 3 4];t 的 len==cap==2,追加必定使用新数组

4. 在 range 里 append 原切片

边遍历边 append 到同一底层数据易乱;先规划目标切片或使用索引循环。

5. 元素是指针时的浅共享

复制 slice 描述符或 copy 元素都只复制指针值,不深拷贝目标对象。

常见误区

[!warning] 常见误区:把 slice 当成引用类型变量本身 错误:以为 func f(s []int) 里 s = append(s, 1) 一定影响调用方。
正确:描述符按值传入;append 可能返回新描述符,调用方必须用返回值或改元素。

[!warning] 常见误区:背诵扩容倍数 错误:写死“一定 2 倍”。
正确:容量策略是实现细节,逻辑只依赖 len/cap 语义。

[!warning] 常见误区:s == nil 判断空 错误:漏掉 empty 非 nil。
正确:多数业务用 len(s)==0。

[!warning] 常见误区:长期保存大缓冲的子切片 错误:return buf[:n] 挂住整个 buf。
正确:需要独立生命周期就 copy/clone。

与相邻概念对比

概念差异
数组长度在类型中;赋值复制全部元素
map键值哈希;无序;nil 不可写
string不可变字节序列;切片得 string 仍共享底层(实现可优化)
指针显式地址;slice 内含指针字段但还有 len/cap

工程实践

  1. API 输入 slice:文档说明会否保留引用、会否修改元素。
  2. API 输出 slice:若来自内部大缓冲,考虑复制后返回。
  3. 预分配:已知长度用 make([]T, 0, n) 减少 append 分配。
  4. 删除元素:注意保持顺序与否;可用 append(s[:i], s[i+1]...) 并留意共享。
  5. 排序与搜索:sort / slices 包。
  6. 并发:slice 描述符可各自持有;对同一底层元素的并发访问只要含写入就需同步,共享描述符本身的读写也需同步。
  7. 测试:对共享/复制行为写表驱动用例。
// 独立克隆的惯用写法之一
func Clone[T any](s []T) []T {
	if s == nil {
		return nil
	}
	out := make([]T, len(s))
	copy(out, s)
	return out
}

(Go 1.21+ 标准库亦提供 slices.Clone。)

可验证实验

实验 1:共享

修改子切片元素,打印原切片。

实验 2:append 与 cap

s := make([]int, 2, 4)
fmt.Println(len(s), cap(s))
s2 := append(s, 9, 9)
s2[0] = 7
fmt.Println(s, s2)
s3 := append(s2, 1, 2, 3) // 新长度 7 超过 cap=4,必定分配新数组
s3[0] = 1
fmt.Println(s2, s3)

观察何时共享断开。

实验 3:完整切片

s[:1:1] 后 append,检查是否影响 s[1]。

实验 4:nil append

var s []int; s = append(s, 1) 应成功。

本节总结

  • 本质:slice 是数组片段的描述符,不是数组对象本身。
  • 关键规则:复制描述符、共享数据、append 必须接返回值、扩容策略非规范承诺。
  • 最易错:所有权不清、子切片钉死大内存、把 slice 当引用对象。
  • 下一步:map 的零值与并发边界;值语义 总览。

自测题

概念题

  1. slice 描述符通常包含哪三个概念字段?
  2. 为什么 func f(s []int) { s[0]=1 } 能改调用方元素,而 s=append(s,1) 不一定?
  3. nil slice 与 empty slice 在 len 与 == nil 上有何不同?

代码推理题

a := []int{1, 2, 3, 4}
b := a[1:3]
b = append(b, 8)
fmt.Println(a, b)

a 与 b 分别是什么?追加写入原数组的哪个索引?

工程思考题

日志库从 64KB 缓冲解析出一行 []byte 异步发送到 channel。如何避免内存被整块缓冲卡住?

参考答案

展开
  1. 指针、len、cap。
  2. 改元素是通过共享数组;append 可能返回新描述符且形参复制。
  3. 二者 len 都可为 0;仅 nil 满足 == nil。
    代码题:len(b)=2、cap(b)=3,追加复用原数组并写入 a[3];a 为 [1 2 3 8],b 为 [2 3 8]。 工程题:append([]byte(nil), line...) 或等价 clone 后再发送。

延伸阅读与资料来源

资料类型支撑
Spec — Slice types规范切片表达式、类型
Go Slices: usage and internals官方博客描述符、append、共享
Arrays, slices (and strings)官方博客更深的内部与字符串关系
Tour — Nil slices / append教程基础行为
slices package标准库Clone、排序等现代辅助函数
创建于 2026/6/20 更新于 2026/10/6
Digital Biome
Access

键盘导航

j / k / ↑ / ↓
选择对象
Enter / o
打开选择
/ 或 Ctrl+K
全站搜索
i / f / 1–4
筛选当前列表
g → h / l / p / t / a
首页 / 笔记 / 项目 / 工具 / About
r → 1–9
打开所选对象的关联
u
返回上一页
Esc
关闭浮层 / 取消命令;输入框内清空文本