【Go 1.26.4】Golang Slice 深度解析
·
Golang Slice 深度解析
基于 Go 1.26.4 源码,源码路径:
github.com/go-go1.26.4
核心源文件:runtime/slice.go、reflect/value.go
1 slice 功能完整介绍
1.1 语法定义与核心概念
Go 切片(slice)是对底层数组的引用视图,由三个字段组成:指针、长度、容量。
// 声明(零值 nil)
var s []int // s == nil, len=0, cap=0
// make 初始化
s = make([]int, 5) // len=5, cap=5, 元素全为0
s = make([]int, 3, 10) // len=3, cap=10
// 字面量初始化
s := []int{1, 2, 3} // len=3, cap=3
s := []int{10: -1} // len=11, cap=11, s[10]=-1 其余为0
// 从数组切片
arr := [5]int{1, 2, 3, 4, 5}
s := arr[1:3] // len=2, cap=4, s=[2,3]
// new(只分配 slice header,不分配底层数组)
sp := new([]int) // *[]int, *sp == nil

- var s []T (nil slice): 零分配,JSON→null,语义"不存在"。零值可用 —
- append/len/cap/range 都安全
- make([]T, len, cap): 预分配,减少扩容,JSON→[0,0,0,…]。性能敏感路径首选
- 字面量 []T{…}: 初始数据已知,编译期写入。适合小且固定的初始集
- 常见错误: make([]T, 0) 后不断 append — 应该 make([]T, 0, expectedCap) 预分配
1.2 slice vs array 核心区别
| 维度 | Array [n]T |
Slice []T |
|---|---|---|
| 长度 | 编译期固定,属于类型 | 运行时可变 |
| 内存 | 值类型,直接存储数据 | 引用类型,3个字段(24字节) |
| 赋值 | 拷贝全部数据 | 拷贝 header(共享底层数组) |
| 传参 | 拷贝整份数组 | 拷贝 header(高效) |
| nil | 不存在 nil | nil slice 合法 |
| 比较 | 可用 == 比较 | 不可用 == 比较(只能与 nil 比) |
| 零值 | 元素全为零值 | nil |
1.3 适用场景
2 底层 runtime 实现原理
2.1 slice 底层结构体完整字段
源码位置:runtime/slice.go:14
type slice struct {
array unsafe.Pointer // ★ 指向底层数组的指针
len int // ★ 当前元素数量
cap int // ★ 底层数组容量(从 array 起始到末尾的元素数)
}
内存布局(64-bit 系统,24 bytes):
slice header (24 bytes on 64-bit)
┌────────────────────────────────────────────────────┐
│ array unsafe.Pointer (8B) ──→ 底层数组首地址 │
├────────────────────────────────────────────────────┤
│ len int (8B) │ 当前长度 │
├────────────────────────────────────────────────────┤
│ cap int (8B) │ 容量 │
└────────────────────────────────────────────────────┘
底层数组:
array ──→ [elem0][elem1][elem2]...[elem_len-1][ ... 已分配但未使用 ... ]
│←──── len ────→│ │←──── cap - len ────→│
│←──────────────── cap ──────────────────────→│
三种 slice 状态的内存布局:
1. nil slice:
┌───────────┐
│ array=nil │
│ len = 0 │
│ cap = 0 │
└───────────┘
底层数组: 无
s == nil → true
2. 空 slice (make([]int, 0) 或 s[:0]):
┌──────────────────────┐
│ array = &zerobase │ ← 指向全局零值地址
│ len = 0 │
│ cap = 0 │
└──────────────────────┘
底层数组: zerobase(全局唯一的零字节)
s == nil → false ★ 注意区别!
3. 正常 slice:
┌──────────────────────┐ ┌────┬────┬────┬────┬────┬────┐
│ array ────────────── │────→│ 10 │ 20 │ 30 │ 0 │ 0 │ 0 │
│ len = 3 │ └────┴────┴────┴────┴────┴────┘
│ cap = 6 │ [0] [1] [2] [3] [4] [5]
└──────────────────────┘ │←─ len ─→│←── 可增长 ──→│
│←──────── cap ──────────→│
2.2 makeslice——创建切片的内存分配
源码位置:runtime/slice.go:57
func makeslice(et *_type, len, cap int) unsafe.Pointer {
// ★ 计算所需内存 = cap × elemSize
mem, overflow := math.MulUintptr(et.Size_, uintptr(cap))
// ★ 溢出或超过最大分配或参数非法 → panic
if overflow || mem > maxAlloc || len < 0 || len > cap {
mem, overflow := math.MulUintptr(et.Size_, uintptr(len))
if overflow || mem > maxAlloc || len < 0 {
panicmakeslicelen() // panic: makeslice: len out of range
}
panicmakeslicecap() // panic: makeslice: cap out of range
}
// ★ 分配内存
return mallocgc(mem, et, true)
// mallocgc 第三个参数 true = 需要零初始化
// 如果 et 包含指针,GC 会扫描这块内存
}

