
Go 的 slice 是日常开发中最常用的数据结构之一，但它的很多行为——为什么 append 有时会让两个变量"断开"，为什么 copy 需要目标 slice 有足够长度，为什么 nil slice 和空 slice 在某些场合表现不同——如果不了解底层机制，遇到时容易踩坑。

本文从 runtime 源码出发，完整梳理 slice 的数据结构、创建、append、copy、子切片、扩容算法，并给出每个操作的执行流程图和内存布局示意。

---

## 数据结构

Go 运行时里 slice 对应 `runtime/slice.go` 中的 `slice` 结构体：

```go
// runtime/slice.go
type slice struct {
    array unsafe.Pointer // 指向底层数组的指针
    len   int            // 当前元素个数
    cap   int            // 底层数组容量
}
```

编译器对外暴露的是 `reflect.SliceHeader`（与 `slice` 内存布局完全一致）：

```go
// reflect/value.go
type SliceHeader struct {
    Data uintptr
    Len  int
    Cap  int
}
```

一个 slice 变量在栈上占 24 字节（64 位平台：3 × 8 字节），存的是"描述符"而不是数据本身。

```
slice 变量（栈上，24 bytes）         底层数组（堆上）
┌──────────────────────┐            ┌───┬───┬───┬───┬───┬───┐
│ array  ──────────────┼──────────► │ 0 │ 1 │ 2 │ 3 │ 4 │ 5 │
├──────────────────────┤            └───┴───┴───┴───┴───┴───┘
│ len = 3              │              ▲               ▲
├──────────────────────┤            元素 0          超出 len 但
│ cap = 6              │                            在 cap 内
└──────────────────────┘
```

---

## 创建方式

### 字面量

```go
s := []int{1, 2, 3}
```

编译器在静态区分配初始数组，运行时构造一个 `slice` 描述符指向它：

```
s.array → [1, 2, 3]（静态/堆上，具体由逃逸分析决定）
s.len   = 3
s.cap   = 3
```

### make

```go
s := make([]int, 3, 6)
```

```
s.array → [0, 0, 0, _, _, _]（堆上，cap=6 个元素的内存）
s.len   = 3
s.cap   = 6
```

执行路径：

```mermaid
flowchart TD
    A["make([]int, 3, 6)"] --> B{编译器判断能否\n在栈上分配}
    B -->|小对象且不逃逸| C[栈上分配]
    B -->|大对象或逃逸| D["runtime.makeslice()"]
    D --> E["math.MulUintptr(et.size, cap)\n检查溢出"]
    E --> F["mallocgc(size, et, true)\n分配并清零内存"]
    F --> G[返回 unsafe.Pointer]
    G --> H["构造 slice{array, len, cap}"]
    C --> H
```

`makeslice` 的关键源码：

```go
// runtime/slice.go
func makeslice(et *_type, len, cap int) unsafe.Pointer {
    mem, overflow := math.MulUintptr(et.size, uintptr(cap))
    if overflow || mem > maxAlloc || len < 0 || len > cap {
        mem, overflow := math.MulUintptr(et.size, uintptr(len))
        if overflow || mem > maxAlloc || len < 0 {
            panicmakeslicelen()
        }
        panicmakeslicecap()
    }
    return mallocgc(mem, et, true)
}
```

### 从数组切片

```go
arr := [6]int{0, 1, 2, 3, 4, 5}
s := arr[1:4]
```

编译器直接用 `arr[1]` 的地址构造 slice 描述符，不分配新内存：

```
arr:  [0][1][2][3][4][5]
           ▲       ▲
           │       │
s.array ───┘       │
s.len = 3          │
s.cap = 5  ────────┘（从 arr[1] 到 arr[5] 共 5 个位置）
```

---

## 子切片（re-slice）

```go
s1 := []int{0, 1, 2, 3, 4, 5}  // len=6, cap=6
s2 := s1[2:5]                   // len=3, cap=4
```

re-slice 不分配新内存，两个描述符指向同一底层数组：

