Slice的实践与底层实现

Slice 的实践与底层实现

在 Go 语言中,slice 是一种非常常用的数据结构,它是对数组的一个抽象和封装。slice 提供了灵活的动态数组操作,底层通过数组实现。理解 slice 的实践和底层实现对编写高效的 Go 程序至关重要。

Slice的概念和实践用法

在Go中,数组(Array)是固定长度的,而Slice(切片)是对数组的抽象,更加灵活。

常见操作:

1
2
3
4
5
6
7
arr := [5]int{1, 2, 3, 4, 5}
s := arr[1:4] // 包含 arr[1], arr[2], arr[3] → [2, 3, 4]

s2 := make([]int, 3, 5) // 长度3,容量5
s2[0] = 10 //s2 before append: [10 0 0] len: 3 cap: 5
s2 = append(s2, 20, 30) // 自动扩容
//s2 after append: [10 0 0 20 30] len: 5 cap: 5

Slice 的三要素

每个切片其实是一个描述符(slice header),包含 3 个字段:

  1. 指针:指向底层数组(backing array)的首地址
  2. **长度 (len)**:切片中实际元素的个数
  3. **容量 (cap)**:从切片首元素到底层数组最后一个元素的数量

举例:

1
2
arr := [5]int{1, 2, 3, 4, 5}
s := arr[1:3] // s = [2, 3]
  • 指针 → 指向 arr[1]
  • len = 2
  • cap = 4 (因为 arr[1] 到 arr[4] 一共有 4 个位置)

Slice 的底层数据结构

Go 的源码里(runtime/slice.go),切片头部结构大致如下:

1
2
3
4
5
type slice struct {
array unsafe.Pointer // 底层数组的指针
len int // 长度
cap int // 容量
}

所以 slice 并不是直接存储数据,它只是个“视图”。

  • 多个切片可以共享同一个底层数组。
  • 修改其中一个切片的内容,可能影响另一个切片。

例子:

1
2
3
4
5
6
7
arr := [5]int{1, 2, 3, 4, 5}
s1 := arr[1:4] // [2, 3, 4]
s2 := arr[2:5] // [3, 4, 5]

s1[1] = 99 // 修改 s1 中的元素
fmt.Println(arr) // [1, 2, 99, 4, 5]
fmt.Println(s2) // [99, 4, 5] <-- 也受影响

Slice 的内存增长机制与坑点

append 时,如果容量不足,Go 会分配一个新的数组,并把旧元素复制过去。

扩容规则(简化版):

  • 若新长度小于 2 倍原容量 → cap *= 2
  • 若超过 → cap = 新长度
  • 具体细节可能随 Go 版本略有变化

例子:

1
2
3
4
5
s := []int{1, 2, 3}
fmt.Println(len(s), cap(s)) // 3, 3

s = append(s, 4) // 扩容
fmt.Println(len(s), cap(s)) // 4, 6 (Go 1.18+ 大约会翻倍)

常见坑点

  1. 切片共享内存,容易“意外修改”

    1
    2
    3
    4
    s1 := []int{1, 2, 3, 4}
    s2 := s1[:2]
    s1[1] = 99
    fmt.Println(s2) // [1, 99]
  2. append 可能导致“底层数组更换”,不会影响原切片

    1
    2
    3
    4
    5
    s1 := []int{1, 2}
    s2 := append(s1, 3, 4)
    s1[0] = 99
    fmt.Println(s1) // [99, 2]
    fmt.Println(s2) // [1, 2, 3, 4] <-- 没有受影响

​ 这是因为Slice本身不存储数据,其只是个“视图”。真正的数据存放在 底层数组(backing array) 中。所以append容量足够时,新老切片共享底层数组;在 容量不足时,Go 会创建一个新数组,新老切片从此分开。

实践案例与调试观察

我们可以用 reflect.SliceHeaderunsafe 来观察切片的底层信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import (
"fmt"
"reflect"
"unsafe"
)

func main() {
s := make([]int, 3, 5)
s[0], s[1], s[2] = 10, 20, 30

hdr := (*reflect.SliceHeader)(unsafe.Pointer(&s))
fmt.Printf("Data Ptr: %x, Len: %d, Cap: %d\n", hdr.Data, hdr.Len, hdr.Cap)
}

输出示例(不同机器结果不同):

1
Data Ptr: c000010240, Len: 3, Cap: 5

这就印证了切片的三要素:指针、长度、容量。