# Go 语言切片

切片（slice）是 Go 语言中使用最频繁的复合类型，它是对数组的动态封装，提供了动态长度、灵活的截取和高效的追加能力。日常开发中，几乎所有集合操作都通过切片完成，可以说切片是 Go 编程的核心数据结构。

## 切片的定义与创建

### 使用字面量创建

```go
package main

import "fmt"

func main() {
    // 与数组类似，但不指定长度
    s := []int{1, 2, 3, 4, 5}
    fmt.Println(s)          // [1 2 3 4 5]
    fmt.Printf("类型: %T\n", s) // []int
}
```

### 使用 make 创建

`make([]T, length, capacity)` 用于创建指定长度和容量的切片。

```go
package main

import "fmt"

func main() {
    // 创建长度为 5，容量为 5 的切片，元素为零值
    s1 := make([]int, 5)
    fmt.Println(s1) // [0 0 0 0 0]
    fmt.Printf("len=%d, cap=%d\n", len(s1), cap(s1)) // len=5, cap=5

    // 创建长度为 3，容量为 10 的切片
    s2 := make([]int, 3, 10)
    fmt.Printf("len=%d, cap=%d\n", len(s2), cap(s2)) // len=3, cap=10
}
```

### 从数组派生

```go
package main

import "fmt"

func main() {
    arr := [6]int{10, 20, 30, 40, 50, 60}

    // 从数组索引 1 到 4（不含 4）创建切片
    s := arr[1:4]
    fmt.Println(s) // [20 30 40]
}
```

## 切片 vs 数组的区别

| 特性 | 数组 | 切片 |
|------|------|------|
| 长度 | 固定，是类型的一部分 | 动态，可变 |
| 类型 | `[N]T` | `[]T` |
| 赋值 | 值拷贝 | 引用底层数组 |
| 比较 | 可比较（元素可比较时） | 不可用 `==` 比较 |
| 函数传参 | 拷贝整个数组 | 只拷贝切片头 |

```go
package main

import "fmt"

func main() {
    // 数组：值类型
    a := [3]int{1, 2, 3}
    b := a
    b[0] = 100
    fmt.Println(a) // [1 2 3] —— 不受影响

    // 切片：引用底层数组
    s1 := []int{1, 2, 3}
    s2 := s1
    s2[0] = 100
    fmt.Println(s1) // [100 2 3] —— 共享底层数组
}
```

## len() 和 cap() 函数

- `len(s)` 返回切片的长度（当前元素个数）
- `cap(s)` 返回切片的容量（底层数组从切片起始位置到末尾的元素个数）

```go
package main

import "fmt"

func main() {
    s := make([]int, 3, 5)
    fmt.Printf("len=%d, cap=%d\n", len(s), cap(s)) // len=3, cap=5

    s = append(s, 1, 2)
    fmt.Printf("len=%d, cap=%d\n", len(s), cap(s)) // len=5, cap=5

    // 超出容量会触发扩容
    s = append(s, 3)
    fmt.Printf("len=%d, cap=%d\n", len(s), cap(s)) // len=6, cap=10
}
```