```
底层数组:  [0][1][2][3][4][5]
            ▲        ▲
s1.array ───┘        │
s1.len = 6           │
s1.cap = 6           │
                     │（s2.array 偏移 2 个元素）
s2.array ────────────┘  即 &底层数组[2]
s2.len = 3
s2.cap = 4  （从 [2] 到 [5] 共 4 个位置）
```

关键推论：修改 `s2[0]` 等于修改 `s1[2]`，因为它们指向同一内存地址。

### 三索引切片（full slice expression）

```go
s3 := s1[2:5:5]  // low:high:max，cap = max - low = 3
```

通过限制 cap，可以防止子切片意外修改父 slice 后面的数据。

---

## append 操作

append 是 Go slice 中最复杂的操作，分两条路径：

```mermaid
flowchart TD
    A["append(s, elems...)"] --> B{"len + len(elems) <= cap?"}
    B -->|是，容量足够| C["将元素写入底层数组\ns[len:len+n]"]
    C --> D[更新 len，返回新描述符]
    B -->|否，需要扩容| E["growslice()"]
    E --> F[计算新容量 newcap]
    F --> G["mallocgc() 分配新底层数组"]
    G --> H[memmove 拷贝旧数据]
    H --> I[写入新元素]
    I --> J["更新 len, cap, array\n返回新描述符"]
```

容量足够时 append：

```go
s := make([]int, 3, 6)  // len=3, cap=6
s = append(s, 10)
```

```
before: array→[0,0,0,_,_,_]  len=3  cap=6
after:  array→[0,0,0,10,_,_] len=4  cap=6（array 不变，共享同一块内存）
```

容量不足时 append：

```go
s := []int{1, 2, 3}  // len=3, cap=3
s = append(s, 4)
```

```
before: s.array → [1, 2, 3]         len=3  cap=3（旧数组）
               growslice 分配新数组
after:  s.array → [1, 2, 3, 4, _, _] len=4  cap=6（新数组）
                   旧数组还在堆上，直到 GC 回收
```

### 扩容算法（growslice）

Go 1.18 对扩容算法做了调整，以下是 Go 1.18+ 的逻辑：

```go
// runtime/slice.go（简化）
func growslice(et *_type, old slice, cap int) slice {
    newcap := old.cap
    doublecap := newcap + newcap

    if cap > doublecap {
        newcap = cap
    } else {
        const threshold = 256
        if old.cap < threshold {
            newcap = doublecap  // 小 slice 直接翻倍
        } else {
            for 0 < newcap && newcap < cap {
                // 从翻倍逐渐过渡到 1.25x，公式：每次增长 (newcap + 3*threshold) / 4
                newcap += (newcap + 3*threshold) / 4
            }
        }
    }
    // 内存对齐后实际分配的 cap 可能更大
    ...
}
```

扩容策略流程图：

```mermaid
flowchart TD
    A["需要容量 cap，当前 old.cap"] --> B{"cap > 2 × old.cap?"}
    B -->|是| C[newcap = cap]
    B -->|否| D{"old.cap < 256?"}
    D -->|是| E["newcap = 2 × old.cap\n（直接翻倍）"]
    D -->|否| F["循环：newcap += (newcap + 768) / 4\n直到 newcap >= cap\n（从 2x 平滑收敛到 1.25x）"]
    C --> G[内存对齐，按 size class 向上取整]
    E --> G
    F --> G
    G --> H["mallocgc() 分配新数组"]
    H --> I[memmove 拷贝旧数据]
```

Go 1.18 之前临界值是 1024，之后改为 256，目的是让中等大小的 slice 扩容更平滑。

---

## copy 操作

```go
n := copy(dst, src)
```

copy 底层调用 `runtime.memmove`，按元素类型的大小做内存拷贝：

```go
// runtime/slice.go
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  // 以较小的 len 为准
    }
    // width == 0 说明是零大小元素，无需拷贝数据
    if width == 0 {
        return n
    }
    memmove(toPtr, fromPtr, uintptr(n)*width)
    return n
}
```

执行路径：