- MulUintptr: 计算 cap × elem.Size_,同时检测溢出。在 64位系统上,乘法溢出是唯一风险(两个大数相乘超过 uint64 范围)
- maxAlloc: 最大可分配内存限制。32位约 2GB,64位由系统内存决定
- len < 0: 负长度参数检测,防止非法输入
- len > cap: 逻辑一致性检查
- mallocgc(mem, et, true): 第三个参数 true 表示需要清零内存,这是 make 的零值保证
源码要点: makeslice 本身非常简洁,核心就是计算大小+安全检查+内存分配。真正的复杂性在 growslice(扩容)中。
makeslicecopy——append 触发的创建+拷贝:
源码位置:runtime/slice.go:28
func makeslicecopy(et *_type, tolen int, fromlen int, from unsafe.Pointer) unsafe.Pointer {
var tomem, copymem uintptr
if uintptr(tolen) > uintptr(fromlen) {
tomem, overflow = math.MulUintptr(et.Size_, uintptr(tolen))
copymem = et.Size_ * uintptr(fromlen) // 只拷贝 fromlen 个元素
} else {
tomem = et.Size_ * uintptr(tolen)
copymem = tomem // 目标更小,全部拷贝
}
var to unsafe.Pointer
if !et.Pointers() {
// 元素无指针:不需要 GC 扫描
to = mallocgc(tomem, nil, false)
if copymem < tomem {
// 多余部分清零
memclrNoHeapPointers(add(to, copymem), tomem-copymem)
}
} else {
// 元素有指针:需要 GC 扫描
to = mallocgc(tomem, et, true) // true = 零初始化
}
memmove(to, from, copymem) // 拷贝旧数据
return to
}
2.3 growslice——扩容核心算法
源码位置:runtime/slice.go:126
这是 slice 最核心的函数,决定了 append 时的扩容策略:
func growslice(oldPtr unsafe.Pointer, newLen, oldCap, num int, et *_type) slice {
oldLen := newLen - num // 原始长度
// ★ 零大小元素的特殊处理
if et.Size_ == 0 {
// append 不应创建 nil 指针但非零 len 的 slice
return slice{unsafe.Pointer(&zerobase), newLen, newLen}
}
// ★★★ 核心扩容策略 ★★★
newcap := nextslicecap(newLen, oldCap)
// ★ 内存对齐优化(根据元素大小分4种情况)
var overflow bool
var lenmem, newlenmem, capmem uintptr
noscan := !et.Pointers()
switch {
case et.Size_ == 1:
// byte 类型:无需乘法
lenmem = uintptr(oldLen)
newlenmem = uintptr(newLen)
capmem = roundupsize(uintptr(newcap), noscan) // ★ 对齐到 size class
newcap = int(capmem) // newcap 可能因对齐而增大
case et.Size_ == goarch.PtrSize:
// 指针大小(8字节 on 64-bit):用位移代替乘除
lenmem = uintptr(oldLen) * goarch.PtrSize
newlenmem = uintptr(newLen) * goarch.PtrSize
capmem = roundupsize(uintptr(newcap)*goarch.PtrSize, noscan)
newcap = int(capmem / goarch.PtrSize)
case isPowerOfTwo(et.Size_):
// 2的幂次大小:用位移代替乘除
var shift uintptr
shift = uintptr(sys.TrailingZeros64(uint64(et.Size_))) & 63
lenmem = uintptr(oldLen) << shift
newlenmem = uintptr(newLen) << shift
capmem = roundupsize(uintptr(newcap)<<shift, noscan)
newcap = int(capmem >> shift)
capmem = uintptr(newcap) << shift
default:
// 通用情况:乘法
lenmem = uintptr(oldLen) * et.Size_
newlenmem = uintptr(newLen) * et.Size_
capmem, overflow = math.MulUintptr(et.Size_, uintptr(newcap))
capmem = roundupsize(capmem, noscan)
newcap = int(capmem / et.Size_)
capmem = uintptr(newcap) * et.Size_
}
// ★ 分配新底层数组
var p unsafe.Pointer
if !et.Pointers() {
p = mallocgc(capmem, nil, false) // 无需 GC 扫描
memclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem) // 清零多余部分
} else {
p = mallocgc(capmem, et, true) // 需要 GC 扫描 + 零初始化
}
// ★ 拷贝旧数据到新数组
memmove(p, oldPtr, lenmem)
return slice{p, newLen, newcap}
}

