Arrays, slices (and strings): The mechanics of ‘append’

数组与切片基础

数组

数组是 Go 的构建块,通常隐藏在 Slice 之下。 数组大小是类型的一部分,限制了表达能力。

var buffer [256]byte

buffer 变量持有 256 字节数据。访问越界会崩溃。 len(buffer) 返回固定值 256。 数组主要用途是作为 Slice 的存储。

切片头

Slice 描述数组的一段连续区域,Slice 不是数组,而是数组片段的描述。

var slice []byte = buffer[100:150]
// or
var slice = buffer[100:150]
// or
slice := buffer[100:150]

Slice 变量内部结构(sliceHeader):

type sliceHeader struct {
    Length        int
    ZerothElement *byte
}

Slice 可以再次切片(reslice):

slice2 := slice[5:10]

slice2 的头指向同一个底层数组 buffer,但偏移量和长度不同。

Nil 切片

nil Slice 的 Header 零值:

sliceHeader{
    Length:        0,
    Capacity:      0,
    ZerothElement: nil,
}

nil Slice 等同于零长度 Slice,但指针为 nil。可以对其调用 append(会自动分配)。

函数传递与修改

传递 Slice 给函数

Slice 是值类型,包含指针和长度。传递 Slice 给函数时,复制的是 Slice Header。 由于 Header 包含指向底层数组的指针,函数内修改元素会影响原数组

func AddOneToEachElement(slice []byte) {
    for i := range slice {
        slice[i]++
    }
}

但函数内修改 Slice Header(如长度)不会影响原变量,因为 Header 是复制的。 若需修改 Header,需返回新 Slice:

func SubtractOneFromLength(slice []byte) []byte {
    slice = slice[0 : len(slice)-1]
    return slice
}

Slice 指针与方法接收者

若需在函数内修改 Slice Header,可传递 Slice 指针:

func PtrSubtractOneFromLength(slicePtr *[]byte) {
    slice := *slicePtr
    *slicePtr = slice[0 : len(slice)-1]
}

惯用法:修改 Slice 的方法使用指针接收者。

type path []byte
 
func (p *path) TruncateAtFinalSlash() {
    i := bytes.LastIndex(*p, []byte("/"))
    if i >= 0 {
        *p = (*p)[0:i]
    }
}

若方法仅修改元素内容(如 ToUpper),值接收者即可。

切片管理与扩容

容量

Slice Header 实际包含三个字段:

type sliceHeader struct {
    Length        int
    Capacity      int
    ZerothElement *byte
}
  • Capacity:底层数组从 Slice 起始位置开始的实际空间大小。
  • Length:Slice 当前长度。

增长超过 Capacity 会导致 panic。 cap(slice) 返回容量。

创建切片 (Make)

make 用于分配新数组并创建 Slice Header。

slice := make([]int, 10, 15) // len=10, cap=15

若省略 capacity,则默认为 length:

gophers := make([]Gopher, 10) // len=10, cap=10

复制切片 (Copy)

copy 函数在两个 Slice 间复制数据,处理重叠,复制数量取两者长度最小值。

newSlice := make([]int, len(slice), 2*cap(slice))
copy(newSlice, slice)

Append 的机制

手动实现 Extend

实现一个 Extend 函数,容量不足时扩容(分配新数组并复制):

func Extend(slice []int, element int) []int {
    n := len(slice)
    if n == cap(slice) {
        // 容量已满,扩容为原来的 2 倍 + 1
        newSlice := make([]int, len(slice), 2*len(slice)+1)
        copy(newSlice, slice)
        slice = newSlice
    }
    slice = slice[0 : n+1]
    slice[n] = element
    return slice
}

必须返回新 Slice,因为扩容后指向了新数组。

内置函数 append

Go 内置 append 函数支持任意类型 Slice,处理扩容逻辑。 必须保存返回值:slice = append(slice, elem)

用法示例:

slice = append(slice, 4)              // 添加元素
slice = append(slice, slice2...)      // 添加另一 Slice
slice3 := append([]int(nil), slice...) // 复制 Slice

字符串

字符串是只读的字节 Slice。 索引访问字节,切片获取子串。

slash := "/usr/ken"[0] // byte '/'
usr := "/usr/ken"[0:4] // string "/usr"

字符串与 []byte 转换需要内存复制(因为字符串不可变,而 []byte 可变)。 字符串切片操作非常高效,只需创建新的 String Header,共享底层数组。