```mermaid
flowchart TD
    A["copy(dst, src)"] --> B["n = min(len(dst), len(src))"]
    B --> C{n == 0?}
    C -->|是| D[返回 0]
    C -->|否| E{elementSize == 0?}
    E -->|是，零大小类型| F[返回 n，无需拷贝]
    E -->|否| G["memmove(dst.array, src.array, n × elementSize)"]
    G --> H[返回 n]
```

copy 的内存行为示意：

```
src: [1][2][3][4][5]   len=5
dst: [0][0][0]         len=3  ← 只有 3 个位置

copy(dst, src) 后：

dst: [1][2][3]         ← 只复制了 3 个元素（dst 的 len 决定上限）
src: [1][2][3][4][5]   ← 不变
返回值 = 3
```

---

## nil slice 与 empty slice

```go
var s1 []int        // nil slice
s2 := []int{}       // empty slice
s3 := make([]int,0) // empty slice
```

内存布局对比：

```
s1 (nil slice):
┌──────────┬─────┬─────┐
│ array=nil│len=0│cap=0│
└──────────┴─────┴─────┘

s2 / s3 (empty slice):
┌──────────────┬─────┬─────┐
│ array=zeroptr│len=0│cap=0│  ← zeroptr 是全局非 nil 地址
└──────────────┴─────┴─────┘
```

`runtime.zerobase` 是一个全局的零大小变量，所有空 slice 和空 map 的 array 指针都指向它，避免了 0 字节分配对 GC 造成压力。

关键差异：

| 操作 | nil slice | empty slice |
|------|:---------:|:-----------:|
| `len(s)` | 0 | 0 |
| `cap(s)` | 0 | 0 |
| `s == nil` | true | false |
| `append(s, 1)` | 可以 | 可以 |
| `json.Marshal(s)` | `"null"` | `"[]"` |

`json.Marshal` 是最常见的陷阱：返回 `null` 还是 `[]` 取决于 slice 是否为 nil。

---

## 完整操作时序图

```mermaid
sequenceDiagram
    participant User as 用户代码
    participant Compiler as 编译器
    participant Runtime as 运行时 (runtime)
    participant GC as GC / 内存分配器

    User->>Compiler: make([]int, 3, 6)
    Compiler->>Runtime: makeslice(*_type, 3, 6)
    Runtime->>Runtime: 检查 len/cap 合法性
    Runtime->>GC: mallocgc(6×8, ..., true)
    GC-->>Runtime: unsafe.Pointer（清零内存）
    Runtime-->>User: slice{ptr, 3, 6}

    User->>Compiler: append(s, 10, 20, 21, 22)
    Compiler->>Runtime: 计算 need = 3+4=7 > cap=6
    Runtime->>Runtime: growslice(*_type, old, 7)
    Runtime->>Runtime: newcap = 12（6×2，因 6<256）
    Runtime->>GC: mallocgc(12×8, ...)
    GC-->>Runtime: newptr
    Runtime->>Runtime: memmove(newptr, oldptr, 3×8)
    Runtime-->>User: slice{newptr, 7, 12}

    User->>Compiler: copy(dst, src)
    Compiler->>Runtime: slicecopy(dst.array, dst.len, src.array, src.len, 8)
    Runtime->>Runtime: n = min(len(dst), len(src))
    Runtime->>Runtime: memmove(dst.array, src.array, n×8)
    Runtime-->>User: n
```

---

## 常见陷阱

### append 后两个变量"断开"

```go
s1 := make([]int, 3, 10)
s2 := s1[:3]       // s2 与 s1 共享底层数组，cap=10
s2 = append(s2, 99)

// s1[3] == 99，因为 cap 够，append 没有触发扩容
// 但 s1.len 还是 3，所以 s1[3] 读不到（需要 s1[:4]）
// 当 s2 继续 append 超过 cap=10 后，两者才真正断开
```

### 子切片导致大数组无法被 GC 回收

```go
bigData := make([]byte, 1<<20)  // 1 MB
small := bigData[:10]           // len=10，但 cap=1<<20
// bigData 虽然不再使用，但 small 持有同一底层数组，1 MB 无法被 GC 回收

// 修复：用 copy 或 append 显式断开共享
small = append([]byte(nil), bigData[:10]...)
```