![切片底层结构](https://img.zhaojq.top/20260729161536301.png "切片底层结构")

## append() 函数

`append()` 用于向切片末尾追加元素，返回新的切片。

```go
package main

import "fmt"

func main() {
    s := []int{1, 2, 3}
    fmt.Printf("原始: len=%d, cap=%d\n", len(s), cap(s))

    // 追加单个元素
    s = append(s, 4)
    fmt.Println(s) // [1 2 3 4]

    // 追加多个元素
    s = append(s, 5, 6, 7)
    fmt.Println(s) // [1 2 3 4 5 6 7]

    // 追加另一个切片（使用 ... 展开）
    s2 := []int{8, 9}
    s = append(s, s2...)
    fmt.Println(s) // [1 2 3 4 5 6 7 8 9]
}
```

### 扩容机制

当切片容量不足时，`append()` 会自动扩容。Go 的扩容策略：

- 容量 < 256 时：新容量 = 旧容量的 2 倍
- 容量 >= 256 时：新容量 = 旧容量 × 1.25 + 192（渐进式增长）

```go
package main

import "fmt"

func main() {
    s := make([]int, 0)

    for i := 0; i < 10; i++ {
        s = append(s, i)
        fmt.Printf("追加 %d 后: len=%d, cap=%d\n", i, len(s), cap(s))
    }
}
```

扩容时会分配新的底层数组，并将旧数据拷贝过去。因此，扩容前后的切片指向不同的底层数组。

## 切片截取

切片截取语法为 `s[low:high]`，含义如下：

- `low`：起始索引（包含）
- `high`：结束索引（不包含）
- 结果长度 = `high - low`
- 结果容量 = `cap(s) - low`

```go
package main

import "fmt"

func main() {
    s := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
    fmt.Printf("原始: len=%d, cap=%d\n", len(s), cap(s))

    // 基本截取
    sub := s[2:5]
    fmt.Println(sub) // [2 3 4]
    fmt.Printf("sub: len=%d, cap=%d\n", len(sub), cap(sub)) // len=3, cap=8

    // 省略 low，从开头截取
    fmt.Println(s[:3]) // [0 1 2]

    // 省略 high，截取到末尾
    fmt.Println(s[7:]) // [7 8 9]

    // 全部省略，复制切片描述符（仍共享底层数组）
    fmt.Println(s[:]) // [0 1 2 3 4 5 6 7 8 9]
}
```

截取后的切片与原切片**共享底层数组**，修改其中一个会影响另一个。

## copy() 函数

`copy(dst, src)` 将 `src` 中的元素复制到 `dst`，返回实际复制的元素个数。

```go
package main

import "fmt"

func main() {
    src := []int{1, 2, 3, 4, 5}
    dst := make([]int, 5)

    n := copy(dst, src)
    fmt.Println(n)   // 5
    fmt.Println(dst) // [1 2 3 4 5]

    // 修改 dst 不影响 src
    dst[0] = 100
    fmt.Println(src) // [1 2 3 4 5] —— 不受影响
}
```

`copy()` 常用于创建独立的切片副本，避免共享底层数组带来的副作用：

```go
package main

import "fmt"

func main() {
    s1 := []int{1, 2, 3}

    // 错误方式：共享底层数组
    s2 := s1
    s2[0] = 100
    fmt.Println(s1) // [100 2 3]

    // 正确方式：使用 copy 创建独立副本
    s3 := make([]int, len(s1))
    copy(s3, s1)
    s3[0] = 200
    fmt.Println(s1) // [100 2 3] —— 不受影响
    fmt.Println(s3) // [200 2 3]
}
```

## 切片的底层结构

切片的底层是一个结构体，包含三个字段：

```go
// runtime 中的定义（简化版）
type slice struct {
    array unsafe.Pointer // 指向底层数组的指针
    len   int            // 切片长度
    cap   int            // 切片容量
}
```

- **array**：指向底层数组的指针
- **len**：当前切片中元素的个数
- **cap**：从切片起始位置到底层数组末尾的容量

```go
package main

import (
    "fmt"
    "unsafe"
)

func main() {
    s := []int{10, 20, 30}

    // 通过 unsafe 查看切片内部结构
    ptr := *(*[3]uintptr)(unsafe.Pointer(&s))
    fmt.Printf("array 指针: %x\n", ptr[0])
    fmt.Printf("len: %d\n", ptr[1])
    fmt.Printf("cap: %d\n", ptr[2])
}
```

## 切片共享底层数组的陷阱

由于切片共享底层数组，不当操作可能导致难以排查的 bug。

```go
package main

import "fmt"

func main() {
    s1 := []int{1, 2, 3, 4, 5}
    s2 := s1[1:3] // [2, 3]，与 s1 共享底层数组

    fmt.Printf("s1: len=%d, cap=%d\n", len(s1), cap(s1))
    fmt.Printf("s2: len=%d, cap=%d\n", len(s2), cap(s2))

    // 修改 s2 的元素会影响 s1
    s2[0] = 200
    fmt.Println(s1) // [1 200 3 4 5]

    // 对 s2 追加，如果未超出容量，也会覆盖 s1 的数据
    s2 = append(s2, 99)
    fmt.Println(s1) // [1 200 3 99 5] —— s1[3] 被覆盖！
}
```

**避免陷阱的原则**：
- 截取切片后，谨慎对子切片使用 `append()`
- 需要独立数据时，使用 `copy()` 创建副本
- 使用 `append()` 时始终接收返回值

## 性能建议：预分配容量

如果预先知道切片需要的容量，使用 `make()` 预分配可以避免多次扩容带来的内存分配和数据拷贝。

```go
package main

import "fmt"

func main() {
    // 不推荐：动态扩容，可能触发多次内存分配
    var s1 []int
    for i := 0; i < 1000; i++ {
        s1 = append(s1, i)
    }

    // 推荐：预分配容量，减少扩容次数
    s2 := make([]int, 0, 1000)
    for i := 0; i < 1000; i++ {
        s2 = append(s2, i)
    }

    fmt.Printf("s1: len=%d, cap=%d\n", len(s1), cap(s1))
    fmt.Printf("s2: len=%d, cap=%d\n", len(s2), cap(s2))
}
```

在性能敏感的场景下，预分配容量可以显著减少内存分配次数，提升程序性能。

## 总结

本篇详细讲解了 Go 语言切片的核心知识：切片的创建方式（字面量、make、从数组派生）、切片与数组的区别、`len()` 和 `cap()` 的含义、`append()` 的追加与扩容机制、切片截取语法、`copy()` 函数、底层结构（pointer、len、cap）以及共享底层数组的陷阱。切片是 Go 中最灵活、最常用的数据结构，掌握其底层原理和最佳实践对编写高效、安全的 Go 程序至关重要。

