Golang Slice 深度解析

基于 Go 1.26.4 源码,源码路径:github.com/go-go1.26.4
核心源文件:runtime/slice.goreflect/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 适用场景

Go Slice 适用场景

动态集合

不确定元素数量

频繁增删

函数参数

避免数组拷贝

传切片而非数组

字符串处理

byte切片操作

rune切片转换

IO缓冲区

读取写入缓冲

零拷贝切片引用

数据管道

生产者append

消费者slice消费


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

扩容策略

128→256
64→128

256→512→832
512→832→1184

容量跳跃

oldCap < 256
2x 增长

快速翻倍

oldCap ≥ 256
≈1.25x 增长

渐进增长

newLen > 2×oldCap
直接用 newLen

一次性满足

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

Yes

No

s = append(s, elem)

len < cap?

直接写入
s.array[len] = elem
s.len++
★ 不分配内存

growslice 扩容

nextslicecap 计算新容量

roundupsize 内存对齐

mallocgc 分配新数组

memmove 拷贝旧数据

写入新元素
newArray[newLen-1] = elem

更新 slice header
array=newArray, len=newLen, cap=newCap

★ 旧数组可能被 GC 回收
(如果没有其他引用)

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 仍然指向旧数组

扩容后分离

append 前后共享

再次append
触发扩容

扩容

s: len=3, cap=5
[1,2,3,_,_]

s2: len=4, cap=5
[1,2,3,4,_]

s: 旧数组
[1,2,3,4,_]

s2: 新数组
[1,2,3,4,5,6,7,_]

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 header
array+len+cap=24B

创建
make/字面量/数组切片

扩容策略
<256: 2x
≥256: ≈1.25x

append
有空间直接写
无空间growslice

共享底层数组
切片是引用视图

三索引切片
限制cap防覆盖

内存泄漏
大切片切小未copy

nil vs 空
nil=无数据
空=零长度集合

copy
min len
memmove

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:14 slice 结构体定义(array/len/cap)
runtime/slice.go:57 makeslice——创建切片内存分配
runtime/slice.go:28 makeslicecopy——append 触发的创建+拷贝
runtime/slice.go:126 growslice——扩容核心(分配新数组+拷贝)
runtime/slice.go:231 nextslicecap——扩容策略(2x/1.25x/newLen)
runtime/slice.go:310 slicecopy——copy 底层实现
runtime/slice.go:345 growsliceNoAlias——已知无别名的优化扩容
runtime/slice.go:415 growsliceBuf——栈缓冲区扩容优化
reflect/value.go:2701 SliceHeader(已废弃,改用 unsafe.Slice)

更多推荐