### 并发写 slice 不安全

多个 goroutine 同时 append 到同一 slice 没有任何内置同步，必须用 `sync.Mutex` 或 channel 保护。slice 描述符本身的赋值（写 len/cap/array 三个字段）也不是原子操作。

### make 的 len 陷阱：append 不从头开始

```go
s := make([]int, 5)     // len=5, cap=5，五个元素已经是 0
s = append(s, 1)        // 追加到第 6 个位置，而不是第 1 个

fmt.Println(s)  // [0 0 0 0 0 1]，而不是 [1]
```

如果只想预分配容量，应该用 `make([]int, 0, 5)`。用 `make([]int, 5)` 再 append 是最常见的初学者陷阱之一。

### 函数传参：外部 slice 不会被内部 append 改变

slice 描述符是值传递，函数内对 len/cap/array 的修改不会反射到调用方：

```go
func addElem(s []int) {
    s = append(s, 99)   // 只修改了函数栈上的局部描述符
}

s := []int{1, 2, 3}
addElem(s)
fmt.Println(s)  // [1 2 3]，99 没有追加进来
```

如果需要在函数内修改 slice 本身（长度、容量、底层数组），需要传指针或返回新 slice：

```go
func addElem(s *[]int) {
    *s = append(*s, 99)
}
```

注意：函数内直接修改元素（`s[0] = 99`）是有效的，因为共享底层数组；改变 len/cap 才需要指针或返回值。

### append 两个 slice 时意外覆盖

当 `s1` 有剩余容量时，`append(s1, s2...)` 会直接写入 s1 底层数组，同时影响所有共享该数组的 slice：

```go
base := make([]int, 0, 5)
s1 := append(base, 1, 2, 3)   // len=3, cap=5，共享 base 底层数组
s2 := append(base, 4, 5, 6)   // 也从 base 开始写，覆盖了 s1 的数据！

fmt.Println(s1)  // [4 5 6]，不是 [1 2 3]
fmt.Println(s2)  // [4 5 6]
```

根源是 `base` 的 cap 足够，两次 append 都在同一块内存上写。修复方式是每次 append 前先断开共享：

```go
s1 := append(base[:len(base):len(base)], 1, 2, 3)  // 用三索引限制 cap，强制扩容
s2 := append(base[:len(base):len(base)], 4, 5, 6)
```

### range 遍历拿到的是值拷贝

```go
type Point struct{ X, Y int }
points := []Point{{1, 2}, {3, 4}}

for _, p := range points {
    p.X = 100   // 修改的是副本，不影响原 slice
}
fmt.Println(points)  // [{1 2} {3 4}]

// 如果需要修改，用下标
for i := range points {
    points[i].X = 100
}
```

对元素类型较大的 slice，range 还会带来额外的拷贝开销，可以改用下标遍历或存指针的 slice。

### 删除元素的正确姿势

Go 没有内置的删除方法，常见有两种写法：

删除下标 `i` 的元素，保持顺序：

```go
s = append(s[:i], s[i+1:]...)
```

删除下标 `i` 的元素，不保持顺序（性能更好）：

```go
s[i] = s[len(s)-1]
s = s[:len(s)-1]
```

第一种写法会把 `i` 之后的元素全部左移，时间复杂度 O(n)。第二种用最后一个元素填坑，O(1)。

对于存指针或含指针字段的元素，删除后还需要清零尾部元素防止内存泄漏：

```go
// 保序删除，清零释放引用
copy(s[i:], s[i+1:])
s[len(s)-1] = nil  // 或对应类型的零值
s = s[:len(s)-1]
```

### 循环中 goroutine 捕获 slice 元素地址

```go
s := []int{1, 2, 3}
for _, v := range s {
    go func() {
        fmt.Println(v)  // 捕获的是循环变量 v 的地址，最终可能打印 3 3 3
    }()
}
```

Go 1.22 之前循环变量只有一个，所有 goroutine 共享同一个 `v`。修复方式：