- List itemGo 1.18+ 新容量计算规则:
• 旧 cap < 256 → newcap = oldcap × 2(双倍增长)
• 旧 cap >= 256 → newcap = oldcap + oldcap/4 + 192(平滑增长,渐进趋近 1.25x) - 还需要对齐到元素大小的倍数 + 检查是否超过 maxAlloc
- 内存分配:roundupsize 向上取整到内存分配器的大小类别(class),减少碎片
- 数据迁移:memmove 将旧数据拷贝到新数组 — 这是扩容的 O(n) 开销
- 扩容后一定分配新底层数组,新旧数据不再共享
强调: 扩容是 O(n) 操作,但摊还到每次 append 是 O(1)。这就是为什么 make 预分配很重要 — 避免多次扩容。
2.4 nextslicecap——扩容策略详解
源码位置:runtime/slice.go:231
func nextslicecap(newLen, oldCap int) int {
newcap := oldCap
doublecap := newcap + newcap // 2 * oldCap
// 规则1: 如果新长度 > 2倍旧容量 → 直接用新长度
if newLen > doublecap {
return newLen
}
// 规则2: 旧容量 < 256 → 2倍增长
const threshold = 256
if oldCap < threshold {
return doublecap
}
// 规则3: 旧容量 ≥ 256 → 1.25倍增长(平滑过渡)
for {
// newcap += (newcap + 3*threshold) / 4
// 等价于: newcap = newcap * 1.25 + 192(近似)
newcap += (newcap + 3*threshold) >> 2
if uint(newcap) >= uint(newLen) {
break
}
}
// 溢出保护
if newcap <= 0 {
return newLen
}
return newcap
}
扩容策略三规则总结:
规则1: newLen > 2*oldCap → newcap = newLen (一次性分配足够)
规则2: oldCap < 256 → newcap = 2*oldCap (小切片翻倍)
规则3: oldCap ≥ 256 → newcap ≈ 1.25*oldCap(大切片1.25倍)
过渡示例:
oldCap=128, newLen=200 → 200 > 256? No → 128 < 256? Yes → newcap=256 (2x)
oldCap=256, newLen=300 → 300 > 512? No → 256 < 256? No → newcap=256+(256+768)/4=256+256=512
oldCap=512, newLen=600 → 600 > 1024? No → 512 < 256? No → 512+(512+768)/4=512+320=832
roundupsize——内存对齐到 size class:
// roundupsize 将申请大小向上对齐到 mallocgc 的 size class
// 这可能导致实际分配的 cap 比计算值更大
//
// 例: []int, 计算出 newcap=10, capmem=80
// roundupsize(80, noscan=true) → 可能对齐到 96 (下一个 size class)
// 实际 newcap = 96/8 = 12
//
// 这就是为什么 make([]int, 10) 的 cap 可能大于 10 的原因

