Loading

array, slice and append in Go

Arrays, slices (and strings): The mechanics of ‘append’ - go.dev

当我们新学习编程语言中的数组时,我们往往需要考虑以下问题:

  • 可变长度还是固定长度?
  • 长度是数组数据结构的一部分吗?
  • 多维数组是怎样的?
  • 空数组有啥含义吗?

将以上问题的答案决定了,对于这门编程语言,数组仅仅是一个功能还是语言的核心设计。

在 Golang 中,开发团队在固定长度array 的基础上,设计了可变长、可拓展的数据结构 slice,这两者共同组成了 Golang 中的数组。

Array

Array (也就是数组)是 Go 语言重要的组成元素,Go 中很多的语言特性都是依托于它实现的。在学习更强大、灵活的 slice 之前,我们必须先对 array 有所了解。

正如前面所说,Go 中的数组 array 是固定长度的,array 的长度是它类型的一部分,这也是在 Go 项目中很少看到数组出现的原因。

例如 var buffer [256]byte 声明了一个长度为 256 的 byte 数组 buffer;如果有一个数组长度为 [512]byte,那么他们会是两个不同类型的数组。

在内存中,数组是占用了一串连续内存空间的元素组成的,内存中 buffer 数组会是下图这样的:

golang数组内存

如图,数组 buffer 仅由 256 个内存中连续分布的元素组成,通过 Go 的内置函数 len 我们可以获取数组的元素个数,即 len(buffer)==256。和大多数语言中的数组一样,我们可以通过索引,形如 buffer[0]buffer[2] ,对它进行随机访问;如果我们使用超过它长度的索引进行访问,程序就会 panic 然后 crash。

func main() {
	var buffer [256]byte
	buffer[1] = 'a'

	fmt.Printf("%c\n", buffer[0]) // 输出空字符 " "
	fmt.Printf("%c\n", buffer[1]) // 输出 "a"
	fmt.Printf("%c\n", buffer[256]) // 数组越界,panic
}

Slice

A slice is not an array. A slice describes a piece of an array.

slice (切片)引用了某个 array 中一段连续的内存,例如下面,我们用 demo 这个切片引用了之前的 buffer 数组中索引 100 到 149 的五十个元素(切片的语法是左闭右开的):

// 三种等价的声明方式
var demo []byte = buffer[100:150]
var demo = buffer[100:150]
demo := buffer[100:150]

简单理解 slice 的底层结构

我们可以将 slice 简单理解为由两个元素组成的数据结构:

  1. 切片的长度
  2. 指向数组特定元素的指针

也就是可以这样认为:

type sliceHeader struct {
    Length        int
    ZerothElement *byte
}

demo := sliceHeader{
    Length:        50,
    ZerothElement: &buffer[100],
}

上面的数据结构只是一个示例,并不是 Go 中 slice 的真正实现方式。这里主要是通过它来理解 slice 的机制。

如上代码所示,demo 的指针指向了 buffer 数组索引为 100 的元素,也就是 demo[0]buffer[100] 等价:

buffer[100] = 'b'
fmt.Printf("%c\n", demo[0]) // 输出 "b"

除了将 array 切片成 slice,我们也可以对 slice 进行切片:

demo2 := demo[5:10]

和对 array 进行切片一样,demo2 引用了 demo 索引 5~9 的 5 个元素,也就是实际引用了 buffer 索引 205~209 的 5 个元素(demo2 => demo => buffer) 。因此,demo2 底层的 sliceHeader 实际是这样的:

demo2 := sliceHeader{
    Length:        5,
    ZerothElement: &buffer[105],
}

我们也可以对 slice 自身进行切片,英文叫 reslice

demo = demo[5:10]

reslice 会将切片后的结果存储到原切片,也就是此时 demodemo2sliceHeader 都是一样的。

理解 slice 是值传递而不是指针传递的

Golang 中很多的函数都是以 slice 作为参数的,而 Golang 永远是值传递的,因此实际上传入的是 sliceHeader 这个结构体的复制。

不过比较特殊的是,sliceHeader 是一个包含指针的结构体,我们需要仔细理解这句话:「slice 是值传递的,传递的是一个包含指向数组特定元素指针的结构体拷贝」。

看下面这个函数:

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

AddOneToEachElement 函数遍历传入的 slice 并令其元素全部自增一。