```go
for _, v := range s {
    v := v  // 重新声明，创建新变量
    go func() {
        fmt.Println(v)
    }()
}
```

Go 1.22 起每次迭代的循环变量是独立的，此问题不再出现。

### 对 nil slice 调用 append 是安全的，但赋值 index 不是

```go
var s []int
s = append(s, 1)  // 安全，append 会处理 nil 情况
s[0] = 1          // panic：index out of range，nil slice 的 len=0
```

nil slice 可以 append，但不能直接通过下标赋值——这两者的行为经常被混淆。

### 含指针元素的 slice 增加 GC 扫描压力

GC 在标记阶段需要扫描所有可达指针。`[]int` 不含指针，GC 直接跳过底层数组；`[]*MyStruct` 或 `[]interface{}` 含指针，GC 必须逐元素扫描。

```go
// GC 不扫描元素内部
data := make([]int, 1_000_000)

// GC 必须扫描所有 100 万个指针
ptrs := make([]*MyStruct, 1_000_000)
```

如果 slice 元素数量巨大且生命周期长（如全局缓存），优先用值类型而不是指针类型，或者考虑 `[]uint64` + 手动序列化来彻底绕开 GC 扫描。

### 截断 slice 后尾部指针不会被 GC 回收

```go
s := make([]*int, 10)
for i := range s {
    v := i
    s[i] = &v
}

s = s[:5]  // 只截断了 len，底层数组后 5 个指针仍然存在，不会被 GC 回收
```

正确做法是清零截断部分：

```go
tail := s[5:]
for i := range tail {
    tail[i] = nil
}
s = s[:5]
```

Go 1.21 引入了内置 `clear`，可以一行完成：

```go
clear(s[5:])   // 将 s[5:] 中所有元素设为零值
s = s[:5]
```

---

## 奇淫技巧

### 零拷贝 string 与 []byte 互转

标准转换 `[]byte(str)` 和 `string(b)` 都会分配新内存并拷贝。对性能敏感的路径可以用 `unsafe` 跳过拷贝：

```go
// Go 1.20+ 推荐写法
import "unsafe"

func stringToBytes(s string) []byte {
    return unsafe.Slice(unsafe.StringData(s), len(s))
}

func bytesToString(b []byte) string {
    return unsafe.String(unsafe.SliceData(b), len(b))
}
```

使用约束：转换后的 `[]byte` 绝对不能写入，因为 string 底层内存是只读的，写入会触发 panic 或数据竞争。只读操作（序列化、哈希、比较）是安全的。

Go 1.20 之前的写法依赖 `reflect.SliceHeader`，但该类型在 Go 1.17 起已标记为不推荐，优先用 `unsafe.Slice` / `unsafe.SliceData`。

### 原地 filter，零分配

通常 filter 写法会分配新 slice：

```go
// 有分配
result := make([]int, 0, len(s))
for _, v := range s {
    if v%2 == 0 {
        result = append(result, v)
    }
}
```

原地复用底层数组，零分配：

```go
n := s[:0]  // len=0，cap 和 array 与 s 相同
for _, v := range s {
    if v%2 == 0 {
        n = append(n, v)
    }
}
s = n
```

注意：原地 filter 会修改原底层数组，如果外部还有其他 slice 共享它，需要先 copy 一份。

### 用 append 实现 insert

Go 没有内置 insert，用 append + copy 组合：

```go
// 在下标 i 处插入 v
func insert(s []int, i int, v int) []int {
    s = append(s, 0)       // 先扩一位（触发扩容或不触发）
    copy(s[i+1:], s[i:])   // i 之后的元素右移
    s[i] = v
    return s
}
```

### 原地反转

双指针从两端向中间逼近，每次交换一对元素，直到两指针相遇。

```mermaid
flowchart TD
    A["reverse(s)"] --> B["i = 0\nj = len(s) - 1"]
    B --> C{i < j ?}
    C -->|否，相遇或交叉| D[完成，返回]
    C -->|是| E["swap s[i] ↔ s[j]"]
    E --> F["i++\nj--"]
    F --> C
```

