Golang: Language: Collection: Array and Slice

 10th March 2021 at 8:34pm

Go 的 array 是定长的;slice 是不定长的。两者长得非常像:

  • Array:类型表达为 [N]T,N 是长度,T 是类型,如 [5]int
  • Slice:类型表达为 []T,T 是类型,如 []int

构建

//// Array: [N]T

// l1: 声明但不赋值。此时每个元素都是 zero value
var l1 [5]int

// l2: 声明且赋值
var l2 [5]int = [5]int{4, 2, 1, 3, 5}

// l3 及 l4: 自动推断类型
var l3 = [5]int{4, 2, 1, 3, 5}
l4 := [5]int{4, 2, 1, 3, 5}

// l5: 自动推断类型及长度([5]int)
l5 := [...]int{4, 2, 1, 3, 5}

//// Slice: []T
// s1: 声明但不赋值,空 slice
var s1 []int

// s2: 声明且赋值
var s2 []int = []int{4, 2, 1, 3, 5}

// s3 及 s4:自动推断类型
var s3 = []int{4, 2, 1, 3, 5}
s4 := []int{4, 2, 1, 3, 5}

// s5: 用 make() 构建
//   func make([]T, len, cap) []T
//   func make([]T, len) []T
s5 := make([]int, 5)

Array 实现

Array 在 Go 的设计中是 值类型的。在 Go blog 中这么描述的:

Go's arrays are values. An array variable denotes the entire array; it is not a pointer to the first array element (as would be the case in C). This means that when you assign or pass around an array value you will make a copy of its contents. (To avoid the copy you could pass a pointer to the array, but then that's a pointer to an array, not an array.) One way to think about arrays is as a sort of struct but with indexed rather than named fields: a fixed-size composite value.

一个 array 变量赋值给其他变量时,值会被拷贝:

a := [...]int{1, 1, 1}
b := a
a[0] = 0
fmt.Println(a, b)
[0 1 1] [1 1 1]

可以修改 array 中的元素值,但是无法改变 array 大小(比如 push / pop)。

Slice 实现

Slice 支持以下标索引形式读写元素值。但是对超出长度部分的写入会报 runtime error:

// len(b) 为 2
b := make([]int, 2, 4)
b[0] = 1
b[1] = 2
b[2] = 3   // runtime error

Slice 的空值是 nil。cap() len() 在此情况下会返回 0。

Slice 本质上是指向某块内存区域的结构信息:

Slice 支持 slicing 操作,slice 出来的结果仍然是 slice,能被读取但是无法被写入:

b := []int{1, 2, 3, 4}
c := b[2:]             // c: {3, 4}
b[2:] = []int{5, 6}    // compile error

Slicing 操作是不复制 slice 中元素的,它生成的 slice 跟原 slice 指向同块区域。因此注意 slicing 后的 len 及 cap:

b := make([]int, 2, 4)
b[0] = 1
b[1] = 2

c := b[2:]

fmt.Printf("len(b): %v, cap(b): %v\n", len(b), cap(b))
fmt.Printf("len(c): %v, cap(c): %v\n", len(c), cap(c))
len(b): 2, cap(b): 4
len(c): 0, cap(c): 2

对 slice 追加数据,需要通过 append() 函数。因为 slice 并不实际拥有底下的元素,因此设计上:

  • 并不是 slice.append()
  • 而是 slice = append(slice, V)

因为如果 append 之后超出了原有的 capacity 时,go 会使 capacity 增大:

b := make([]int, 4)
fmt.Printf("len(b): %v, cap(b): %v\n", len(b), cap(b))

b = append(b, 0)
fmt.Printf("len(b): %v, cap(b): %v\n", len(b), cap(b))
len(b): 4, cap(b): 4
len(b): 5, cap(b): 8

当 append 后原有的位置内存不足时,会把数据拷贝到另外一处内存;此时会生成一个新的 slice 对象(ptr len cap 均有变化),赋值给原有的变量 slice(可能观察到的 slice 的指针地址是没有变化的,有可能是 Go 把老的对象 GC 了,或者是做了优化)。

Slice 合并

对于两个同类型的 slice s1 s2,如果想把 s2 的元素全部追加到 s1 后面,可以这样写:

s := append(s1, s2...)

因为 append() 是个 variadic function。并且这样没有性能问题,go 在实现上并不会把参数一个个 push 进栈里面,而是类似传一个数组进来。

参考