func main() {
    slice := buffer[10:20]
    for i := 0; i < len(slice); i++ {
        slice[i] = byte(i)
    }
    fmt.Println("before", slice) // before [0 1 2 3 4 5 6 7 8 9]
    AddOneToEachElement(slice)
    fmt.Println("after", slice) // after [1 2 3 4 5 6 7 8 9 10]
}

通过上面的例子,我们可以确认:因为 sliceHeader 包含指向数组元素的指针,它们指向相同的底层数组;因此即使 sliceHeader 是值传递的,我们在 AddOneToEachElement 函数中对切片元素的修改,也会影响到传入的源 slice。

下面的例子则证明了 slice 确实是值传递的:

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

func main() {
    fmt.Println("Before: len(slice) =", len(slice)) // Before: len(slice) = 50
    newSlice := SubtractOneFromLength(slice)
    fmt.Println("After:  len(slice) =", len(slice)) // After:  len(slice) = 50
    fmt.Println("After:  len(newSlice) =", len(newSlice)) // After:  len(slice) = 49
}

上面代码中,我们在 SubtractOneFromLength 函数中修改了 slice 的长度 Length,可以看到源 slice 并没有被影响,这就是因为 slice 是值传递的,SubtractOneFromLength 中修改的是 sliceHeader 中的长度字段 Length

下面的代码中,我们显式传递了 slice 的指针,可以看到即使是长度的变化,也会影响到源 slice:

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

func main() {
    fmt.Println("Before: len(slice) =", len(slice)) // Before: len(slice) = 50
    PtrSubtractOneFromLength(&slice)
    fmt.Println("After:  len(slice) =", len(slice)) // After:  len(slice) = 49
}

又或者,我们将 slice 作为方法的 Receiver 也可以实现这一目的:

type path []byte

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

func main() {
    pathName := path("/usr/bin/tso") // Conversion from string to path.
    pathName.TruncateAtFinalSlash()
    fmt.Printf("%s\n", pathName) // /usr/bin
}

Capacity:slice 的「容量」

我们首先看看下面的例子,这里我们通过 Extend 方法扩充 int 类型的 slice:

func Extend(slice []int, element int) []int {
    n := len(slice)
    slice = slice[0 : n+1]
    slice[n] = element
    // 注意这里我们要将 slice 返回
    return slice
}

func main() {
    var iBuffer [10]int
    slice := iBuffer[1:1]
    for i := 1; i < 20; i++ {
        slice = Extend(slice, i)
        fmt.Println(i, len(slice), cap(slice))
    }
}

运行上面的代码,结果如下:

1 1 9
2 2 9
3 3 9
4 4 9
5 5 9
6 6 9
7 7 9
8 8 9
9 9 9
panic: runtime error: slice bounds out of range [:10] with capacity 9

goroutine 1 [running]:
main.Extend(...)
	/tmp/sandbox1646997272/prog.go:16
main.main()
	/tmp/sandbox1646997272/prog.go:27 +0xde

Program exited: status 2.

可以看到,在 main 函数的 i=10 的循环中,程序 panic 了。这是因为 sliceHeader 其实有三个我们需要关心的元素,除了前面提到的指针、长度 Length 外,还有一个容量 Capacity

type sliceHeader struct {
    Length        int
    Capacity      int
    ZerothElement *byte
}

容量 Capacity 记录了 slice 底层数组的具体空间最大容量限制:也就是从 ZerothElement 指针算起、最远能访问底层数组的长度,在上例子中应该是 9。

即,当例子中,我们通过 slice := iBuffer[0:0] 创建 slice 时,它的 sliceHeader 是下面这样的:

slice := sliceHeader{
    Length:        0,
    Capacity:      10,
    ZerothElement: &iBuffer[0],
}

Capacity 是底层数组 iBuffer 的长度(10)减去 slice 指针指向元素所在索引(1)得到的,也就是 10-1=9。

而在 i=10 的循环中,我们通过 Extend 函数,将 slice 增长至超过它的容量、也就是超过底层数组 iBuffer 的最大索引,因此在访问的时候触发了切片越界的 panic。

代码里我们可以看到,通过 len 函数可以获取一个 slice 的长度,cap 则可以获取一个 slice 的容量;当 slice 的长度和容量相等时,这个 slice 就是一个「满的 slice」了:

if cap(slice) == len(slice) {
    fmt.Println("slice is full!")
}

Slice 的相关函数

前面提到了 slice 的一些基本属性,Golang 中还内置了一些操纵 slice 的函数。

Make