以 `[1, 2, 3, 4, 5]` 为例，每一轮的内存状态：

```
初始:   [1] [2] [3] [4] [5]
         i↑               j↑

第 1 轮: swap(s[0], s[4])
        [5] [2] [3] [4] [1]
             i↑       j↑

第 2 轮: swap(s[1], s[3])
        [5] [4] [3] [2] [1]
                 i↑ j↑      ← i < j 仍成立，但下一轮 i==j，停止

结果:   [5] [4] [3] [2] [1]
```

偶数长度（如 `[1,2,3,4]`）时，两指针在中间错过（i=2, j=1，i > j）自然终止，不会多做一次交换。

完整实现（泛型，适用任意可比类型）：

```go
func reverse[T any](s []T) {
    for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
        s[i], s[j] = s[j], s[i]
    }
}
```

时间复杂度 O(n/2) = O(n)，空间复杂度 O(1)，原地操作不分配内存。

---

### 用 append 实现双端队列

标准库没有双端队列，用 slice 可以快速实现一个，适合数据量不大、不追求极致性能的场景。

#### 数据结构

```go
type Deque[T any] []T
```

底层就是一个普通 slice，不需要额外字段。

#### 四个操作的实现与内存行为

**PushBack（尾插）**

```go
func (d *Deque[T]) PushBack(v T) {
    *d = append(*d, v)
}
```

```mermaid
flowchart TD
    A["PushBack(v)"] --> B{"len < cap ?"}
    B -->|是| C["array[len] = v\nlen++"]
    B -->|否| D["growslice：分配新数组\nmemmove 拷贝旧数据"]
    D --> E["array[len] = v\nlen++，更新 array/cap"]
    C --> F[返回]
    E --> F
```

```
before: [A][B][C][_][_]   len=3 cap=5
         ↑
PushBack(D):
after:  [A][B][C][D][_]   len=4 cap=5（未扩容，O(1) 均摊）
```

**PushFront（头插）**

```go
func (d *Deque[T]) PushFront(v T) {
    *d = append([]T{v}, *d...)
}
```

```mermaid
flowchart TD
    A["PushFront(v)"] --> B["分配临时 []T{v}\n（长度 1 的新数组）"]
    B --> C["append([]T{v}, *d...)\n必然触发扩容（cap 只有 1）"]
    C --> D["growslice：分配 1+len(d) 大小的新数组"]
    D --> E["array[0] = v"]
    E --> F["memmove 将旧数据复制到 array[1:]"]
    F --> G["更新描述符，len = old_len + 1"]
    G --> H[返回]
```

```
before: [A][B][C]   len=3
PushFront(Z):
        新分配:  [Z][_][_][_][_][_]  （容量由 growslice 决定）
        复制后:  [Z][A][B][C][_][_]  len=4
```

`PushFront` 每次必然分配新数组，时间复杂度 O(n)。频繁头插的场景，应该改用环形缓冲区或链表。

**PopBack（尾删）**

```go
func (d *Deque[T]) PopBack() T {
    s := *d
    v := s[len(s)-1]
    *d = s[:len(s)-1]
    return v
}
```

```mermaid
flowchart TD
    A["PopBack()"] --> B["v = array[len-1]"]
    B --> C["len--（缩短描述符，不释放内存）"]
    C --> D["返回 v"]
```

```
before: [A][B][C][D]   len=4 cap=6
PopBack() → 返回 D
after:  [A][B][C][ ]   len=3 cap=6（D 仍在内存中，等待被覆盖或 GC）
```

若元素是指针类型，尾部的指针不会被 GC 回收，需要手动清零：

```go
func (d *Deque[T]) PopBack() T {
    s := *d
    v := s[len(s)-1]
    var zero T
    s[len(s)-1] = zero  // 清零，释放 GC 引用
    *d = s[:len(s)-1]
    return v
}
```

**PopFront（头删）**

```go
func (d *Deque[T]) PopFront() T {
    s := *d
    v := s[0]
    *d = s[1:]
    return v
}
```

