Array

在使用Go编程的时候,大部分情况下不用array,因为数组长度是数组类型的一部分,灵活性不够好.但是它是Golang很重要的部分,与slice有密切的关系.

定义: var buffer [256]byte

定义了一个拥有256个字节的数组,在内存中大概是这么表示的:

buffer: byte byte … 256 times … byte byte

可一通过 buffer[0]buffer[255] 访问buffer数组. 试图访问数组之外的元素会导致程序挂掉.使用内置函数 len() 返回数组的长度,在这个例子中, len(buffer) 返回256.

在Go的设计中, array是为slice服务的.

Slice

什么是slice

Slice是一种数据结构,它描绘了底层数组一段连续的部分.

定义: var slice []byte = buffer[100:150] 或着 var slice = buffer[100:150]

如果在函数中这样定义 slice := buffer[100:150] 这样slice就是底层数组buffer[100, 149]区间的一个视图.

可以简单的把slice想象成如下的数据结构:

type sliceHeader struct {
    Length  int
    Element *byte
}
slice := sliceHeader{
    Length:  50,
    Element: &buffer[100],
}

可以用slice定义slice: slice2 := slice[5:10] 新的slice包含原slice[5,9]区间的元素,也就是说包含buffer[105,109]区间的元素.类似:

slice2 := sliceHeader{
    Length:  5,
    Element: &buffer[105],
}

注意到slice2仍然指向底层的buffer数组,对slice2做改变会影响底层buffer和其它指向底层数组的slice.

函数调用时Slice的传递

要理解: 虽然slice包含了指向底层数组的指针,但是slice本身仍然是一个值类型.

在函数调用的时候传递的是类似sliceHeader的结构.比如定义了一个函数:

func f(s []int) {
    //...
}

f(slice) 在值传递的过程中,s拷贝了一份slice的sliceHeader. 也就是说 f(slice) 可以改变slice底层所指向的数组,但是不能改变slice的其它属性,比如 length 以及还有没有提及的 cap

package main

import "fmt"

func main() {
    s := make([]int, 3, 10)
    double(s)
    fmt.Println("slice: ", s, "length: ", len(s), "cap: ", cap(s))
}

func double(s []int) {
    s = append(s, s...)
}

// Output:
// slice:  [0 0 0] length:  3 cap:  10

main函数中的slice并没有改变它的 lengthcap 属性.

如果一定要改变呢?可以让 double(s []int) []int 返回改变后的slice赋值给main函数中的slice.

第二种方法是使用指向slice的指针:

package main

import "fmt"

func main() {
    s := make([]int, 2, 10)
    fmt.Println("Before: len(s) = ", len(s))
    ptrSubtractOneFromLength(&s)
    fmt.Println("After: len(s) = ", len(s))
}

func ptrSubtractOneFromLength(slicePtr *[]int) {
    *slicePtr = (*slicePtr)[0 : len(*slicePtr)-1]
}

// Output:
// Before: len(s) =  2
// After: len(s) =  1

比起第二种方法,使用指针接收者改变slice是比较常见的方式:

package main

import (
    "bytes"
    "fmt"
)

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")
    pathName.TruncateAtFinalSlash()
    fmt.Printf("%s\n", pathName)
}

// Output:
// /usr/bin

如果你只想改变slice指向的底层数组的值,可以使用值接收者:

package main

import (
    "fmt"
)

type path []byte

func (p path) ToUpper() {
    for i, b := range p {
        if b >= 'a' && b <= 'z' {
            p[i] = b + 'A' - 'a'
        }
    }
}

func main() {
    pathName := path("/usr/bin/tso")
    pathName.ToUpper()
    fmt.Printf("%s\n", pathName)
}

// Output:
// /USR/BIN/TSO

Cap

除了指向底层数组的指针和长度,slice拥有另外一个属性:容量cap.

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

cap属性表示了slice指向的底层数组最多拥有的空间长度,如果在超过容量的情况下试图增长slice会导致panic. 看看下面的函数,该函数增加一个元素到传入的切片上:

package main

import (
    "fmt"
)

func Extend(slice []int, element int) []int {
    n := len(slice)
    slice = slice[0 : n+1]
    slice[n] = element
    return slice
}

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

// Output:
// [0]
// [0 1]
// [0 1 2]
// [0 1 2 3]
// [0 1 2 3 4]
// [0 1 2 3 4 5]
// [0 1 2 3 4 5 6]
// [0 1 2 3 4 5 6 7]
// [0 1 2 3 4 5 6 7 8]
// [0 1 2 3 4 5 6 7 8 9]
// panic: runtime error: slice bounds out of range

在main函数中 slice := buffer[0:0] 相当于:

slice := sliceHeader{
    Element:  &buffer[0],
    Length:   0,
    Capacity: 10,
}