- Go 1.17: cap < 1024 双倍; cap >= 1024 按 1.25x 增长 — 阶梯式,有对齐跳变
- Go 1.18+: cap < 256 双倍; cap >= 256 按 oldcap + oldcap/4 + 192 — 连续函数式增长
- 影响:中等大小 slice (256-1024) 增长率更平滑,不再有 1024 处的突变
- 实际新容量还受内存对齐影响,roundupsize 可能大于计算值
- 任何版本:小 slice 都双倍增长,append 单个元素 O(n) 摊还 O(1)
为什么改: 旧算法在 1024 处有明显跳变,实际内存增长不均匀。新算法用连续函数替代阶梯,更可预测。
2.5 append 完整执行流程
编译器将 append 翻译为以下步骤:
源码: s = append(s, x)
编译器生成的伪代码:
1. 如果 cap > len:
// 有空间,直接写入
s[len] = x
s.len = len + 1
2. 如果 cap <= len:
// 需要扩容
newSlice := growslice(s.array, len+1, cap, 1, elemType)
// growslice: 分配新数组 + 拷贝旧数据
newSlice.array[len] = x // 在新数组中写入
newSlice.len = len + 1
s = newSlice // 更新 slice header
3. 多元素 append: s = append(s, x, y, z)
// 同理,但 num=3
2.6 切片表达式——s[i:j] 和 s[i:j:k]
编译器对切片表达式的处理:
s[i:j] → slice{array: s.array + i*elemsize, len: j-i, cap: cap(s)-i}
s[i:j:k] → slice{array: s.array + i*elemsize, len: j-i, cap: k-i}
例: s = []int{0,1,2,3,4,5}, len=6, cap=6
s[1:4] → array=s.array+8, len=3, cap=5 [1,2,3]
s[1:4:5] → array=s.array+8, len=3, cap=4 [1,2,3] 限制cap=4
s[:3] → array=s.array, len=3, cap=6 [0,1,2]
s[2:] → array=s.array+16, len=4, cap=4 [2,3,4,5]
原 slice: [0][1][2][3][4][5]
↑array
│←── len=6 ──→│
│←────── cap=6 ──────→│
s[1:4]: [1][2][3][4][5]
↑s.array+8
│← len=3 →│
│←── cap=5 ──────→│
s[1:4:5]: [1][2][3][4][5]
↑s.array+8
│← len=3 →│
│← cap=4 →│
★ 三索引切片限制了 cap
★ 防止 append 覆盖后续元素
2.7 copy 底层实现
源码位置:runtime/slice.go:310
func slicecopy(toPtr unsafe.Pointer, toLen int, fromPtr unsafe.Pointer, fromLen int, width uintptr) int {
if fromLen == 0 || toLen == 0 { return 0 }
n := fromLen
if toLen < n { n = toLen } // 拷贝 min(fromLen, toLen) 个
if width == 0 { return n } // 零大小元素
size := uintptr(n) * width
if size == 1 {
// ★ 1字节的优化:直接赋值
*(*byte)(toPtr) = *(*byte)(fromPtr)
} else {
// ★ 通用:memmove
memmove(toPtr, fromPtr, size)
}
return n
}
copy 的关键语义:
// copy 返回实际拷贝的元素数 = min(len(dst), len(src))
n := copy(dst, src)
// 源和目标可以重叠(底层是 memmove 不是 memcpy)
// copy 保证了即使 src 和 dst 有重叠区域,结果也是正确的
3 示例代码逐行解析
3.1 声明与初始化
var s []int
// [编译器] s = slice{array: nil, len: 0, cap: 0}
// s == nil → true
// len(s) → 0
// cap(s) → 0
s = make([]int, 3)
// [编译器] 调用 makeslice(int类型, 3, 3)
// [运行时] mallocgc(3*8, int类型, true) → 返回 24 字节数组
// s = slice{array: 指针, len: 3, cap: 3}
s = make([]int, 3, 10)
// [编译器] 调用 makeslice(int类型, 3, 10)
// [运行时] mallocgc(10*8, int类型, true) → 返回 80 字节数组
// s = slice{array: 指针, len: 3, cap: 10}
// s[0..2] = 0 (零初始化)
// s[3..9] 已分配但不可直接访问
s := []int{1, 2, 3}
// [编译器] 在只读数据段生成数组 [1,2,3]
// 然后拷贝到堆上(逃逸分析决定)
// s = slice{array: 堆数组, len: 3, cap: 3}
3.2 append 逐行解析
s := make([]int, 0, 2) // len=0, cap=2
s = append(s, 10)
// [编译器] 生成:
// len = 0, cap = 2, len < cap → 有空间
// s.array[0] = 10 // 直接写入
// s.len = 1 // 更新长度
// 结果: s=[10], len=1, cap=2 ✅ 无扩容
s = append(s, 20)
// len = 1, cap = 2, len < cap → 有空间
// s.array[1] = 20
// s.len = 2
// 结果: s=[10,20], len=2, cap=2 ✅ 无扩容
s = append(s, 30)
// len = 2, cap = 2, len == cap → ★ 触发扩容!
// [编译器] 调用 growslice(s.array, 3, 2, 1, int类型)
// oldLen = 3-1 = 2
// newcap = nextslicecap(3, 2):
// doublecap = 4, 3 > 4? No
// oldCap(2) < 256? Yes → newcap = 4
// capmem = roundupsize(4*8, noscan) = roundupsize(32, true)
// → 可能对齐到 32(已经是 size class)
// mallocgc(32, nil, false) → 分配新数组 32 字节
// memmove(新数组, 旧数组, 16) → 拷贝 [10, 20]
// 新数组[2] = 30(由调用者写入)
// s = slice{array: 新指针, len: 3, cap: 4}
// ★ 旧数组(16字节)等待 GC 回收
s = append(s, 40, 50)
// len=3, cap=4, 需要2个空间, 但 cap-len=1 < 2 → ★ 扩容
// growslice(s.array, 5, 4, 2, int类型)
// newcap = nextslicecap(5, 4):
// doublecap = 8, 5 > 8? No
// oldCap(4) < 256? Yes → newcap = 8
// capmem = roundupsize(8*8) = 64
// mallocgc(64, nil, false)
// memmove 拷贝 [10,20,30]
// 新数组[3]=40, [4]=50
// s = slice{array: 新指针, len: 5, cap: 8}
3.3 切片共享底层数组——最经典陷阱
arr := [5]int{10, 20, 30, 40, 50}
// 底层数组: [10, 20, 30, 40, 50]
s1 := arr[1:3] // s1=[20,30], len=2, cap=4
s2 := arr[2:4] // s2=[30,40], len=2, cap=3
// ★ s1 和 s2 共享同一个底层数组!
s1[1] = 999
// 修改底层数组: [10, 20, 999, 40, 50]
// s2 也变了!s2=[999, 40]
// ★ 更隐蔽的陷阱:append 覆盖
s1 = append(s1, 100)
// s1 cap=4, len=2, 还有空间
// 底层数组: [10, 20, 999, 100, 50] ← s2[1] 从 40 变成了 100!
// s1 = [20, 999, 100], s2 = [999, 100] ← s2 被意外修改!
共享底层数组图示:
arr: [10][20][30][40][50]
↑arr
│←──── 5 ────→│
s1: [20][30][40][50]
↑s1.array
│←2→│←─4─→│
len cap
s2: [30][40][50]
↑s2.array
│←2→│←3→│
len cap
★ s1[2:4] 和 s2[0:2] 指向相同内存!
★ append(s1, 100) 会覆盖 s2 看到的数据!
3.4 nil slice vs 空 slice
var s1 []int // nil slice
s2 := []int{} // 空 slice(非nil)
s3 := make([]int, 0) // 空 slice(非nil)
// s1: slice{array: nil, len: 0, cap: 0} → s1 == nil → true
// s2: slice{array: &zerobase, len: 0, cap: 0} → s2 == nil → false
// s3: slice{array: 指针, len: 0, cap: 0} → s3 == nil → false
// ★ 行为差异:
// - JSON 编码: nil slice → null, 空 slice → []
// - reflect: nil slice 的 Value.IsNil() = true
// - 其他操作 (len/cap/append/for range) 行为完全相同
// ★ 最佳实践:
// 如果要区分"无数据"和"空集合",用 nil slice 表示"无数据"
4 完整使用示例合集
4.1 基础增删改查
package main
import "fmt"
func main() {
// 创建
s := make([]int, 0, 10)
// 增(append)
s = append(s, 1, 2, 3) // s=[1,2,3]
s = append(s, 4) // s=[1,2,3,4]
// 批量追加
other := []int{5, 6, 7}
s = append(s, other...) // s=[1,2,3,4,5,6,7]
// 改
s[0] = 100 // s=[100,2,3,4,5,6,7]
// 删(删除索引 i=3)
i := 3
s = append(s[:i], s[i+1:]...) // s=[100,2,3,5,6,7]
// 插入(在索引 i=2 处插入 999)
s = append(s[:2], append([]int{999}, s[2:]...)...)
// s=[100,2,999,3,5,6,7]
// 查
target := 999
for idx, v := range s {
if v == target {
fmt.Printf("found %d at index %d\n", v, idx)
}
}
fmt.Println(s)
}
4.2 删除元素的三种方式
// 方式1: 保持顺序(慢,O(n))
s = append(s[:i], s[i+1:]...)
// 方式2: 不保持顺序(快,O(1))
s[i] = s[len(s)-1]
s = s[:len(s)-1]
// 方式3: Go 1.21+ 使用 slices.Delete
s = slices.Delete(s, i, i+1) // 保持顺序
4.3 结构体切片
type Item struct {
Name string
Price float64
}
items := []Item{
{"Apple", 1.5},
{"Banana", 0.8},
}
// 修改字段
items[0].Price = 2.0 // ✅ 直接修改
// append
items = append(items, Item{"Cherry", 3.0})
4.4 二维切片
// 创建 m×n 的二维切片
m, n := 3, 4
grid := make([][]int, m) // 外层
for i := range grid {
grid[i] = make([]int, n) // 内层
}
// 或者共享底层数组(内存连续,缓存友好)
data := make([]int, m*n) // 一块连续内存
grid := make([][]int, m)
for i := range grid {
grid[i] = data[i*n : (i+1)*n] // 切片引用同一段内存
}
4.5 切片去重
func dedup(s []string) []string {
seen := make(map[string]bool)
result := s[:0] // ★ 复用底层数组,零分配
for _, v := range s {
if !seen[v] {
seen[v] = true
result = append(result, v)
}
}
return result
}
5 经典问题与优化
5.1 append 后原 slice 的数据变化
s := make([]int, 3, 5) // [0,0,0], len=3, cap=5
s[0], s[1], s[2] = 1, 2, 3
s2 := append(s, 4) // 未扩容,写入 s.array[3]=4
// s = [1,2,3] len=3, cap=5
// s2 = [1,2,3,4] len=4, cap=5
// ★ s 和 s2 共享底层数组!
// ★ s.array[3] 实际已被写入 4,但 s.len=3 所以 s "看不到"
s2[0] = 100
// ★ s[0] 也变成了 100!共享底层数组!
s2 = append(s2, 5, 6, 7) // 触发扩容
// s2 指向新数组,不再与 s 共享
// s 仍然指向旧数组
5.2 大量 append 的性能优化——预分配
// ❌ 不预分配:多次扩容,多次拷贝
var s []int
for i := 0; i < 10000; i++ {
s = append(s, i) // 可能扩容 ~14 次
}
// ✅ 预分配:一次分配,无拷贝
s := make([]int, 0, 10000)
for i := 0; i < 10000; i++ {
s = append(s, i) // 从不扩容
}
// ✅ 更好:直接用长度
s := make([]int, 10000)
for i := range s {
s[i] = i
}
扩容次数对比:
不预分配 append 10000 个 int:
0→1→2→4→8→16→32→64→128→256→512→1024→2048→4096→8192→12288
扩容 15 次,拷贝 1+2+4+...+8192 = 16383 个元素
预分配 make([]int, 0, 10000):
扩容 0 次,拷贝 0 个元素
5.3 切片内存泄漏
// ❌ 内存泄漏:大切片切小后,底层数组仍持有全部数据
func leak() []int {
big := make([]int, 1000000) // 8MB
// ... 填充数据 ...
return big[:10] // 只返回前10个,但底层 8MB 不会被 GC!
}
// ✅ 修复:拷贝到新切片
func noLeak() []int {
big := make([]int, 1000000)
// ... 填充数据 ...
result := make([]int, 10)
copy(result, big[:10])
return result // big 可以被 GC 回收
}
5.4 三索引切片限制容量
arr := [10]int{0,1,2,3,4,5,6,7,8,9}
s := arr[2:5:5] // len=3, cap=3(限制 cap=5-2=3)
// ★ cap 被限制为 3,而不是 8
s = append(s, 100) // len=3, cap=3 → 必须扩容!
// ★ 扩容后 s 指向新数组,不再影响 arr
// 如果不用三索引:
s2 := arr[2:5] // len=3, cap=8
s2 = append(s2, 100) // 不扩容!直接写入 arr[5]=100
// ★ 意外修改了 arr!
5.5 slice 不可比较的原因
// s1 == s2 → 编译错误!
// 原因:
// 1. slice 是引用类型,== 比较的是 header 还是内容?语义不明确
// 2. 如果比较内容,深比较 O(n),不符合 == 的 O(1) 预期
// 3. slice 可能有共享底层数组,比较语义复杂
// ✅ 正确的比较方式
func sliceEqual[T comparable](a, b []T) bool {
if len(a) != len(b) { return false }
for i := range a {
if a[i] != b[i] { return false }
}
return true
}
// Go 1.21+: 使用 slices.Equal
slices.Equal(s1, s2)
5.6 切片表达式越界 panic 规则
s := make([]int, 5, 10)
// ✅ 合法
_ = s[0:5] // 0 ≤ low ≤ high ≤ cap
_ = s[2:8] // high 可以超过 len,但不超过 cap
_ = s[:5] // 省略 low = 0
_ = s[2:] // 省略 high = len
// ❌ panic
_ = s[0:11] // high > cap → panic: slice bounds out of range
_ = s[6:8] // low > len → panic: slice bounds out of range
_ = s[3:2] // high < low → panic: slice bounds out of range
6 总结:Slice 核心知识图谱
slice 操作一图总结:
┌───────────────────────────────────────────────────────────┐
│ Slice 操作速查 │
├─────────────┬─────────────────────────────────────────────┤
│ 创建 │ make([]T, len, cap) 字面量 数组[:] │
│ 追加 │ append(s, x...) 可能触发扩容 │
│ 删除(i) │ append(s[:i], s[i+1:]...) O(n) │
│ 插入(i,v) │ append(s[:i], append([]T{v}, s[i:]...)...) │
│ 复制 │ copy(dst, src) min(len) 个 │
│ 切片 │ s[i:j] s[i:j:k] 共享底层数组 │
│ 遍历 │ for i,v := range s │
│ 长度/容量 │ len(s) / cap(s) │
│ 零值 │ nil (array=nil, len=0, cap=0) │
│ 比较 │ 只能与 nil 比较,内容比较用 slices.Equal │
├─────────────┼─────────────────────────────────────────────┤
│ 扩容策略 │ oldCap < 256 → 2× │
│ │ oldCap ≥ 256 → ≈1.25× │
│ │ newLen > 2×oldCap → newLen │
│ │ 实际可能因 roundupsize 更大 │
└─────────────┴─────────────────────────────────────────────┘
源码索引
文件 关键内容 runtime/slice.go:14slice结构体定义(array/len/cap)runtime/slice.go:57makeslice——创建切片内存分配runtime/slice.go:28makeslicecopy——append 触发的创建+拷贝runtime/slice.go:126growslice——扩容核心(分配新数组+拷贝)runtime/slice.go:231nextslicecap——扩容策略(2x/1.25x/newLen)runtime/slice.go:310slicecopy——copy 底层实现runtime/slice.go:345growsliceNoAlias——已知无别名的优化扩容runtime/slice.go:415growsliceBuf——栈缓冲区扩容优化reflect/value.go:2701SliceHeader(已废弃,改用 unsafe.Slice)
更多推荐

所有评论(0)