Go基础3 | 青训营笔记

75 阅读5分钟

这是我参与「第五届青训营 」笔记创作活动的第3天,今天的文章主要介绍数组及切片的使用方法及底层实现。

数组

定义方式

var 数组名[长度]数组类型 = [长度]数组类型{数组初始化} 或者 var 数组名[长度]数组类型 //此时数组中元素的值为类型对应的零值 或者 数组名:=[长度]数组类型{数组初始化} //如果定义长度和初始化的数据个数不等,则会补零值 //当然长度也可以保留,使用[...]来代替[长度],此时数组长度是根据初始值个数来计算


var a [3]int             // array of 3 integers
var q [3]int = [3]int{1, 2, 3}
var r [3]int = [3]int{1, 2}
fmt.Println(r[2]) // "0"
q := [...]int{1, 2, 3}

数组的长度是数组类型的一个组成部分,因此[3]int和[4]int是两种不同的数组类型。数组的长度必须是常量表达式,因为数组的长度需要在编译阶段确定

因此自由长度和元素类型一样的数组才可以比较大小,否则会报错。

a := [2]int{1, 2}
b := [...]int{1, 2}
c := [2]int{1, 3}
fmt.Println(a == b, a == c, b == c) // "true false false"
d := [3]int{1, 2}
fmt.Println(a == d) // compile error: cannot compare [2]int == [3]int

此外,在数组初始化时可以自定义顺序进行初始化,也就是通过指定索引和对应值列表的方式进行初始化,如下面代码所示:

type Currency int

const (
    USD Currency = iota // 美元
    EUR                 // 欧元
    GBP                 // 英镑
    RMB                 // 人民币
)
//定义常量时,使用const,而不是var
symbol := [...]string{USD: "$", EUR: "€", GBP: "£", RMB: "¥"}

fmt.Println(RMB, symbol[RMB]) // "3 ¥"

如果这样初始化数组时,没有用到的索引可以省略,未指定初始值的元素将用零值初始化。

切片

slice类型一般写作[]T(在函数的形参中用它代表slice)

slice底层引用了一个数组对象,slice对象由三部分构成:指针,长度和容量

并且,多个slice之间可以共享底层的数据

💡 如果切片操作超出了容量上限将会导致panic异常,但是超出长度则意味着扩展了slice,如下面代码所示:
fmt.Println(summer[:20]) // panic: out of range

endlessSummer := summer[:5] // extend a slice (within capacity)
fmt.Println(endlessSummer)  // "[June July August September October]"

切片操作是常量时间复杂度,这是因为新的切片new与原始切片old共享底层数组,也就意味着对new的更改将会导致old的改变。

slice的初始化方式:

s := []类型{初始化列表}
//上述方式与数组初始化很类似,但是slice并没有指明序列的长度
var s []类型

make([]T, len)
make([]T, len, cap) // same as make([]T, cap)[:len]
//如果省略容量的话,容量将等于长度

在底层,make创建了一个匿名的数组变量,然后返回一个slice;只有通过返回的slice才能引用底层匿名的数组变量。在第一种语句中,slice是整个数组的view。在第二个语句中,slice只引用了底层数组的前len个元素,但是容量将包含整个的数组。额外的元素是留给未来的增长用的。

💡 slice之间不能能进行比较,slice唯一合法的操作是和nil比较,一个零值的slice等于nil。**一个nil值的slice并没有底层数组**。

一个nil值的slice的长度和容量都是0,但是也有非nil值的slice的长度和容量也是0的,例如[]int{}或make([]int, 3)[3:]。与任意类型的nil值一样,我们可以用[]int(nil)类型转换表达式来生成一个对应类型slice的nil值。

var s []int    // len(s) == 0, s == nil
s = nil        // len(s) == 0, s == nil
s = []int(nil) // len(s) == 0, s == nil
s = []int{}    // len(s) == 0, s != nil

如果你需要测试一个slice是否是空的,使用len(s) == 0来判断,而不应该用s == nil来判断。

append函数的实现

下面是第一个版本的appendInt函数,专门用于处理[]int类型的slice

func appendInt(x []int, y int) []int {
    var z []int
    zlen := len(x) + 1
    if zlen <= cap(x) {
        // There is room to grow.  Extend the slice.
        z = x[:zlen]
    } else {
        // There is insufficient space.  Allocate a new array.
        // Grow by doubling, for amortized linear complexity.
        zcap := zlen
        if zcap < 2*len(x) {
            zcap = 2 * len(x)
        }
        z = make([]int, zlen, zcap)
        **copy(z, x)** // a built-in function; see text
    //内置的copy函数可以方便地将一个slice复制另一个相同类型的slice。copy函数的第一个参数是要复制的目标slice,第二个参数是源slice
		}
    z[len(x)] = y
    return z
}

发现,如果容量不够,则会重新分配slice,此时返回的slice和传入的slice的底层内存空间不同。因此,通常是将append返回的结果直接赋值给输入的slice变量。

我们的appendInt函数每次只能向slice追加一个元素,但是内置的append函数则可以追加多个元素,甚至追加一个slice。如下面代码所示:

var x []int
x = append(x, 1)
x = append(x, 2, 3)
x = append(x, 4, 5, 6)
x = append(x, x...) // append the slice x
fmt.Println(x)      // "[1 2 3 4 5 6 1 2 3 4 5 6]"

通过下面的小修改,我们可以达到append函数类似的功能。其中在appendInt函数参数中的最后的“...”省略号表示接收变长的参数为slice。

func appendInt(x []int, y ...int) []int {
    var z []int
    zlen := len(x) + len(y)
    // ...expand z to at least zlen...
    copy(z[len(x):], y)
    return z
}