Make

如果你想不受限于slice的容量来增长它呢?从slice的定义上来说是不能做到的,容量是slice所容纳数据数量的上限.但是可以通过重新分配一个容量更大的新数组,把数据拷贝过去,让原来的slice指向新的底层数组达到扩张slice的目的.内置的 make 方法接受3个参数: slice的类型初始长度slice的容量,如果不指定容量,默认容量和其初始长度相等.它具体的工作就是创建一个指定属性的数组,然后返回一个指向底层数组的slice。

package main

import "fmt"

func Insert(slice []int, index, value int) []int {
    slice = slice[0 : len(slice)+1]
    copy(slice[index+1:], slice[index:])
    slice[index] = value
    return slice
}

func main() {
    slice := make([]int, 10, 20)
    for i := range slice {
        slice[i] = i
    }
    fmt.Println(slice)
    slice = Insert(slice, 5, 100)
    fmt.Println(slice)
}

Copy

在上一节中定义了 newslice 并且用循环去给它赋值,Go内置 copy 函数使其更简单:

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

copy 足够聪明,它只拷贝两个slice中长度最短的部分,同时它返回一个int类型的值表示有多少元素被拷贝.

下面是使用 copy 函数的一个例子:

package main

import "fmt"

func Insert(slice []int, index, value int) []int {
    slice = slice[0 : len(slice)+1]
    copy(slice[index+1:], slice[index:])
    slice[index] = value
    return slice
}

func main() {
    slice := make([]int, 10, 20)
    for i := range slice {
        slice[i] = i
    }
    fmt.Println(slice)
    slice = Insert(slice, 5, 100)
    fmt.Println(slice)
}

// Output:
// [0 1 2 3 4 5 6 7 8 9]
// [0 1 2 3 4 100 5 6 7 8 9]

Append

在Cap章节中写的 Extend 函数存在bug:如果slice的容量太小,向slice添加元素会导致程序崩溃.下面来修复这个bug:

package main

import "fmt"

func Extend(slice []int, element int) []int {
    n := len(slice)
    if n == cap(slice) {
        newSlice := make([]int, n, 2*n+1)
        copy(newSlice, slice)
        slice = newSlice
    }
    slice = slice[0 : n+1]
    slice[n] = element
    return slice
}

func main() {
    slice := make([]int, 0, 5)
    for i := 0; i < 10; i++ {
        slice = Extend(slice, i)
        fmt.Printf("len = %d, cap = %d slice = %v\n", len(slice), cap(slice), slice)
        fmt.Println("address of 0th element: ", &slice[0])
    }
}

// Output:
// len = 1, cap = 5 slice = [0]
// address of 0th element:  0xc0000160c0
// len = 2, cap = 5 slice = [0 1]
// address of 0th element:  0xc0000160c0
// len = 3, cap = 5 slice = [0 1 2]
// address of 0th element:  0xc0000160c0
// len = 4, cap = 5 slice = [0 1 2 3]
// address of 0th element:  0xc0000160c0
// len = 5, cap = 5 slice = [0 1 2 3 4]
// address of 0th element:  0xc0000160c0
// len = 6, cap = 11 slice = [0 1 2 3 4 5]
// address of 0th element:  0xc000070060
// len = 7, cap = 11 slice = [0 1 2 3 4 5 6]
// address of 0th element:  0xc000070060
// len = 8, cap = 11 slice = [0 1 2 3 4 5 6 7]
// address of 0th element:  0xc000070060
// len = 9, cap = 11 slice = [0 1 2 3 4 5 6 7 8]
// address of 0th element:  0xc000070060
// len = 10, cap = 11 slice = [0 1 2 3 4 5 6 7 8 9]
// address of 0th element:  0xc000070060

注意到当原来的slice被填满后,新分配的slice容量和地址都发生了改变,也就是说slice指向了新的底层数组.

类似于内置 append() 函数,要支持添加多个元素可以这样写:

func Append(slice []int, elements ...int) []int {
    n := len(slice)
    total := len(slice) + len(elements)
    if total > cap(slice) {
        newCap := total*2 + 1
        newSlice := make([]int, total, newCap)
        copy(newSlice, slice)
        slice = newSlice
    }
    slice = slice[:total]
    copy(slice[n:], elements)
    return slice
}

这就是Go内置的 append() 的工作原理,只是在容量增长算法上更复杂,支持任何类型的slice.

nil slice

nil slice 是指:

nilSlice := sliceHeader{
    Element:  nil,
    Length:   0,
    Capacity: 0,
}

或者这样定义: nilSlice := sliceHeader{}

但是这样定义的slice不属于nil slice: notNilSlice := buffer[0:0]

关键点在于指向底层数组的指针是否为空.