切片原理 #

一、切片底层结构 #

1.1 slice结构体 #

切片在运行时表示为:

go
type slice struct {
    ptr unsafe.Pointer  // 指向底层数组的指针
    len int             // 长度
    cap int             // 容量
}

1.2 内存布局 #

text
切片变量
┌─────────────────────────────────┐
│ ptr ──────┐  len  │  cap       │
└───────────┼─────────────────────┘
            │
            ▼
底层数组
┌───┬───┬───┬───┬───┬───┬───┬───┐
│ 0 │ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │ 7 │
└───┴───┴───┴───┴───┴───┴───┴───┘
      ↑               ↑
      │               │
      └─ 切片范围 ─────┘
      len=4, cap=6

1.3 示例 #

go
s := make([]int, 3, 6)
fmt.Printf("len=%d, cap=%d\n", len(s), cap(s))

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

二、创建切片 #

2.1 字面量创建 #

go
s := []int{1, 2, 3}
// 底层:创建数组,切片指向它

2.2 make创建 #

go
s := make([]int, 3, 6)
// 底层:分配数组,创建切片

2.3 从数组切片 #

go
arr := [5]int{1, 2, 3, 4, 5}
s := arr[1:4]
// 底层:切片指向arr的子区间

2.4 切片表达式 #

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

三、扩容机制 #

3.1 扩容触发 #

len == cap时,append触发扩容:

go
s := make([]int, 0, 2)
s = append(s, 1, 2)  // len=2, cap=2
s = append(s, 3)     // 触发扩容

3.2 扩容规则 #

Go 1.18+ 扩容规则:

  1. 如果新容量 > 2倍旧容量,直接使用新容量
  2. 如果旧容量 < 256,新容量 = 2倍旧容量
  3. 如果旧容量 >= 256,新容量 = 旧容量 + (旧容量+3*256)/4

3.3 扩容示例 #

go
s := make([]int, 0)

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

// 输出:
// len=1, cap=1
// len=2, cap=2
// len=3, cap=4
// len=4, cap=4
// len=5, cap=8
// len=6, cap=8
// ...

3.4 扩容过程 #

go
func growslice(oldSlice, newLen) {
    newCap := calculateNewCap(oldSlice.cap, newLen)
    newArray := malloc(newCap * elementSize)
    copy(newArray, oldSlice.ptr, oldSlice.len)
    return slice{ptr: newArray, len: newLen, cap: newCap}
}

四、内存共享 #

4.1 切片共享底层数组 #

go
s1 := []int{1, 2, 3, 4, 5}
s2 := s1[1:3]  // 共享底层数组

s2[0] = 20
fmt.Println(s1)  // [1 20 3 4 5]

4.2 切片表达式与容量 #

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

// s2可以访问s1[3]和s1[4](通过扩容)
s2 = append(s2, 30, 40)
fmt.Println(s1)  // [1 2 3 30 40](修改了s1)

4.3 限制容量避免共享 #

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

s2 = append(s2, 30)  // 扩容,新数组
fmt.Println(s1)      // [1 2 3 4 5](未改变)

五、性能优化 #

5.1 预分配容量 #

go
// 不好:频繁扩容
var s []int
for i := 0; i < 10000; i++ {
    s = append(s, i)
}

// 好:预分配
s := make([]int, 0, 10000)
for i := 0; i < 10000; i++ {
    s = append(s, i)
}

5.2 避免不必要的复制 #

go
// 不好:复制整个切片
func process(s []int) []int {
    result := make([]int, len(s))
    copy(result, s)
    // 处理result...
    return result
}

// 好:直接使用
func process(s []int) {
    // 直接处理s
}

5.3 切片作为缓冲区 #

go
var buf [1024]byte
n, _ := conn.Read(buf[:])
process(buf[:n])

5.4 重用切片 #

go
var pool = sync.Pool{
    New: func() interface{} {
        return make([]byte, 1024)
    },
}

func process() {
    buf := pool.Get().([]byte)
    defer pool.Put(buf)
    
    // 使用buf...
}

六、内存泄漏 #

6.1 切片引用导致泄漏 #

go
func leak() []int {
    arr := [10000]int{1, 2, 3, ...}
    return arr[:10]  // 返回的切片引用整个数组
}

6.2 解决方法 #

go
func noLeak() []int {
    arr := [10000]int{1, 2, 3, ...}
    result := make([]int, 10)
    copy(result, arr[:10])
    return result  // 只复制需要的部分
}

6.3 删除元素泄漏 #

go
type Data struct {
    Value int
    Info  []byte
}

func remove(s []*Data, i int) []*Data {
    s[i] = nil  // 置nil,允许GC回收
    return append(s[:i], s[i+1:]...)
}

七、切片与数组转换 #

7.1 数组转切片 #

go
arr := [5]int{1, 2, 3, 4, 5}
s := arr[:]  // 切片引用数组

7.2 切片转数组 #

go
s := []int{1, 2, 3, 4, 5}
arr := [5]int(s)  // Go 1.20+

7.3 切片转数组指针 #

go
s := []int{1, 2, 3, 4, 5}
arr := (*[5]int)(s)  // 共享底层数据

八、常见陷阱 #

8.1 append可能改变底层数组 #

go
s1 := []int{1, 2, 3}
s2 := s1

s1 = append(s1, 4)  // 可能不扩容
fmt.Println(s2)     // [1 2 3] 或 [1 2 3 4]

s1 = append(s1, 5, 6, 7)  // 扩容
fmt.Println(s2)           // [1 2 3](未改变)

8.2 切片作为函数参数 #

go
func appendValue(s []int) {
    s = append(s, 1)  // 可能改变s的指针
}

func main() {
    s := []int{1, 2, 3}
    appendValue(s)
    fmt.Println(s)  // 可能是 [1 2 3] 或 [1 2 3 1]
}

8.3 nil切片 vs 空切片 #

go
var s1 []int        // nil切片
s2 := []int{}       // 空切片
s3 := make([]int, 0) // 空切片

fmt.Println(s1 == nil)  // true
fmt.Println(s2 == nil)  // false
fmt.Println(s3 == nil)  // false

九、最佳实践 #

9.1 预分配容量 #

go
s := make([]int, 0, expectedSize)

9.2 使用完整切片表达式 #

go
s := data[low:high:max]  // 限制容量

9.3 避免切片引用大数组 #

go
// 复制需要的数据
result := make([]int, len(s))
copy(result, s)

9.4 注意函数返回切片 #

go
// 返回新切片
func process(s []int) []int {
    result := make([]int, len(s))
    // ...
    return result
}

十、总结 #

切片原理要点:

特性 说明
结构 ptr + len + cap
扩容 2倍或约25%增长
共享 切片共享底层数组
泄漏 注意引用导致泄漏

关键点:

  1. 底层结构:指针、长度、容量
  2. 扩容机制:2倍或约25%增长
  3. 内存共享:切片共享底层数组
  4. 内存泄漏:切片引用可能导致泄漏
  5. 性能优化:预分配容量,避免频繁扩容

准备好学习映射(Map)了吗?让我们进入下一章!

最后更新:2026-03-26