前面提到,slice 的增长必须要在 capacity 的限制内;不过我们可以通过分配一个新数组、拷贝原有数组的数据,然后让 slice 将这个新数组作为其底层数组,间接实现 slice 的增长。

通过 Golang 的内置函数 new 我们可以分配新的数组,然后进行上述操作。不过另一个内置函数 make 会是一个更简单的选择。

make 函数会先分配一个匿名数组,然后创建并返回一个指向这个匿名数组的 slice,它共有 3 个参数:

  1. slice 的类型
  2. slice 的初始长度
  3. slice 的容量(也就是匿名数组的长度)

创建的 slice 指向匿名数组的第一个元素,因此容量和匿名数组的长度相等。下面我们便创建了一个长度为 10,容量为 15 的 slice:

slice := make([]int, 10, 15)
fmt.Printf("len: %d, cap: %d\n", len(slice), cap(slice)) // len: 10, cap: 15

在使用时,也可以忽略第三个容量参数,这样 make 会默认将第二个参数(长度)作为 slice 的容量:

gophers := make([]Gopher, 10)

上面代码我们便创建了一个长度、容量都是 10 的 slice。

相比于形如 nums[0:10] 这样明确引用数组,实践中更多是通过 make 来声明 slice,这样我们就可以直接操纵灵活的 slice 而无需先自己声明 array 了。

copy

在前面的例子中,可以看到由于 sliceHeader 传递了一个指向底层数组的指针,我们对指针指向的数组元素进行修改时会被原 slice 所感知,因此有时候需要对 slice 进行深拷贝(也就是新建一个 slice,将原有 slice 中的元素复制到新 slice 中)。

Golang 提供了内置函数 copy 来实现这一功能,下例中我们新建了一个容量为两倍的 newSlice

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

copy 函数复制的元素个数为 min[len(src), len(dst)],例如下面的例子:

func len_0() {
	src := []int{1, 2, 3}
	dst := make([]int, 0, 1) // len(dst) = 0
	fmt.Println(dst)         // []

	// len(dst)==0, won't copy item
	copy(dst, src)
	fmt.Println(dst) // []
}

func len_1() {
	src := []int{1, 2, 3}
	dst := make([]int, 1) // len(dst) = 1
	fmt.Println(dst)      // [0]

	// len(dst)==1, copy src[0] to dst[0]
	copy(dst, src)
	fmt.Println(dst) // [1]
}

copy 函数允许源 slice 和目标 slice 存在重叠,在下面代码中,我们利用这一特性实现了「在 slice 指定位置插入元素」:

// Insert inserts the value into the slice at the specified index,
// which must be in range.
// The slice must have room for the new element.
func Insert(slice []int, index, value int) []int {
    // Grow the slice by one element.
    slice = slice[0 : len(slice)+1]
    // Use copy to move the upper part of the slice out of the way and open a hole.
    copy(slice[index+1:], slice[index:])
    // Store the new value.
    slice[index] = value
    // Return the result.
    return slice
}

代码中我们通过 copy(slice[index+1:], slice[index:]) 将 slice 中索引 index 及之后的元素复制到 slice 的 index+1 索引位置(会覆盖 index+1 开始的元素),然后在 index 位置插入新的元素。下面则是使用的一个例子:

slice := make([]int, 5, 10) // Note capacity > length: room to add element.
for i := range slice {
    slice[i] = i
}
fmt.Println(slice) // [0 1 2 3 4]

slice = Insert(slice, 4, 99) // [0 1 2 3 99 4]
fmt.Println(slice)

append

在前面谈论 campacity 时,我们用 Extend 方法举例;这里我们通过前面讲到的 copy 实现一个具有鲁棒性的 Extend 方法:

func Extend(slice []int, element int) []int {
    n := len(slice)
    if n == cap(slice) {
        // Slice is full; must grow.
        // We double its size and add 1, so if the size is zero we still grow.
        newSlice := make([]int, len(slice), 2*len(slice)+1)
        copy(newSlice, slice)
        slice = newSlice
    }
    slice = slice[0 : n+1]
    slice[n] = element
    return slice
}

如上所示,我们在尝试 Extend 时,先判断当前 slice 的长度是否已经达到了最大容量,如果是的话,我们通过 copy 方法将它的容量扩充为 2*length+1,然后在进行实际的 Extend 操作。

一般而言,如果能提前知道 slice 的最大容量,最好在初始化时、也就是调用 make 时显式指定,这样可以避免后续 append

posted @ 2022-04-20 00:52  KawaiHe  阅读(54)  评论(0编辑  收藏  举报