Go Slice 详解

Go Slice 详解

一、Slice 是什么

Slice(切片)是 Go 中对数组的轻量级抽象 ,本质是一个引用类型的描述符,包含三个字段:

复制代码
┌─────────────┬──────┬──────┐
│    ptr      │ len  │ cap  │
│  *array     │  int │  int │
└─────────────┴──────┴──────┘
  • ptr --- 指向底层数组的指针
  • len --- 当前元素个数(可读写的范围)
  • cap --- 底层数组从 ptr 开始到末尾的容量
go 复制代码
s := make([]int, 3, 5)
// ptr -> [0, 0, 0, _, _]
// len = 3, cap = 5

二、创建方式

go 复制代码
// 1. 字面量
s1 := []int{1, 2, 3}

// 2. make
s2 := make([]int, 3)       // len=3, cap=3, 零值
s3 := make([]int, 3, 10)   // len=3, cap=10

// 3. 从数组/切片切取
arr := [5]int{0, 1, 2, 3, 4}
s4 := arr[1:3]  // [1, 2], len=2, cap=4 (从index1到数组末尾)

// 4. new --- 不推荐,得到的是 *[]int,且 len/cap 都为 0
s5 := new([]int)

三、扩容机制(核心原理)

append 导致 len > cap 时,Go 会分配更大的底层数组并拷贝数据:

go 复制代码
s := make([]int, 0, 2)
s = append(s, 1)   // [1], cap=2
s = append(s, 2)   // [1,2], cap=2 --- 刚好满
s = append(s, 3)   // [1,2,3], cap=4 --- 触发扩容,ptr 已变!

扩容策略(Go 1.18+)

复制代码
newcap = oldcap

如果 oldcap < 256:
    newcap += newcap  (翻倍)
如果 oldcap >= 256:
    newcap += newcap * 3/4  (每次增长 25%)

// 再考虑元素大小和内存对齐做微调

关键点:扩容后 ptr 指向新数组,旧数组等待 GC。


四、常见操作

增删改查

go 复制代码
// 追加
s = append(s, 4)

// 批量追加
s = append(s, []int{5, 6, 7}...)

// 插入到索引 i
s = append(s[:i], append([]int{x}, s[i:]...)...)

//插入可以看下面三种方式
// 方法 1:三行展开,逻辑清晰
func insert(s []int, i int, x int) []int {
 tail := append([]int{x}, s[i:]...) // [x, 原i及之后的所有元素]
 return append(s[:i], tail...) // [原i之前, x, 原i及之后]
}

// 方法 2:append + copy(Go Wiki 推荐,最清晰)
func insert(s []int, i int, x int) []int {
 s = append(s, 0) // 先扩一个位置
 copy(s[i+1:], s[i:]) // 把 i 及之后的内容整体右移一位
 s[i] = x // 在 i 位置放入 x
 return s
}

// 方法 3:Go 1.21+ slices 包
import "slices"
s = slices.Insert(s, i, x) // 直接用标准库

// 删除索引 i
s = append(s[:i], s[i+1:]...)

// 删除范围 [i, j)
s = append(s[:i], s[j:]...)

拷贝

go 复制代码
dst := make([]int, len(src))
copy(dst, src)  // 内置函数,处理了重叠内存

copy 返回实际拷贝的元素数 = min(len(dst), len(src))
如果 dst 比 src 短,只拷贝前面部分,不会 panic
copy 内部自动处理了重叠内存(类似 C 的 memmove,不是 memcpy),所以同一个 slice 内拷贝是安全的

为什么安全:memmove 会先判断重叠方向,必要时从后往前拷贝,避免数据还没读就被覆盖。

copy 是 Go 里做 slice 深拷贝的唯一内置方式,等价于 memmove(处理重叠),拷贝数量 = min(len(dst), len(src)),类型必须严格匹配。