```mermaid
flowchart TD
    A["PopFront()"] --> B["v = array[0]"]
    B --> C["array 指针前移一个元素\nlen--，cap--"]
    C --> D["返回 v"]
```

```
before: [A][B][C][D]   array→0x10, len=4, cap=6
         ↑
PopFront() → 返回 A
after:  [_][B][C][D]   array→0x18, len=3, cap=5
             ↑
             （A 所在的内存不再可达，但整个底层数组仍被引用）
```

PopFront 的隐患：每次头删只是把 array 指针向后移，底层数组头部的内存永远不会被 GC 回收，持续 PopFront 会造成内存"单向泄漏"。对于只做尾插头删（FIFO 队列）场景，泄漏会随操作次数线性增长。

解决办法一：PopFront 后定期把剩余数据 copy 到新 slice：

```go
func (d *Deque[T]) Compact() {
    fresh := make([]T, len(*d))
    copy(fresh, *d)
    *d = fresh
}
```

解决办法二：改用环形缓冲区实现，头尾都是 O(1) 且无泄漏，参考 `golang.org/x/exp/slices` 或自行实现。

#### 各操作复杂度对比

| 操作 | 时间复杂度 | 是否分配内存 |
|------|:---:|:---:|
| PushBack | O(1) 均摊 | 仅扩容时 |
| PushFront | O(n) | 每次 |
| PopBack | O(1) | 否 |
| PopFront | O(1) | 否（但有内存泄漏风险） |

slice 实现的双端队列适合"大量尾插、少量头删"或"临时用用"的场景。若头尾操作频率相近，推荐换用基于环形缓冲区的实现。

### 三索引切片做防御性传参

向函数传 slice 时，如果不希望被调用方通过 append 意外写入父 slice 的后续空间：

```go
// 不安全：callee 可以 append 进 buf 的剩余空间
process(buf[:n])

// 安全：限制 cap，append 会强制扩容而不影响 buf
process(buf[:n:n])
```

这在实现类似 `io.Reader` 的缓冲分发逻辑时非常有用。

### 利用 copy 返回值快速求重叠长度

```go
// min(len(a), len(b)) 的语义化写法
n := copy(a, b)  // 返回实际复制的元素数，即 min(len(a), len(b))
```

有时比显式 `if` 判断更简洁，且语义清晰。

### 预分配是最有效的性能手段

```go
// 不知道最终大小，一次次扩容
var s []int
for i := 0; i < 10000; i++ {
    s = append(s, i)  // 触发约 14 次 growslice + memmove
}

// 已知大小，一次分配
s := make([]int, 0, 10000)
for i := 0; i < 10000; i++ {
    s = append(s, i)  // 零次扩容
}
```

不知道精确大小时，给一个合理的上界估计也比完全不预分配好。`append` 的均摊复杂度虽然是 O(1)，但每次扩容都有 `memmove` 开销，数据量大时差距明显。

---

## 小结

| 操作 | 分配新内存 | 共享底层数组 | 时间复杂度 |
|------|:---:|:---:|------|
| 字面量创建 | 是 | 否 | O(n) |
| make | 是 | 否 | O(n)（清零） |
| re-slice `a[i:j]` | 否 | 是 | O(1) |
| append（不扩容） | 否 | 是 | O(k) |
| append（扩容） | 是 | 否 | O(n) |
| copy | 否（目标已有） | 否 | O(min(n,m)) |

理解 slice 的核心是区分"描述符"和"底层数组"：描述符是值拷贝，底层数组是引用语义。append 触发扩容时会切断共享，不触发时不会。这条规律能解释绝大多数 slice 相关的困惑行为。

---

参考：

- [runtime/slice.go](https://github.com/golang/go/blob/master/src/runtime/slice.go)
- [reflect/value.go — SliceHeader](https://github.com/golang/go/blob/master/src/reflect/value.go)
- [Go 1.18 Release Notes — slice growth](https://tip.golang.org/doc/go1.18)
- [Go spec: Slice expressions](https://go.dev/ref/spec#Slice_expressions)