和赋值的区别
a := s // 只拷贝 slice 头部(24 字节),共享底层数组
b := make([]int, len(s))
copy(b, s) // 深拷贝,独立的底层数组


// 截断(原地缩容,不改变 cap)
s = s[:n]
只是改了 len,cap 不变,底层数组不变
被截掉的部分还在内存中(cap 保留),不会立即释放
指针不动,只是 header 的 len 字段变小了

切片表达式

go 复制代码
a := [5]int{0, 1, 2, 3, 4}
s1 := a[1:3]     // [1, 2], len=2, cap=4
s2 := a[1:3:3]   // [1, 2], len=2, cap=2  ← 3-index slice

3-index slice a[low:high:max] --- 显式限制 cap,防止通过 append 意外修改原数组后续元素。Go 1.2+ 支持。


五、经典陷阱

1. 共享底层数组

go 复制代码
a := []int{1, 2, 3, 4, 5}
b := a[1:3]  // b -> [2, 3]

b[0] = 99
fmt.Println(a) // [1, 99, 3, 4, 5] --- a 也变了!

2. append 未捕获返回值

go 复制代码
s := []int{1, 2, 3}
append(s, 4)  // ❌ 丢弃返回值,如果触发扩容则完全无效
s = append(s, 4)  // ✅ 必须接收

3. 切片作函数参数 --- 修改可见

go 复制代码
func modify(s []int) {
    s[0] = 999  // 影响原切片
    s = append(s, 4)  // 如果扩容了,外部看不到
}

4. 大切片导致内存泄漏

go 复制代码
// 从大切片取一小段,底层数组不会被回收
var big = make([]byte, 1<<30)
small := big[:100]

// 解决:手动拷贝
small := make([]byte, 100)
copy(small, big[:100])

六、Slice vs Array 对比

Array Slice
类型签名 [3]int --- 长度是类型的一部分 []int --- 长度不固定
值语义 拷贝整个数组 拷贝的是描述符(24字节)
可否 == 比较 可以(同类型同长度) 不可以 ,只能和 nil
零值 元素零值 nil(len=0, cap=0, ptr=nil)
长度 编译期确定 运行时动态

七、最佳实践

  1. 预知大小时用 make([]T, 0, n) --- 避免多次扩容
  2. 需要传值语义时用 copy,不要依赖切片共享
  3. var s []int vs s := []int{} --- 前者是 nil,后者是空切片(非 nil),json.Marshal 行为不同:nilnull[][]
  4. 字符串转 []byte 会拷贝,频繁操作用 unsafe.Stringstrings.Builder
  5. 3-index slice 在从数组切取子切片时优先使用,防止意外覆盖
相关推荐
kkeeper~1 天前
0基础C语言积跬步之数据在内存中的存储
c语言·数据结构·算法
鹏北海-RemHusband1 天前
Go 语言进阶笔记 — 面向 JS/TS 前端开发者
笔记·golang
wabs6661 天前
关于贪心算法的一些自我总结【力扣45.跳跃游戏II】【灵感来源:代码随想录】
算法·贪心算法·复盘
2401_876964131 天前
【湖北专升本】2026湖北专升本真题PDF+备考资料汇总
数据结构·人工智能·经验分享·深度学习·算法·计算机视觉
嗝o゚1 天前
CANN GE 算子融合——融合算法与调度策略
算法·昇腾·cann·ge
小江的记录本1 天前
【JVM虚拟机】垃圾回收GC:垃圾回收算法:标记-清除、标记-复制、标记-整理、分代收集(附《思维导图》+《面试高频考点清单》)
java·jvm·后端·python·算法·安全·面试
Ulyanov1 天前
用声明式语法重新定义Python桌面UI:QML+PySide6现代开发入门(一)
开发语言·python·算法·ui·系统仿真·雷达电子对抗仿真
数据科学小丫1 天前
特征工程处理
人工智能·算法·机器学习
z落落1 天前
C#参数区别
java·算法·c#
c238561 天前
vector(下)
数据结构·算法