Skip to content

内建容器

很难遇到要编写一个不需要存储和读取集合数据的程序的情况。
如果使用数据库或者文件,或者访问网络,总需要一种方法来处理接收和发送的数据。
Go 语言有 3 种数据结构可以让用户管理集合数据: 数组、切片和映射。
这 3 种数据结构是语言核心的一部分,在标准库里被广泛使用。
一旦学会如何使用这些数据结构,用 Go 语言编写程序会变得快速、有趣且十分灵活。

在主流的编程语言中数组及其相关的数据结构是使用得最为频繁的,只有在它(们)不能满足时才会考虑链表、hash表(hash表可以看作是数组和链表的混合体)和更复杂的自定义数据结构。

Go语言中数组、字符串和切片三者是密切相关的数据结构。
这三种数据类型,在底层原始数据有着相同的内存结构,在上层,因为语法的限制而有着不同的行为表现。
首先,Go语言的数组是一种值类型,虽然数组的元素可以被修改,但是数组本身的赋值和函数传参都是以整体复制的方式处理的。
Go语言字符串底层数据也是对应的字节数组,但是字符串的只读属性禁止了在程序中对底层字节数组的元素的修改。
字符串赋值只是复制了数据地址和对应的长度,而不会导致底层数据的复制。
切片的行为更为灵活,切片的结构和字符串结构类似,但是解除了只读限制。
切片的底层数据虽然也是对应数据类型的数组,但是每个切片还有独立的长度和容量信息,切片赋值和函数传参数时也是将切片头信息部分按传值方式处理。
因为切片头含有底层数据的指针,所以它的赋值也不会导致底层数据的复制。
其实Go语言的赋值和函数传参规则很简单,除了闭包函数以引用的方式对外部变量访问之外,其它赋值和函数传参数都是以传值的方式处理。
要理解数组、字符串和切片三种不同的处理方式的原因需要详细了解它们的底层数据结构。

数组

了解这些数据结构,一般会从数组开始,因为数组是切片和映射的基础数据结构。
理解了数组的工作原理,有助于理解切片的映射提供的优雅和强大的功能。

定义数组类型时,数组长度必须是非负整型常量表达式,长度是类型组成部分。
也就是说,元素类型相同,但长度不同的数组不属于同一类型

1
2
3
4
5
func main() {
    var d1[3]int
    var d2[2]int
    d1 = d2 // 错误: cannot use d2(type[2])int as type[3]int in assignment
}

灵活的初始化方式:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package main

import (
    "fmt"
)

func main() {
    var a [4]int // 元素自动初始化为零

    b := [4]int{2, 5}     // 未提供初始值的元素自动初始化为 0
    c := [4]int{5, 3: 10} // 可指定索引位置初始化

    d := [...]int{1, 2, 3}    // 编译器按初始化值数量确定数组长度
    e := [...]int{10, 3: 100} // 支持索引初始化,但注意数组长度与此有关

    fmt.Println(a, b, c, d, e)
}

输出:

1
[0 0 0 0] [2 5 0 0] [5 0 0 10] [1 2 3] [10 0 0 100]

对于结构等复合类型,可省略元素初始化类型标签。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
package main

import (
    "fmt"
)

func main() {
    type user struct {
        name string
        age  byte
    }

    d := [...]user{
        {"Tome", 20}, // 省略了类型标签
        {"Mary", 18},
    }

    fmt.Printf("%#v\n", d)
}

输出:

1
[2]main.user{main.user{name:"Tome", age:0x14}, main.user{name:"Mary", age:0x12}}

在定义多维数组时,仅第一维度允许使用...

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package main

import "fmt"

func main() {
    a := [2][2]int{
        {1, 2},
        {3, 4},
    }

    b := [...][2]int{
        {10, 20},
        {30, 40},
    }

    c := [...][2][2]int{ // 三维数组
        {
            {1, 2},
            {3, 4},
        },
        {
            {10, 20},
            {30, 40},
        },
    }

    fmt.Println(a)
    fmt.Println(b)
    fmt.Println(c)
}

输出:

1
2
3
[[1 2] [3 4]]
[[10 20] [30 40]]
[[[1 2] [3 4]] [[10 20] [30 40]]]

内置函数 len 和 cap 都返回第一维度长度

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package main

func main() {
    a := [2]int{}
    b := [...][2]int{
        {10, 20},
        {30, 40},
        {50, 60},
    }
    println(len(a), cap(a))
    println(len(b), cap(b))
    println(len(b[1]), cap(b[1]))
}

输出:

1
2
3
2 2
3 3
2 2

如元素类型支持==、!=操作符,那么数组也支持此操作

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package main

func main() {
    var a, b [2]int
    println(a == b)
    c := [2]int{1, 2}
    d := [2]int{0, 1}
    println(c != d)

    var e, f [2]map[string]int
    println(e == f) // 无效操作: e==f([2]map[string]int cannot be compared)
}

内部实现

在 Go 语言里,数组是一个长度固定的数据类型,用于存储一段具有相同的类型的元素的连续块。
数组存储的类型可以是内置的类型,如整型或者字符串,也可以是某种结构类型。

数组是一种非常有用的数据结构,因为其占用的内存是连续分配的。由于内存连续,CPU 能把正在使用的数据缓存更久的时间。
而且内存连续很容易计算索引,可以快速迭代数组里的所有元素。数组的类型信息可以提供每次访问一个元素时需要在内存中移动的距离。
既然数组的每个元素类型相同,又是连续分配,就可以以固定速度索引数组中的任意数据,速度非常快。

声明和初始化

声明数组时需要指定内存存储的数据的类型,以及需要存储的元素的数量,这个数量也称为数组的长度。

声明一个数组,并设置为零值:

1
2
// 声明一个包含 5 个元素的整型数组
var array [5]int

一旦声明,数组里存储的数据类型和数组长度就都不能改变了。如果需要存储更多的元素,就需要先创建一个更长的数组,再把原来数组里的值复制到新数组里。

在 Go 语言中声明变量时,总会使用对应类型的零值来对变量进行初始化。数组也不例外。
当数组初始化时,数组内每个元素都初始化为对应类型的零值。整型数组里的每个元素都初始化为 0,也就是整型的零值。

一种快速创建数组并初始化的方式时使用数组字面量。
数组字面量允许声明数组里元素的数量同时指定每个元素的值

1
2
3
// 声明一个包含 5 个元素的整型数组
// 用具体值初始化每个元素
array := [5]int{10, 20, 30, 40, 50}

如果使用...替代数组的长度,Go 语言会根据初始化时数组元素的数量来确定该数组的长度

1
2
3
4
// 声明一个整型数组
// 用具体值初始化每个元素
// 容量由初始化值的数量决定
array := [...]int{10, 20, 30, 40, 50}

也可以指定一个索引和对应值列表的方式初始化,就像下面这样:

1
2
3
4
// 声明一个有 5 个元素的数组
// 用具体值初始化索引为 1 和 2 的元素
// 其余元素保持零值
array := [5]int{1: 10, 2: 20}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
type Currency int

const (
    USD Currency = iota // 美元
    EUR                 // 欧元
    GBP                 // 英镑
    RMB                 // 人民币
)

symbol := [...]string{USD: "$", EUR: "€", GBP: "£", RMB: "¥"}

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

在这种形式的数组字面值形式中,初始化索引的顺序是无关紧要的,而且没用到的索引可以省略,和前面提到的规则一样,未指定初始值的元素将用零值初始化。例如,

1
r := [...]int{99: -1}

定义了一个含有100个元素的数组r,最后一个元素被初始化为-1,其它元素都是用0初始化。

如果一个数组的元素类型是可以相互比较的,那么数组类型也是可以相互比较的,这时候我们可以直接通过==比较运算符来比较两个数组,只有当两个数组的所有元素都是相等的时候数组才是相等的。
不相等比较运算符 != 遵循同样的规则。

1
2
3
4
5
6
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

使用数组

正像之前提到的,因为内存布局是连续的,所以数组是效率很高的数据结构。
在访问数组里任意元素的时候,这种高效都是数组的优势。要访问数组里某个单独元素,使用[]运算符

1
2
3
4
5
6
// 声明一个包含 5 个元素的整型数组
// 用具体值初始为每个元素
array := [5]int{10, 20, 30, 40, 50}

// 修改索引为 2 的元素的值
array[2] = 35

声明一个所有元素都是指针的数组。使用*运算符就可以访问元素指针所指向的值。

1
2
3
4
5
6
7
// 声明包含 5 个元素的指向整数的数组
// 用整型指针初始化索引为 0 和 1 的数组元素
array := [5]*int{0: new(int), 1: new(int)}

// 为索引为 0 和 1 的元素赋值
*array[0] = 10
*array[1] = 20

在 Go 语言里,数组是一个值。这意味着数组可以用在赋值操作中。
变量名代表整个数组,因此,同样类型的数组可以赋值给另一个数组

1
2
3
4
5
6
7
8
9
// 声明第一个包含 5 个元素的字符串数组
var array1 [5]string

// 声明第二个包含 5 个元素的字符串数组
// 用颜色初始化数组
array2 := [5]string{"Red", "Blue", "Green", "Yellow", "Pink"}

// 把 array2 的值复制到 array1
array1 = array2

复制之后,两个数组的值完全一样。
数组变量的类型包括数组长度和每个元素的类型。只有这两部分都相同的数组,才是类型相同的数组,才能互相赋值。

编译器会阻止类型不同的数组互相赋值

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 声明第一个包含 4 个元素的字符串数组
var array1 [4] string

// 声明第二个包含 5 个元素的字符串数组
// 使用颜色初始化数组
array2 := [5]string{"Red", "Blue", "Green", "Yellow", "Pink"}

// 将 array2 复制给 array1
array1 = array2

Compiler Error:
cannot use array2(type [5]string) as type [4]string in assignment

多维数组

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 声明一个二维整型数组,两个维度分别存储 4 个元素和 2 个元素
var array [4][2]int

// 使用数组字面量来声明并初始化一个二维整型数组
array := [4][2]int{{10, 11}, {20, 21}, {30, 31}, {40, 41}}

// 声明并初始化外层数组中索引为 1 和 3 的元素
array := [4][2]int{1: {20, 21}, 3: {40, 41}}

// 声明并初始化外层数组和内层数组的单个元素
array := [4][2]int{1: {0: 20}, 3: {1: 41}}

为了访问单个元素,需要反复组合使用[]运算符

1
2
3
4
5
6
7
8
// 声明一个 2 乘 2 的二维整型数组
var array [2][2]int

// 设置每个元素的整型值
array[0][0] = 10
array[0][1] = 20
array[1][0] = 30
array[1][1] = 40

在函数间传递数组

根据内存和性能来看,在函数间传递数组是一个开销很大的操作。
在函数之间传递变量时,总是以值的方式传递的。如果这个变量是一个数组,意味着整个数组,不管有多长,都会完整复制,并传递给函数

为了考察这个操作,我们来创建一个包含 100 万个 int 类型元素的数组。
在 64 位架构上,这将需要 800 万字节,即 8MB 的内存。如果声明了这种大小的数组,并将其传递给函数,会发生什么呢?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 声明一个需要 8MB 的数组
var array [1e6]int

// 将数组传递给函数 foo
foo(array)

// 函数 foo 接受一个 100 万个整型值的数组
func foo(array [1e6]int) {
    ...
}

每次函数 foo 被调用时,必须在栈上分配 8MB 的内存。之后,整个数组的值(8 MB的内存)被复制到刚分配的内存里。
虽然 Go 语言自己会处理这个复制操作,不过还有一种更好且更有效的方法来处理这个操作。
可以只传入指向数组的指针,这样只需要复制 8 字节的数据而不是 8 MB 的内存数据到栈上。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 分配一个需要 8MB 的数组
var array [1e6]int

// 将数组的地址传递给函数 foo
foo(&array)

// 函数 foo 接受一个指向 100 万个整型值的数组的指针
func foo(array *[1e6]int) {
    ...
}

这次函数 foo 接受一个指向 100 万个整型值的数组的指针。
现在将数组的地址传入函数,只需要在栈上分配 8 字节的内存给指针就可以。

这个操作会更有效地利用内存,性能也更好。
不过要意识到,因为现在传递的是指针,所以如果改变指针指向的值,会改变共享的内存。如你所见,使用切片能更好地处理这类共享问题。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package main

import "fmt"

func main() {
    var arr1 [5]int
    arr2 := [3]int{1, 3, 5}
    arr3 := [...]int{2, 4, 6, 8, 10}
    var grid [4][5]int
    fmt.Println(arr1, arr2, arr3, grid)
}
// [0 0 0 0 0] [1 3 5] [2 4 6 8 10] [[0 0 0 0 0] [0 0 0 0 0] [0 0 0 0 0] [0 0 0 0 0]]
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package main

import "fmt"

func main() {
    var arr1 [5]int
    arr2 := [3]int{1, 3, 5}
    arr3 := [...]int{2, 4, 6, 8, 10}
    var grid [4][5]int
    fmt.Println(arr1, arr2, arr3, grid)
    // for i := 0; i < len(arr3); i++ {
    //  fmt.Println(arr3[i])
    // }
    // for i := range arr3 {
    //  fmt.Println(arr3[i])
    // }
    for i, v := range arr3 {
        fmt.Println(i, v)
    }
    // for _, v := range arr3 {
    //  fmt.Println(v)
    // }
}
// [0 0 0 0 0] [1 3 5] [2 4 6 8 10] [[0 0 0 0 0] [0 0 0 0 0] [0 0 0 0 0] [0 0 0 0 0]]
// 0 2
// 1 4
// 2 6
// 3 8
// 4 10

数组是值类型

[10]int[20]int 是不同类型
调用 func f(arr [10]int)拷贝数组

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package main

import "fmt"

func printArray(arr [5]int) {
    arr[0] = 100
    for i, v := range arr {
        fmt.Println(i, v)
    }
}

func main() {
    var arr1 [5]int
    arr3 := [...]int{2, 4, 6, 8, 10}
    printArray(arr1)
    printArray(arr3)
    fmt.Println(arr1, arr3)
}
// 0 100
// 1 0
// 2 0
// 3 0
// 4 0
// 0 100
// 1 4
// 2 6
// 3 8
// 4 10
// [0 0 0 0 0] [2 4 6 8 10]
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package main

import "fmt"

func printArray(arr *[5]int) {
    arr[0] = 100
    for i, v := range arr {
        fmt.Println(i, v)
    }
}

func main() {
    var arr1 [5]int
    arr3 := [...]int{2, 4, 6, 8, 10}
    printArray(&arr1)
    printArray(&arr3)
    fmt.Println(arr1, arr3)
}
// 0 100
// 1 0
// 2 0
// 3 0
// 4 0
// 0 100
// 1 4
// 2 6
// 3 8
// 4 10
// [100 0 0 0 0] [100 4 6 8 10]

指针

要分清指针数组和数组指针的区别。
指针数组是指元素为指针类型的数组,数组指针是获取数组变量的地址

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package main

import "fmt"

func main() {
    x, y := 10, 20
    a := [...]*int{&x, &y} // 元素为指针的指针数组
    p := &a                // 存储数组地址的指针

    fmt.Printf("%T, %v\n", a, a)
    fmt.Printf("%T, %v\n", p, p)
}

输出:

1
2
[2]*int, [0xc0000ae008 0xc0000ae010]
*[2]*int, &[0xc0000ae008 0xc0000ae010]

可获取任意元素地址

1
2
3
4
5
6
package main

func main() {
    a := [...]int{1, 2}
    println(&a, &a[0], &a[1])
}

输出:

1
0xc000038768 0xc000038768 0xc000038770

数组指针可直接用来操作元素

1
2
3
4
5
6
7
8
9
package main

func main() {
    a := [...]int{1, 2}
    p := &a

    p[1] += 10
    println(p[1])
}

输出:

1
12

可通过 unsafe.Pointer 转换不同长度的数组指针来实现越界访问,或使用参数 gcflags "-B" 阻止编译器插入检查指令。只是想不出什么时候会有这个需求

复制

与 C 数组变量隐式作为指针使用不同,Go 数组是值类型,赋值和传参操作都会复制整个数组数据

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
package main

import "fmt"

func test(x [2]int) {
    fmt.Printf("x: %p, %v\n", &x, x)
}

func main() {
    a := [2]int{10, 20}

    var b [2]int
    b = a

    fmt.Printf("a: %p, %v\n", &a, a)
    fmt.Printf("b: %p, %v\n", &b, b)

    test(a)
}

输出:

1
2
3
a: 0xc000122010, [10 20]
b: 0xc000122020, [10 20]
x: 0xc000122060, [10 20]

如果需要,可改用指针或切片,以此避免数据复制

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package main

import "fmt"

func test(x *[2]int) {
    fmt.Printf("x: %p, %v\n", x, *x)
    x[1] += 100
}

func main() {
    a := [2]int{10, 20}

    test(&a)

    fmt.Printf("a: %p, %v\n", &a, a)
}

输出:

1
2
x: 0xc000014080, [10 20]
a: 0xc000014080, [10 120]

空数组

我们还可以定义一个空的数组:

1
2
3
var d [0]int       // 定义一个长度为0的数组
var e = [0]int{}   // 定义一个长度为0的数组
var f = [...]int{} // 定义一个长度为0的数组

长度为0的数组在内存中并不占用空间。
空数组虽然很少直接使用,但是可以用于强调某种特有类型的操作时避免分配额外的内存空间,比如用于管道的同步操作:

1
2
3
4
5
6
c1 := make(chan [0]int)
go func() {
    fmt.Println("c1")
    c1 <- [0]int{}
}()
<-c1

在这里,我们并不关心管道中传输数据的真实类型,其中管道接收和发送操作只是用于消息的同步。
对于这种场景,我们用空数组来作为管道类型可以减少管道元素赋值时的开销。
当然一般更倾向于用无类型的匿名结构体代替:

1
2
3
4
5
6
c2 := make(chan struct{})
go func() {
    fmt.Println("c2")
    c2 <- struct{}{} // struct{}部分是类型, {}表示对应的结构体值
}()
<-c2

我们可以用fmt.Printf函数提供的%T或%#v谓词语法来打印数组的类型和详细信息:

1
2
fmt.Printf("b: %T\n", b)  // b: [3]int
fmt.Printf("b: %#v\n", b) // b: [3]int{1, 2, 3}

在Go语言中,数组类型是切片和字符串等结构的基础。数组的很多操作都可以直接用于字符串或切片中。

在 go 语言中一般不直接使用数组

切片

切片是一种数据结构,这种数据结构便于使用和管理数据集合。
切片是围绕动态数组的概念构建的,可以按需自动增长和缩小。切片的动态增长是通过内置函数 append 来实现的。
这个函数可以快速且高效地增长切片。还可以通过对切片再次切片来缩小一个切片的大小。
因为切片的底层内存也是在连续块中分配的,所以切片还能获取索引、迭代以及为垃圾回收优化的好处

切片本身并非动态数组或数组指针。
它内部通过指针引用底层数组,设定相关属性将数据读写操作限定在指定区域内。

1
2
3
4
5
type slice struct {
    array unsafe.Pointer
    len int
    cap int
}

切片本身是个只读对象,其工作机制类似数组指针的一种包装

可基于数组或数组指针创建切片,以开始和结束索引位置确定所引用的数组片段。
不支持反向索引,实际范围是一个右半开区间

属性 cap 表示切片所引用数组片段的真实长度,len 用于限定可读的写元素数量。
另外,数组必须 addressable,否则会引发错误

1
2
3
4
5
6
7
8
func main() {
    m := map[string][2]int{
        "a": {1, 2},
    }

    s := m["a"][:]  // 无效操作: m["a"][:] (slice of unaddressable value)
    fmt.Println(s)
}

和数组一样,切片同样使用索引号访问元素内容。起始索引为 0,而非对应的底层数组真实索引位置

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
package main

func main() {
    x := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
    s := x[2:5]

    for i := 0; i < len(s); i++ {
        println(s[i])
    }
}

输出:

1
2
3
2
3
4

可直接创建切片对象,无须预先准备数组。
因为是引用类型,须使用 make 函数或显式初始化语句,它会自动完成底层数组内存分配

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package main

import "fmt"

func main() {
    s1 := make([]int, 3, 5)    // 指定 len、cap 底层数组初始化为零值
    s2 := make([]int, 3)       // 省略 cap 和 len 相等
    s3 := []int{10, 20, 5: 30} // 按初始化元素分配底层数组,并设置 len、cap

    fmt.Println(s2, len(s2), cap(s2))
    fmt.Println(s1, len(s1), cap(s1))
    fmt.Println(s3, len(s3), cap(s3))
}

输出:

1
2
3
[0 0 0] 3 3
[0 0 0] 3 5
[10 20 0 0 0 30] 6 6

注意下面两种定义方式的区别。
前者仅定义了一个[]int类型变量,并未执行初始化操作,而后者则用初始化表达式完成了全部创建过程

1
2
3
4
5
6
7
8
package main

func main() {
    var a []int
    b := []int{}

    println(a == nil, b == nil)
}

输出:

1
true false

通过输出更详细的信息,我们可以看到两者的差异

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package main

import (
    "fmt"
    "reflect"
    "unsafe"
)

func main() {
    var a []int
    b := []int{}

    fmt.Printf("a: %#v\n", (*reflect.SliceHeader)(unsafe.Pointer(&a)))
    fmt.Printf("b: %#v\n", (*reflect.SliceHeader)(unsafe.Pointer(&b)))
    fmt.Printf("a size: %d\n", unsafe.Sizeof(a))
}

输出:

1
2
3
a: &reflect.SliceHeader{Data:0x0, Len:0, Cap:0}
b: &reflect.SliceHeader{Data:0x118e390, Len:0, Cap:0}
a size: 24

变量 b 的内部指针被赋值,尽管它指向 runtime.zerobase,但它依然完成了初始化操作

另外,a == nil,仅表示它是个未初始化的切片对象,切片本身依然会分配所需内存。
可以直接对 nil 切片执行slice[:]操作,同样返回 nil

不支持比较操作,就算元素类型支持也不行,仅能判断是否为 nil

1
2
3
4
5
6
func main() {
    a := make([]int, 1)
    b := make([]int, 1)

    println(a == b)  // 无效操作: a==b(slice can only be compared to nil)
}

可获取元素地址,但不能像数组那样直接用指针访问元素内容

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
    "fmt"
)

func main() {
    s := []int{0, 1, 2, 3, 4}

    p := &s     // 取 header 地址
    p0 := &s[0] // 取 array[0] 地址
    p1 := &s[1]

    println(p, p0, p1)

    (*p)[0] += 100 // *[]int 不支持索引操作,须先返回 []int 对象
    *p1 += 100     // 直接用元素指针操作

    fmt.Println(s)
}

输出:

1
2
0xc000092f60 0xc00010c030 0xc00010c038
[100 101 2 3 4]

如果元素类型也是切片,那么就可实现类似交错数组功能

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package main

import (
    "fmt"
)

func main() {
    x := [][]int{
        {1, 2},
        {10, 20, 30},
        {100},
    }

    fmt.Println(x[1])
    x[2] = append(x[2], 200, 300)
    fmt.Println(x[2])
}

输出:

1
2
[10 20 30]
[100 200 300]

很显然,切片只是很小的结构体对象,用来代替数组传参可避免复制开销。
还有,make 函数允许在运行期动态指定数组长度,绕开了数组类型必须使用编译器常量的限制

并非所有时候都适合用切片代替数组,因为切片底层数组可能会在堆上分配内存。
而且小数组在栈上拷贝的消耗也未必就比 make 代价大

内部实现

切片是一个很小的对象,对底层数组进行了抽象,并提供相关的操作方法。
切片是有 3 个字段的数据结构,这些数据结构包含 Go 语言需要操作底层数组的元数据。

这 3 个字段分别是指向底层数组的指针、切片访问的元素的个数(即长度)和切片允许增长到的元素个数(即容量)

创建和初始化

Go 语言中有几种方法可以创建和初始化切片。

make 和切片字面量

一种创建切片的方法是使用内置的 make 函数。当使用 make 时,需要传入一个参数,指定切片的长度

1
2
3
// 创建一个字符串切片
// 其长度和容量都是 5 个元素
slice := make([]string, 5)

如果只指定长度,那么切片的容量和长度相等。也可以分别指定长度和容量:

1
2
3
// 创建一个整型切片
// 其长度为 3 个元素,容量为 5 个元素
slice := make([]int, 3, 5)

分别指定长度和容量时,创建的切片,底层数组的长度时指定的容量,但是初始化后并不能访问所有的数组元素。

另一种常用的创建切片的方法时使用切片字面量。
这种方法和创建数组类似,只是不需要指定[]运算符里的值。初始的长度和容量会基于初始化时提供的元素的个数确定。

1
2
3
4
5
6
7
// 创建字符串切片
// 其长度和容量都是 5 个元素
slice := []string{"Red", "Blue", "Green", "Yellow", "Pink"}

// 创建一个整型切片
// 其长度和容量都是 3 个元素
slice := []int{10, 20, 30}

当使用切片字面量时,可以设置初始长度和容量。要做的就是在初始化时给出所需的长度和容量作为索引。

1
2
3
// 创建字符串切片
// 使用空字符串初始化第 100 个元素
slice := []string{99: ""}

记住,如果在[]运算符里指定一个值,那么创建的就是数组而不是切片。只有不指定值的时候,才会创建切片。

1
2
3
4
5
// 创建有 3 个元素的整型数组
array := [3]int{10, 20, 30}

创建长度和容量都是 3 的整型切片
slice := []int{10, 20, 30}

nil 和空切片

有时,程序可能需要声明一个值为 nil 的切片(也成 nil 切片)。
只要在声明时不做任何初始化,就会创建一个 nil 切片

1
2
// 创建 nil 整型切片
var slice []int

在 Go 语言里,nil 切片时很常见的创建切片的方法。nil 切片可以用于很多标准库和内置函数。
在需要描述一个不存在的切片时,nil 切片会很好用。例如,函数要求返回一个切片但是发生异常的时候。

1
2
3
4
5
// 使用 make 创建空的整型切片
slice := make([]int, 0)

// 使用切片字面量创建空的整型切片
slice := []int{}

空切片在底层数组包含 0 个元素,也没有分配任何存储空间。 想表示空集合时空切片很有用,例如,数据库查询返回 0 个查询结果时。
不管时使用 nil 切片还是空切片,对其调用内置函数 append、len 和 cap 的效果都是一样的。

使用切片

赋值和切片

对切片里某个索引指向的元素赋值和对数组里某个索引指向的元素赋值的方法完全一样。

1
2
3
4
5
// 创建一个整型切片
// 其容量和长度都是 5 个元素
slice := []int{10, 20, 30, 40, 50}
// 改变索引为 1 的元素的值
slice[1] = 25

切片之所以被称为切片,是因为创建一个新的切片就是把底层数组切出一部分

1
2
3
4
5
6
7
// 创建一个整型切片
// 其长度和容量都是 5 个元素
slice := []int{10, 20, 30, 40, 50}

// 创建一个新的切片
// 其长度为 2 个元素,容量为 4 个元素
newSlice := slice[1:3]

执行完代码后,我们有了两个切片,它们共享同一段底层代码,但通过不同的切片会看到底层数组的不同部分。

第一个切片 slice 能够看到底层数组全部 5 个元素的容量,不过之后的 newSlice 就看不到。
对于 newSlice,底层数组的容量只有 4 个元素。newSlice 无法访问到它所指向的底层数组的第一个元素之前的部分。
所以,对 newSlice 来说,之前的那些元素就是不存在的。

对于底层数组容量是 k 的切片slice[i:j]来说,长度:j - i,容量: k - i

需要记住的是,现在两个切片共享同一个底层数组。如果一个切片修改了该底层数组的共享部分,另一个切片也能感知到。

1
2
3
4
5
6
7
8
9
// 创建一个整型切片
// 其长度和容量都是 5 个元素
slice := []int{10, 20, 30, 40, 50}
// 创建一个新切片
// 其长度是 2 个元素,容量是 4 个元素
newSlice := slice[1:3]
// 修改 newSlice 索引为 1 的元素
// 同时也修改了原来的 slice 的第 3 个元素(索引为 2 的元素)
newSlice[1] = 35

切片增长

相对于数组而言,使用切片的一个好处是,可以按需增加切片的容量。Go 语言内置的 append 函数会处理增加长度时的所有操作细节。

要使用 append,需要一个被操作的切片和一个要追加的值。
当 append 调用返回时,会返回一个包含修改结果的新切片。
函数 append 总是会增加新切片的长度,而容量有可能会改变,也可能不会改变,这取决于被操作的切片的可用容量。

1
2
3
4
5
6
7
8
9
// 创建一个整型切片
// 其长度和容量都是 5 个元素
slice := []int{10, 20, 30, 40, 50}
// 创建一个新切片
// 其长度是 2 个元素,容量是 4 个元素
newSlice := slice[1:3]
// 使用原有的容量来分配一个新的元素
// 将新元素赋值为 60
newSlice = append(newSlice, 60)

因为 newSlice 在底层数组里还有额外的容量可用,append 操作将可用的元素合并到切片的长度,并将其进行赋值。
由于和原始的 slice 共享同一个底层数组,slice 中的索引为 3 的元素的值也被改动了

如果切片的底层数组没有足够的可用容量,append 函数会创建一个新的底层数组,将被引用的现有的值复制到新数组里,再追加新的值

1
2
3
4
5
6
7
// 创建一个整型切片
// 其长度和容量都是 4 个元素
slice := []int{10, 20, 30, 40}

// 向切片追加一个新元素
// 将新元素赋值为 50
newSlice := append(slice, 50)

当这个 append 操作完成后,newSlice 拥有一个全新的底层数组,这个数组的容量是原来的两倍

函数 append 会智能地处理底层数组的容量增长。在切片的容量小于 1000 个元素时,总是会成倍地增加容量。
一旦元素个数超过 1000,容量的增长因子会设为 1.25,也就是会每次增加 25% 的容量。随着语言的演化,这种增长算法可能会有所改变。

创建切片时的 3 个索引

在创建切片时,还可以使用之前我们没有提及的第三个索引选项。第三个索引可以用来控制新切片的容量。
其目的并不是要增加容量,而是要限制容量。可以看到,允许限制新切片的容量为底层数组提供了一定的保护,可以更好地控制追加操作。

1
2
3
4
5
6
7
// 创建字符串切片
// 其长度和容量都是 5 个元素
source := []string{"Apple", "Orange", "Plum", "Banana", "Grape"}

// 将第三个元素切片,并限制容量
// 将长度为 1 个元素,容量为 2 个元素
slice := source[2:3:4]

这个切片操作执行后,新切片里从底层数组引用了 1 个元素,容量是 2 个元素。
具体来说,新切片引用了 Plum 元素,并将容量扩展到 Banana 元素

对于slice[i:j:k][2:3:4],长度: j - i3 - 2 = 1,容量: k - i4 - 2 = 2

内置函数 append 会首先使用可用容量。一旦没有可用容量,会分配一个新的底层数组。
这导致很容易忘记切片间正在共享同一个底层数组。一旦发生这种情况,对切片进行修改,很可能会导致随机且奇怪的问题。
对切片内容的修改会影响多个切片,却很难找到问题的原因。

如果在创建切片时设置切片的容量和长度一样,就可以强制让新切片的第一个 append 操作创建新的底层数组,与原有的底层数组分离。
新切片与原有的底层数组分离后,可以安全地进行后续修改。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 创建字符串切片
// 其长度和容量都是 5 个元素
source := []string{"Apple", "Orange", "Plum", "Banana", "Grape"}

// 将第三个元素切片,并限制容量
// 将长度和容量都是 1 个元素
slice := source[2:3:3]

// 向 slice 追加新字符串
slice = append(slice, "Kiwi")

如果不加第三个索引,由于剩余的所有容量都属于 slice,向 slice 追加 Kiwi 会改变原有底层数组索引为 3 的元素的值 Banana。
不过我们限制了 slice 的容量为 1.当我们第一次对 slice 调用 append 的时候,会创建一个新的底层数组,这个数组包括 2 个元素,并将水果 Plum 复制进来,再追加新水果 Kiwi,并返回一个引用了这个底层数组的新切片.

因为新的切片 slice 拥有了自己的底层数组,所以杜绝了可能发生的问题。
我们可以继续向新切片里追加水果,而不用担心会不小心修改了其他切片里的水果。同时,也保持了为切片申请新的底层数组的简洁。

内置函数 append 也是一个可变参数的函数。这意味着可以在一次调用传递多个追加的值。
如果使用...运算符,可以将一个切片的所有元素追加到另一个切片里。

1
2
3
4
5
6
7
8
9
// 创建两个切片,并分别用两个整数进行初始化
s1 := []int{1, 2}
s2 := []int{3, 4}

// 将两个切片追加在一起,并显示结果
fmt.Printf("%v\n", append(s1, s2...))

Output:
[1 2 3 4]

就像通过输出看到的那样,切片 s3 里的所有值都追加到了切片 s1 的后面。
使用 Printf 时用来显示 append 函数返回的新切片的值

迭代切片

既然切片是一个集合,可以迭代其中的元素。
Go 语言有个特殊的关键字 range,它可以配合关键字 for 来迭代切片里的元素

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// 创建一个整型切片
// 其长度和容量都是 4 个元素
slice := []int{10, 20, 30, 40}

// 迭代每一个元素,并显示其值
for index, value := range slice {
    fmt.Printf("Index: %d Value: %d\n", index, value)
}

Output:
Index: 0 Value: 10
Index: 1 Value: 20
Index: 2 Value: 30
Index: 3 Value: 40

当迭代切片时,关键字 range 会返回两个值。第一个值是当前迭代到的索引位置,第二个值是该位置对应元素值的一份副本。

需要强调的是,range 创建了每个元素的副本,而不是直接返回对该元素的引用。如果使用该值变量的地址作为指向每个元素的指针,就会造成错误。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// 创建一个整型切片
// 其长度和容量都是 4 个元素
slice := []int{10, 20, 30, 40}

// 迭代每一个元素,并显示其值
for index, value := range slice {
    fmt.Printf("Value: %d\n Value-Addr: %X ElemAddr: %X\n", value, &value, &slice[index])
}

Output:
Value: 10 Value-Addr: 10500168 ElemAddr: 1052E100
Value: 20 Value-Addr: 10500168 ElemAddr: 1052E104
Value: 30 Value-Addr: 10500168 ElemAddr: 1052E108
Value: 40 Value-Addr: 10500168 ElemAddr: 1052E10C

因为迭代返回的变量是一个迭代过程中根据切片依次赋值的新变量,所以 value 的地址总是相同的。
要想获取每个元素的地址,可以使用切片变量和索引值。

关键字 range 总是会从切片头部开始迭代。如果想对迭代做更多控制,依旧可以使用传统的 for 循环

1
2
3
4
5
6
7
// 创建一个整型切片
// 其长度和容量都是 4 个元素
slice := []int{10, 20, 30, 40}
// 从第三个元素开始迭代每个元素
for index := 2; index < len(slice); index++ {
    fmt.Printf("Index: %d Value: %d\n", index, slice[index])
}

多维切片

1
2
// 创建一个整型切片的切片
slice := [][]int{{10}, {100, 200}}

我们有了一个包含两个元素的外层切片,每个元素包含一个内层的整型切片。
可以看到组合切片的操作是如何将一个切片嵌入到另一个切片中的。外层的切片包括两个元素,每个元素都是一个切片。
第一个元素中的切片使用单个整数 10 来初始化,第二个元素中的切片包括两个整数,即 100 和 200

1
2
3
4
5
// 创建一个整型切片的切片
slice := [][]int{{10}, {100, 200}}

// 为第一个切片追加值为 20 的元素
slice[0] = append(slice[0], 20)

Go 语言里使用 append 函数处理追加的方式很简明: 先增长切片,再将新的整型切片赋值给外层切片的第一个元素。
当操作完成后,会为新的整型切片分配新的底层数组,然后将切片复制到外层切片的索引为 0 的元素

即便是这么简单的多维切片,操作时也会涉及众多布局的值。
看起来在函数间像这样传递数据也会很复杂。不过切片本身结构很简单,可以以很小的成本在函数间传递

在函数间传递切片

在函数间传递切片就是要在函数间以值的方式传递切片。由于切片的尺寸很小,在函数间复制和传递切片成本也很低。
让我们创建一个大切片,并将这个切片以值的方式传递给函数 foo

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 分配包含 100 万个整型值的切片
slice := make([]int, 1e6)

// 将 slice 传递到函数 foo
slice = foo(slice)

// 函数 foo 接收一个整型切片,并返回这个切片
func foo(slice []int) []int {
    ...
    return slice
}

在 64 位架构的机器上,一个切片需要 24 字节的内存: 指针字段需要 8 字节,长度和容量字段分别需要 8 字节。
由于与切片关联的数据包含在底层数组里,不属于切片本身,所以将切片复制到任意函数的时候,对底层数组大小都不会有影响。
复制时只会复制切片本身,不会涉及底层数组。

在函数间传递 24 字节的数据会非常快速、简单。这也是切片效率高的地方。
不需要传递指针和处理复杂的语法,只需要复制切片,按想要的方式修改数据,然后传递回一份新的切片副本。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
package main

import "fmt"

func main() {
    arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8}
    s := arr[2:6]
    fmt.Println(s)
}
// [2 3 4 5]
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package main

import "fmt"

func updateSlice(s []int) {
    s[0] = 100
}

func main() {
    arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8}
    fmt.Println(arr[:6])
    fmt.Println(arr[2:])
    fmt.Println(arr[:])
    fmt.Println("After updateSlice")
    s1 := arr[2:]
    s2 := arr[:]
    updateSlice(s1)
    fmt.Println(s1)
    fmt.Println(arr)
    updateSlice(s2)
    fmt.Println(s2)
    fmt.Println(arr)
}
// [0 1 2 3 4 5]
// [2 3 4 5 6 7 8]
// [0 1 2 3 4 5 6 7 8]
// After updateSlice
// [100 3 4 5 6 7 8]
// [0 1 100 3 4 5 6 7 8]
// [100 1 100 3 4 5 6 7 8]
// [100 1 100 3 4 5 6 7 8]
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package main

import "fmt"

func updateSlice(s []int) {
    s[0] = 100
}

func main() {
    arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8}
    fmt.Println(arr[:6])
    fmt.Println(arr[2:])
    fmt.Println(arr[:])
    fmt.Println("After updateSlice")
    s1 := arr[2:]
    s2 := arr[:]
    updateSlice(s1)
    fmt.Println(s1)
    fmt.Println(arr)
    updateSlice(s2)
    fmt.Println(s2)
    fmt.Println(arr)
    fmt.Println("Reslice")
    s2 = s2[:5]
    fmt.Println(s2)
    s2 = s2[2:]
    fmt.Println(s2)
}
// [0 1 2 3 4 5]
// [2 3 4 5 6 7 8]
// [0 1 2 3 4 5 6 7 8]
// After updateSlice
// [100 3 4 5 6 7 8]
// [0 1 100 3 4 5 6 7 8]
// [100 1 100 3 4 5 6 7 8]
// [100 1 100 3 4 5 6 7 8]
// Reslice
// [100 1 100 3 4]
// [100 3 4]
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package main

import "fmt"

func main() {
    arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7}
    s1 := arr[2:6]
    s2 := s1[3:5]
    fmt.Println(s1, s2)
}
// [2 3 4 5] [5 6]

slice 可以向后扩展,不可以向前扩展
s[i]不可超越len(s),向后扩展不可超越底层数组cap(s)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package main

import "fmt"

func main() {
    arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7}
    s1 := arr[2:6]
    s2 := s1[3:5]
    fmt.Printf("s1 = %v, len(s1) = %d, cap(s1) = %d\n", s1, len(s1), cap(s1))
    fmt.Printf("s2 = %v, len(s2) = %d, cap(s2) = %d\n", s2, len(s2), cap(s2))
    fmt.Println(s1[3:6])
}
// s1 = [2 3 4 5], len(s1) = 4, cap(s1) = 6
// s2 = [5 6], len(s2) = 2, cap(s2) = 3
// [5 6 7]

切片操作

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package main

import "fmt"

func main() {
    arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7}
    s1 := arr[2:6]
    s2 := s1[3:5]
    s3 := append(s2, 10)
    fmt.Println(s3, cap(s3), s3[:cap(s3)])
    s4 := append(s3, 11)
    fmt.Println(s4, cap(s4), s4[:cap(s4)])
    s5 := append(s4, 12)
    fmt.Println(s5, cap(s5), s5[:cap(s5)])
    fmt.Println("arr = ", arr)
}
// [5 6 10] 3 [5 6 10]
// [5 6 10 11] 6 [5 6 10 11 0 0]
// [5 6 10 11 12] 6 [5 6 10 11 12 0]
// arr =  [0 1 2 3 4 5 6 10]

添加元素时如果超越 cap,系统会重新分配更大的底层数组

由于值传递的关系,必须接收 append 的返回值

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
package main

import "fmt"

func printSlice(s []int) {
    fmt.Printf("len = %d, cap = %d\n", len(s), cap(s))
}

func main() {
    var s []int // Zero value for slice is nil
    fmt.Println(s == nil)
    for i := 0; i < 100; i++ {
        printSlice(s)
        s = append(s, 2*i+1)
    }
    fmt.Println(s)
}
// true
// len = 0, cap = 0
// len = 1, cap = 1
// len = 2, cap = 2
// len = 3, cap = 4
// len = 4, cap = 4
// len = 5, cap = 8
// len = 6, cap = 8
// len = 7, cap = 8
// len = 8, cap = 8
// len = 9, cap = 16
// len = 10, cap = 16
// len = 11, cap = 16
// len = 12, cap = 16
// len = 13, cap = 16
// len = 14, cap = 16
// len = 15, cap = 16
// len = 16, cap = 16
// len = 17, cap = 32
// len = 18, cap = 32
// len = 19, cap = 32
// len = 20, cap = 32
// len = 21, cap = 32
// len = 22, cap = 32
// len = 23, cap = 32
// len = 24, cap = 32
// len = 25, cap = 32
// len = 26, cap = 32
// len = 27, cap = 32
// len = 28, cap = 32
// len = 29, cap = 32
// len = 30, cap = 32
// len = 31, cap = 32
// len = 32, cap = 32
// len = 33, cap = 64
// len = 34, cap = 64
// len = 35, cap = 64
// len = 36, cap = 64
// len = 37, cap = 64
// len = 38, cap = 64
// len = 39, cap = 64
// len = 40, cap = 64
// len = 41, cap = 64
// len = 42, cap = 64
// len = 43, cap = 64
// len = 44, cap = 64
// len = 45, cap = 64
// len = 46, cap = 64
// len = 47, cap = 64
// len = 48, cap = 64
// len = 49, cap = 64
// len = 50, cap = 64
// len = 51, cap = 64
// len = 52, cap = 64
// len = 53, cap = 64
// len = 54, cap = 64
// len = 55, cap = 64
// len = 56, cap = 64
// len = 57, cap = 64
// len = 58, cap = 64
// len = 59, cap = 64
// len = 60, cap = 64
// len = 61, cap = 64
// len = 62, cap = 64
// len = 63, cap = 64
// len = 64, cap = 64
// len = 65, cap = 128
// len = 66, cap = 128
// len = 67, cap = 128
// len = 68, cap = 128
// len = 69, cap = 128
// len = 70, cap = 128
// len = 71, cap = 128
// len = 72, cap = 128
// len = 73, cap = 128
// len = 74, cap = 128
// len = 75, cap = 128
// len = 76, cap = 128
// len = 77, cap = 128
// len = 78, cap = 128
// len = 79, cap = 128
// len = 80, cap = 128
// len = 81, cap = 128
// len = 82, cap = 128
// len = 83, cap = 128
// len = 84, cap = 128
// len = 85, cap = 128
// len = 86, cap = 128
// len = 87, cap = 128
// len = 88, cap = 128
// len = 89, cap = 128
// len = 90, cap = 128
// len = 91, cap = 128
// len = 92, cap = 128
// len = 93, cap = 128
// len = 94, cap = 128
// len = 95, cap = 128
// len = 96, cap = 128
// len = 97, cap = 128
// len = 98, cap = 128
// len = 99, cap = 128
// [1 3 5 7 9 11 13 15 17 19 21 23 25 27 29 31 33 35 37 39 41 43 45 47 49 51 53 55 57 59 61 63 65 67 69 71 73 75 77 79 81 83 85 87 89 91 93 95 97 99 101 103 105 107 109 111 113 115 117 119 121 123 125 127 129 131 133 135 137 139 141 143 145 147 149 151 153 155 157 159 161 163 165 167 169 171 173 175 177 179 181 183 185 187 189 191 193 195 197 199]
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package main

import "fmt"

func printSlice(s []int) {
    fmt.Printf("%v len = %d, cap = %d\n", s, len(s), cap(s))
}

func main() {
    s1 := []int{2, 4, 6, 8}
    printSlice(s1)

    s2 := make([]int, 16)
    s3 := make([]int, 10, 32)
    printSlice(s2)
    printSlice(s3)

    fmt.Println("Copying slice")
    copy(s2, s1)
    printSlice(s2)

    fmt.Println("Deleting elements from slice")
    s2 = append(s2[:3], s2[4:]...)
    printSlice(s2)

    fmt.Println("Popping from front")
    front := s2[0]
    s2 = s2[1:]
    fmt.Println(front)
    printSlice(s2)

    fmt.Println("Popping from back")
    tail := s2[len(s2)-1]
    s2 = s2[:len(s2)-1]
    fmt.Println(tail)
    printSlice(s2)
}
// [2 4 6 8] len = 4, cap = 4
// [0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0] len = 16, cap = 16
// [0 0 0 0 0 0 0 0 0 0] len = 10, cap = 32
// Copying slice
// [2 4 6 8 0 0 0 0 0 0 0 0 0 0 0 0] len = 16, cap = 16
// Deleting elements from slice
// [2 4 6 0 0 0 0 0 0 0 0 0 0 0 0] len = 15, cap = 16
// Popping from front
// 2
// [4 6 0 0 0 0 0 0 0 0 0 0 0 0] len = 14, cap = 15
// Popping from back
// 0
// [4 6 0 0 0 0 0 0 0 0 0 0 0] len = 13, cap = 15

reslice

将切片视作[cap]slice数据源,据此创建新切片对象。不能超出 cap,但不受 len 限制

新建切片对象依旧指向原底层数组,也就是说修改对所有关联切片可见

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
package main

import (
    "fmt"
)

func main() {
    d := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
    s1 := d[3:7]
    s2 := s1[1:3]

    for i := range s2 {
        s2[i] += 100
    }

    fmt.Println(d)
    fmt.Println(s1)
    fmt.Println(s2)
}

输出:

1
2
3
[0 1 2 3 104 105 6 7 8 9]
[3 104 105 6]
[104 105]

利用 reslice 操作,很容易就能实现一个栈式数据结构

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package main

import (
    "errors"
    "fmt"
)

func main() {
    // 栈最大容量 5
    stack := make([]int, 0, 5)

    // 入栈
    push := func(x int) error {
        n := len(stack)
        if n == cap(stack) {
            return errors.New("stack is full")
        }
        stack = stack[:n+1]
        stack[n] = x

        return nil
    }

    // 出栈
    pop := func() (int, error) {
        n := len(stack)
        if n == 0 {
            return 0, errors.New("stack is empty")
        }

        x := stack[n-1]
        stack = stack[:n-1]

        return x, nil
    }

    // 入栈测试
    for i := 0; i < 7; i++ {
        fmt.Printf("push %d: %v, %v\n", i, push(i), stack)
    }

    // 出栈测试
    for i := 0; i < 7; i++ {
        x, err := pop()
        fmt.Printf("pop: %d, %v, %v\n", x, err, stack)
    }
}

输出:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
push 0: <nil>, [0]
push 1: <nil>, [0 1]
push 2: <nil>, [0 1 2]
push 3: <nil>, [0 1 2 3]
push 4: <nil>, [0 1 2 3 4]
push 5: stack is full, [0 1 2 3 4]
push 6: stack is full, [0 1 2 3 4]
pop: 4, <nil>, [0 1 2 3]
pop: 3, <nil>, [0 1 2]
pop: 2, <nil>, [0 1]
pop: 1, <nil>, [0]
pop: 0, <nil>, []
pop: 0, stack is empty, []
pop: 0, stack is empty, []

append

向切片尾部添加数据,返回新的切片对象

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package main

import (
    "fmt"
)

func main() {
    s := make([]int, 0, 5)

    s1 := append(s, 10)
    s2 := append(s1, 20, 30) // 追加多个数据

    fmt.Println(s, len(s), cap(s)) // 不会修改原 slice 属性
    fmt.Println(s1, len(s1), cap(s1))
    fmt.Println(s2, len(s2), cap(s2))
}

输出:

1
2
3
[] 0 5
[10] 1 5
[10 20 30] 3 5

数据被追加到原底层数组。如超出 cap 限制,则为新切片对象重新分配数组

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package main

import (
    "fmt"
)

func main() {
    s := make([]int, 0, 100)
    s1 := s[:2:4]
    s2 := append(s1, 1, 2, 3, 4, 5, 6) // 超出 s1 cap 限制,分配新底层数组

    fmt.Printf("s1: %p: %v\n", &s1[0], s1)
    fmt.Printf("s2: %p: %v\n", &s2[0], s2)

    fmt.Printf("s data: %v\n", s[:10])
    fmt.Printf("s1 cap: %d, s2 cap: %d\n", cap(s1), cap(s2))
}

输出:

1
2
3
4
s1: 0xc0000b6000: [0 0]
s2: 0xc0000b8000: [0 0 1 2 3 4 5 6]  // 数组地址不同,确认新分配
s data: [0 0 0 0 0 0 0 0 0 0]  // append 并未向原数组写入部分数据
s1 cap: 4, s2 cap: 8  // 新数组是原 cap 的 2 倍

注意:

  • 是超出切片 cap 限制,而非底层数组长度限制,因为 cap 可小于数组长度
  • 新分配数组长度是原 cap 的 2 倍,而非原数组的 2 倍

并非总是 2 倍,对于较大的切片,会尝试扩容 1/4,以节约内存

向 nil 切片追加数据时,会为其分配底层数组内存

1
2
3
4
5
func main() {
    var s[]int
    s = append(s, 1, 2, 3)
    fmt.Println(s)
}

输出:

1
[1 2 3]

正因为存在重新分配底层数组的缘故,在某些场合建议预留足够多的空间,避免中途内存分配和数据复制开销

copy

在两个切片对象间复制数据,允许指向同一底层数组,允许目标区间重叠。
最终所复制长度以较短的切片长度为准

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package main

import (
    "fmt"
)

func main() {
    s := []int{0, 1, 2, 4, 4, 5, 6, 7, 8, 9}

    s1 := s[5:8]
    n := copy(s[4:], s1) // 在同一底层数组的不同区间复制
    fmt.Println(n, s)

    s2 := make([]int, 6) // 在不同数组间复制
    n = copy(s2, s)
    fmt.Println(n, s2)
}

输出:

1
2
3 [0 1 2 4 5 6 7 7 8 9]  // copy([4 5 6 7 8 9], [5, 6, 7])
6 [0 1 2 4 5 6]

还可直接从字符串中复制数据到[]byte

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package main

import (
    "fmt"
)

func main() {
    b := make([]byte, 3)
    n := copy(b, "abcde")
    fmt.Println(n, b)
}

输出:

1
3 [97 98 99]

如果切片长时间引用大数组中很小的片段,那么建议新建独立切片,复制出所需数据,以便原数组内存可被回收

切片与 nil

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package main

import (
    "fmt"
)

func main() {
    var l1 []int64
    fmt.Printf("%+v l1 == ni: %t, len(l1) = %d\n", l1, l1 == nil, len(l1))
    l2 := []int64{1, 2, 3}
    l3 := append(l2, l1...)
    l4 := append(l1, l2...)
    fmt.Printf("%+v\n", l3)
    fmt.Printf("%+v\n", l4)
}

输出:

1
2
[] l1 == ni: true, len(l1) = 0
[1 2 3]
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package main

import (
    "fmt"
)

func main() {
    var l1 []int64
    l1 = append(l1, 2)
    fmt.Printf("%+v\n", l1)
}

输出:

1
[2]

原理

我们先看看切片的结构定义,reflect.SliceHeader

1
2
3
4
5
type SliceHeader struct {
    Data uintptr
    Len  int
    Cap  int
}

可以看出切片的开头部分和Go字符串是一样的,但是切片多了一个Cap成员表示切片指向的内存空间的最大容量(对应元素的个数,而不是字节数)。
下图是 x := []int{2,3,5,7,11}y := x[1:3] 两个切片对应的内存结构。

切片布局

让我们看看切片有哪些定义方式:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
var (
    a []int               // nil切片, 和 nil 相等, 一般用来表示一个不存在的切片
    b = []int{}           // 空切片, 和 nil 不相等, 一般用来表示一个空的集合
    c = []int{1, 2, 3}    // 有3个元素的切片, len和cap都为3
    d = c[:2]             // 有2个元素的切片, len为2, cap为3
    e = c[0:2:cap(c)]     // 有2个元素的切片, len为2, cap为3
    f = c[:0]             // 有0个元素的切片, len为0, cap为3
    g = make([]int, 3)    // 有3个元素的切片, len和cap都为3
    h = make([]int, 2, 3) // 有2个元素的切片, len为2, cap为3
    i = make([]int, 0, 3) // 有0个元素的切片, len为0, cap为3
)

和数组一样,内置的len函数返回切片中有效元素的长度,内置的cap函数返回切片容量大小,容量必须大于或等于切片的长度。
也可以通过 reflect.SliceHeader 结构访问切片的信息(只是为了说明切片的结构,并不是推荐的做法)。
切片可以和nil进行比较,只有当切片底层数据指针为空时切片本身为nil,这时候切片的长度和容量信息将是无效的。
如果有切片的底层数据指针为空,但是长度和容量不为0的情况,那么说明切片本身已经被损坏了(比如直接通过 reflect.SliceHeaderunsafe 包对切片作了不正确的修改)。

遍历切片的方式和遍历数组的方式类似:

1
2
3
4
5
6
7
8
9
for i := range a {
    fmt.Printf("a[%d]: %d\n", i, a[i])
}
for i, v := range b {
    fmt.Printf("b[%d]: %d\n", i, v)
}
for i := 0; i < len(c); i++ {
    fmt.Printf("c[%d]: %d\n", i, c[i])
}

其实除了遍历之外,只要是切片的底层数据指针、长度和容量没有发生变化的话,对切片的遍历、元素的读取和修改都和数组是一样的。
在对切片本身赋值或参数传递时,和数组指针的操作方式类似,只是复制切片头信息(reflect.SliceHeader),并不会复制底层的数据。
对于类型,和数组的最大不同是,切片的类型和长度信息无关,只要是相同类型元素构成的切片均对应相同的切片类型。

如前所说,切片是一种简化版的动态数组,这是切片类型的灵魂。除了构造切片和遍历切片之外,添加切片元素、删除切片元素都是切片处理中经常遇到的问题。

添加切片元素

内置的泛型函数append可以在切片的尾部追加N个元素:

1
2
3
4
var a []int
a = append(a, 1)               // 追加1个元素
a = append(a, 1, 2, 3)         // 追加多个元素, 手写解包方式
a = append(a, []int{1,2,3}...) // 追加一个切片, 切片需要解包

不过要注意的是,在容量不足的情况下,append的操作会导致重新分配内存,可能导致巨大的内存分配和复制数据代价。
即使容量足够,依然需要用append函数的返回值来更新切片本身,因为新切片的长度已经发生了变化。

除了在切片的尾部追加,我们还可以在切片的开头添加元素:

1
2
3
var a = []int{1,2,3}
a = append([]int{0}, a...)        // 在开头添加1个元素
a = append([]int{-3,-2,-1}, a...) // 在开头添加1个切片

在开头一般都会导致内存的重新分配,而且会导致已有的元素全部复制1次。因此,从切片的开头添加元素的性能一般要比从尾部追加元素的性能差很多。

由于append函数返回新的切片,也就是它支持链式操作。我们可以将多个append操作组合起来,实现在切片中间插入元素:

1
2
3
var a []int
a = append(a[:i], append([]int{x}, a[i:]...)...)     // 在第i个位置插入x
a = append(a[:i], append([]int{1,2,3}, a[i:]...)...) // 在第i个位置插入切片

每个添加操作中的第二个append调用都会创建一个临时切片,并将 a[i:] 的内容复制到新创建的切片中,然后将临时创建的切片再追加到 a[:i]

可以用copy和append组合可以避免创建中间的临时切片,同样是完成添加元素的操作:

1
2
3
a = append(a, 0)     // 切片扩展1个空间
copy(a[i+1:], a[i:]) // a[i:]向后移动1个位置
a[i] = x             // 设置新添加的元素

第一句append用于扩展切片的长度,为要插入的元素留出空间。
第二句copy操作将要插入位置开始之后的元素向后挪动一个位置。第三句真实地将新添加的元素赋值到对应的位置。
操作语句虽然冗长了一点,但是相比前面的方法,可以减少中间创建的临时切片。

用copy和append组合也可以实现在中间位置插入多个元素(也就是插入一个切片):

1
2
3
a = append(a, x...)       // 为x切片扩展足够的空间
copy(a[i+len(x):], a[i:]) // a[i:]向后移动len(x)个位置
copy(a[i:], x)            // 复制新添加的切片

稍显不足的是,在第一句扩展切片容量的时候,扩展空间部分的元素复制是没有必要的。
没有专门的内置函数用于扩展切片的容量,append本质是用于追加元素而不是扩展容量,扩展切片容量只是append的一个副作用。

删除切片元素

根据要删除元素的位置有三种情况:从开头位置删除,从中间位置删除,从尾部删除。其中删除切片尾部的元素最快:

1
2
3
a = []int{1, 2, 3}
a = a[:len(a)-1]   // 删除尾部1个元素
a = a[:len(a)-N]   // 删除尾部N个元素

删除开头的元素可以直接移动数据指针:

1
2
3
a = []int{1, 2, 3}
a = a[1:] // 删除开头1个元素
a = a[N:] // 删除开头N个元素

删除开头的元素也可以不移动数据指针,但是将后面的数据向开头移动。
可以用append原地完成(所谓原地完成是指在原有的切片数据对应的内存区间内完成,不会导致内存空间结构的变化):

1
2
3
a = []int{1, 2, 3}
a = append(a[:0], a[1:]...) // 删除开头1个元素
a = append(a[:0], a[N:]...) // 删除开头N个元素

也可以用copy完成删除开头的元素:

1
2
3
a = []int{1, 2, 3}
a = a[:copy(a, a[1:])] // 删除开头1个元素
a = a[:copy(a, a[N:])] // 删除开头N个元素

对于删除中间的元素,需要对剩余的元素进行一次整体挪动,同样可以用append或copy原地完成:

1
2
3
4
5
6
7
a = []int{1, 2, 3, ...}

a = append(a[:i], a[i+1:]...) // 删除中间1个元素
a = append(a[:i], a[i+N:]...) // 删除中间N个元素

a = a[:i+copy(a[i:], a[i+1:])]  // 删除中间1个元素
a = a[:i+copy(a[i:], a[i+N:])]  // 删除中间N个元素

删除开头的元素和删除尾部的元素都可以认为是删除中间元素操作的特殊情况。

切片内存技巧

我们提到过有类似 [0]int 的空数组,空数组一般很少用到。但是对于切片来说,len为0但是cap容量不为0的切片则是非常有用的特性。
当然,如果len和cap都为0的话,则变成一个真正的空切片,虽然它并不是一个nil值的切片。
在判断一个切片是否为空时,一般通过len获取切片的长度来判断,一般很少将切片和nil值做直接的比较。

比如下面的 TrimSpace 函数用于删除 []byte 中的空格。函数实现利用了0长切片的特性,实现高效而且简洁。

1
2
3
4
5
6
7
8
9
func TrimSpace(s []byte) []byte {
    b := s[:0]
    for _, x := range s {
        if x != ' ' {
            b = append(b, x)
        }
    }
    return b
}

其实类似的根据过滤条件原地删除切片元素的算法都可以采用类似的方式处理(因为是删除操作不会出现内存不足的情形):

1
2
3
4
5
6
7
8
9
func Filter(s []byte, fn func(x byte) bool) []byte {
    b := s[:0]
    for _, x := range s {
        if !fn(x) {
            b = append(b, x)
        }
    }
    return b
}

切片高效操作的要点是要降低内存分配的次数,尽量保证append操作不会超出cap的容量,降低触发内存分配的次数和每次分配内存大小。

避免切片内存泄漏

如前面所说,切片操作并不会复制底层的数据。底层的数组会被保存在内存中,直到它不再被引用。
但是有时候可能会因为一个小的内存引用而导致底层整个数组处于被使用的状态,这会延迟自动内存回收器对底层数组的回收。

例如,FindPhoneNumber函数加载整个文件到内存,然后搜索第一个出现的电话号码,最后结果以切片方式返回。

1
2
3
4
func FindPhoneNumber(filename string) []byte {
    b, _ := ioutil.ReadFile(filename)
    return regexp.MustCompile("[0-9]+").Find(b)
}

这段代码返回的 []byte 指向保存整个文件的数组。因为切片引用了整个原始数组,导致自动垃圾回收器不能及时释放底层数组的空间。
一个小的需求可能导致需要长时间保存整个文件数据。这虽然这并不是传统意义上的内存泄漏,但是可能会拖慢系统的整体性能。

要修复这个问题,可以将感兴趣的数据复制到一个新的切片中(数据的传值是Go语言编程的一个哲学,虽然传值有一定的代价,但是换取的好处是切断了对原始数据的依赖):

1
2
3
4
5
func FindPhoneNumber(filename string) []byte {
    b, _ := ioutil.ReadFile(filename)
    b = regexp.MustCompile("[0-9]+").Find(b)
    return append([]byte{}, b...)
}

类似的问题,在删除切片元素时可能会遇到。假设切片里存放的是指针对象,那么下面删除末尾的元素后,被删除的元素依然被切片底层数组引用,从而导致不能及时被自动垃圾回收器回收(这要依赖回收器的实现方式):

1
2
var a []*int{ ... }
a = a[:len(a)-1]    // 被删除的最后一个元素依然被引用, 可能导致GC操作被阻碍

保险的方式是先将需要自动内存回收的元素设置为nil,保证自动回收器可以发现需要回收的对象,然后再进行切片的删除操作:

1
2
3
var a []*int{ ... }
a[len(a)-1] = nil // GC回收最后一个元素内存
a = a[:len(a)-1]  // 从切片删除最后一个元素

当然,如果切片存在的周期很短的话,可以不用刻意处理这个问题。因为如果切片本身已经可以被GC回收的话,切片对应的每个元素自然也就是可以被回收的了。

切片类型强制转换

为了安全,当两个切片类型 []T[] Y的底层原始切片类型不同时,Go语言是无法直接转换类型的。
不过安全都是有一定代价的,有时候这种转换是有它的价值的——可以简化编码或者是提升代码的性能。
比如在64位系统上,需要对一个 []float64 切片进行高速排序,我们可以将它强制转为 []int 整数切片,然后以整数的方式进行排序(因为float64遵循IEEE754浮点数标准特性,当浮点数有序时对应的整数也必然是有序的)。

下面的代码通过两种方法将 []float64 类型的切片转换为 []int 类型的切片:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// +build amd64 arm64

import "sort"

var a = []float64{4, 2, 5, 7, 2, 1, 88, 1}

func SortFloat64FastV1(a []float64) {
    // 强制类型转换
    var b []int = ((*[1 << 20]int)(unsafe.Pointer(&a[0])))[:len(a):cap(a)]

    // 以int方式给float64排序
    sort.Ints(b)
}

func SortFloat64FastV2(a []float64) {
    // 通过 reflect.SliceHeader 更新切片头部信息实现转换
    var c []int
    aHdr := (*reflect.SliceHeader)(unsafe.Pointer(&a))
    cHdr := (*reflect.SliceHeader)(unsafe.Pointer(&c))
    *cHdr = *aHdr

    // 以int方式给float64排序
    sort.Ints(c)
}

第一种强制转换是先将切片数据的开始地址转换为一个较大的数组的指针,然后对数组指针对应的数组重新做切片操作。
中间需要unsafe.Pointer来连接两个不同类型的指针传递。
需要注意的是,Go语言实现中非0大小数组的长度不得超过2GB,因此需要针对数组元素的类型大小计算数组的最大长度范围([]uint8 最大2GB,[]uint16 最大1GB,以此类推,但是 []struct{} 数组的长度可以超过2GB)。

第二种转换操作是分别取到两个不同类型的切片头信息指针,任何类型的切片头部信息底层都是对应 reflect.SliceHeader 结构,然后通过更新结构体方式来更新切片信息,从而实现a对应的 []float64 切片到c对应的 []int 类型切片的转换。

通过基准测试,我们可以发现用 sort.Ints 对转换后的 []int 排序的性能要比用 sort.Float64s 排序的性能好一点。
不过需要注意的是,这个方法可行的前提是要保证 []float64 中没有 NaNInf 等非规范的浮点数(因为浮点数中NaN不可排序,正0和负0相等,但是整数中没有这类情形)。

映射Map

映射是一种数据结构,用于存储一系列无序的键值对。
映射里基于键来存储值。映射功能强大的地方是,能够基于键快速检索数据。键就像索引一样,指向与该键关联的值。

字典(哈希表)是一种使用频率极高的数据结构。将其作为语言内置类型,从运行时层面进行优化,可获得更高效的性能

作为无序键值对集合,字典要求 key 必须是支持相等运算符(==、!=)的数据类型,比如,数字、字符串、指针、数组、结构体,以及对应接口类型

字典是引用类型,使用 make 函数或初始化表达语句来创建

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
    "fmt"
)

func main() {
    m := make(map[string]int)
    m["a"] = 1
    m["b"] = 2

    m2 := map[int]struct {
        x int
    }{
        1: {x: 100}, // 可省略 key、value 类型标签
        2: {x: 200},
    }

    fmt.Println(m, m2)
}

基本操作演示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package main

func main() {
    m := map[string]int{
        "a": 1,
        "b": 2,
    }

    m["a"] = 10 // 修改
    m["c"] = 30 // 新增

    if v, ok := m["d"]; ok { // 使用 ok-idiom 判断 key 是否存在,返回值
        println(v)
    }

    delete(m, "d") // 删除键值对。不存在时,不会出错
}

访问不存在的键值,默认返回零值,不会引发错误。
但推荐使用 ok-idiom 模式,毕竟通过零值无法判断键值是否存在,或许存储的 value 本就是零

对字典进行迭代,每次返回的键值次序都不相同.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package main

func main() {
    m := make(map[string]int)

    for i := 0; i < 8; i++ {
        m[string('a'+i)] = i
    }

    for i := 0; i < 4; i++ {
        for k, v := range m {
            print(k, ":", v, " ")
        }
    }
}

输出:

1
2
3
4
g:6 h:7 a:0 b:1 c:2 d:3 e:4 f:5 
d:3 e:4 f:5 g:6 h:7 a:0 b:1 c:2 
h:7 a:0 b:1 c:2 d:3 e:4 f:5 g:6 
b:1 c:2 d:3 e:4 f:5 g:6 h:7 a:0

函数 len 返回当前键值对数量,cap 不接受字典类型。
另外,因内存访问安全和哈希算法等缘故,字典被设计成“not addressable”,故不能直接修改 value 成员(结构或数组)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
func main() {
    type user struct {
        name string
        age byte
    }

    m := map[int]user{
        1: {"Tom", 19},
    }

    m[1].age += 1  // 错误: cannot assign to m[1].age
}

正确做法是返回整个 value,待修改后再设置字典键值,或直接用指针类型

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

type user struct {
    name string
    age  byte
}

func main() {
    m := map[int]user{
        1: {"Tom", 19},
    }

    u := m[1]
    u.age += 1
    m[1] = u // 设置整个 value

    m2 := map[int]*user{ // value 是指针类型
        1: &user{"Jack", 20},
    }

    m2[1].age++ // m2[1] 返回是指针,可透过指针修改目标对象
}

同理,m[key]++相当于m[key] = m[key] + 1,是合法操作

不能对 nil 字典进行写操作,但却能读

1
2
3
4
5
func main() {
    var m map[string]int
    println(m["a"])  // 返回零值
    m["a"] = 1  // panic: assignment to entry in nil map
}

注意: 内容为空的字典,与 nil 是不同的

1
2
3
4
5
6
func main() {
    var m1 map[string]int
    m2 := map[string]int{}  // 已初始化,等同 make 操作

    println(m1 == nil, m2 == nil)
}

输出:

1
true false

内部实现

映射是一个集合,可以使用类似处理数组和切片的方式迭代映射的元素。
但映射是无序的结合,意味着没有办法预测键值对被返回的顺序。
即便使用同样的顺序保存键值对,每次迭代映射的时候顺序也可能不一样。无序的原因是映射的实现使用了散列表。

创建和初始化

Go 语言中有很多种方法可以创建并初始化映射,可以使用内置的 make 函数,也可以使用映射字面量

1
2
3
4
5
6
// 创建一个映射,键的类型是 string,值的类型是 int
dict := make(map[string]int)

// 创建一个映射,键和值的类型都是 string
// 使用两个键值对初始化映射
dict := map[string]string{"Red": "#da1337", "Orange": "#e95a22"}

创建映射时,更常用的方法是使用映射字面量。
映射的初始长度会根据初始化时指定的键值对的数量来确定。

映射的键可以是任何值。这个值的类型可以是内置的类型,也可以是结构类型,只要这个值可以使用==运算符做比较。
切片、函数以及包含切片的结构类型这些类型由于具有引用语义,不能作为映射的键,使用这些类型会造成编译错误。

没有任何理由阻止用户使用切片作为映射的值。这个在使用一个映射键对应一组数据时,会非常有用。

1
2
// 创建一个映射,使用字符串切片作为值
dict := map[int][]string{}

使用映射

键值对赋值给映射,是通过指定适当类型的键并给这个键赋一个值来完成的

1
2
3
4
5
// 创建一个空映射,用来存储颜色以及颜色对应的十六进制代码
colors := map[string]string{}

// 将 Red 的代码加入到映射
colors["Red"] = "#da1337"

可以通过声明一个未初始化的映射来创建一个值为 nil 的映射(称为 nil 映射)。
nil 映射不能用于存储键值对,否则,会产生一个语言运行时错误

测试映射里是否存在某个键是映射的一个重要操作。
这个操作允许用户写一些逻辑来确定是否完成了某个操作或者是否映射里缓存了特定数据。
这个操作也可以用来比较两个映射,来确定哪些键值对互相匹配,哪些键值对不匹配。

从映射取值时有两个选择。第一个选择是,可以同时获得值,以及一个表示这个键是否存在的标志

1
2
3
4
5
6
7
// 获取键 Blue 对应的值
value, exists := colors["Blue"]

// 这个键存在吗?
if exists {
    fmt.Println(value)
}

另一个选择是,只返回键对应的值,然后通过判断这个值是不是零值来确定键是否存在

1
2
3
4
5
6
7
// 获取键 Blue 对应的值
value := colors["Blue"]

// 这个键存在吗?
if value != "" {
    fmt.Println(value)
}

在 Go 语言里,通过键来索引映射时,即便这个键不存在也总会返回一个值。在这种情况下,返回的是该值对应的类型的零值

迭代映射里的所有值和迭代数组或切片一样,使用关键字 range。
但对映射来说,range 返回的不是索引和值,而是键值对。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 创建一个映射,存储颜色以及颜色对应的十六进制代码
colors := map[string]string {
    "AliceBlue": "#f0f8ff",
    "Coral": "#ff7F50",
    "DarkGray": "#a9a9a9",
    "ForestGreen": "#228b22"
}

// 显示映射里的所有颜色
for key, value := range colors {
    fmt.Printf("Key: %s Value: %s\n", key, value)
}

如果想把一个键值对从映射里删除,就使用内置的 delete 函数

1
2
// 删除键为 Coral 的键值对
delete(colors, "Coral")

遍历

要想遍历map中全部的key/value对的话,可以使用range风格的for循环实现,和之前的slice遍历语法类似。
下面的迭代语句将在每次迭代时设置name和age变量,它们对应下一个键/值对:

1
2
3
for name, age := range ages {
    fmt.Printf("%s\t%d\n", name, age)
}

Map的迭代顺序是不确定的,并且不同的哈希函数实现可能导致不同的遍历顺序。
在实践中,遍历的顺序是随机的,每一次遍历的顺序都不相同。
这是故意的,每次都使用随机的遍历顺序可以强制要求程序不会依赖具体的哈希函数实现。
如果要按顺序遍历key/value对,我们必须显式地对key进行排序,可以使用sort包的Strings函数对字符串slice进行排序。下面是常见的处理方式:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import "sort"

var names []string
for name := range ages {
    names = append(names, name)
}
sort.Strings(names)
for _, name := range names {
    fmt.Printf("%s\t%d\n", name, ages[name])
}

因为我们一开始就知道names的最终大小,因此给slice分配一个合适的大小将会更有效。下面的代码创建了一个空的slice,但是slice的容量刚好可以放下map中全部的key:

1
names := make([]string, 0, len(ages))

在上面的第一个range循环中,我们只关心map中的key,所以我们忽略了第二个循环变量。
在第二个循环中,我们只关心names中的名字,所以我们使用“_”空白标识符来忽略第一个循环变量,也就是迭代slice时的索引。

map类型的零值是nil,也就是没有引用任何哈希表。

1
2
3
var ages map[string]int
fmt.Println(ages == nil)    // "true"
fmt.Println(len(ages) == 0) // "true"

map上的大部分操作,包括查找、删除、len和range循环都可以安全工作在nil值的map上,它们的行为和一个空的map类似。
但是向一个nil值的map存入元素将导致一个panic异常:

1
ages["carol"] = 21 // panic: assignment to entry in nil map

在向map存数据前必须先创建map。

和slice一样,map之间也不能进行相等比较;唯一的例外是和nil进行比较。要判断两个map是否包含相同的key和value,我们必须通过一个循环实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
func equal(x, y map[string]int) bool {
    if len(x) != len(y) {
        return false
    }
    for k, xv := range x {
        if yv, ok := y[k]; !ok || yv != xv {
            return false
        }
    }
    return true
}

要注意我们是如何用!ok来区分元素缺失和元素不同的。我们不能简单地用 xv != y[k] 判断,那样会导致在判断下面两个map时产生错误的结果:

1
2
// True if equal is written incorrectly.
equal(map[string]int{"A": 0}, map[string]int{"B": 42})

在函数间传递映射

在函数间传递映射并不会制造出该映射的一个副本。
实际上,当传递映射给一个函数,并对这个映射做了修改时,所有对这个映射的引用都会觉察到这个修改。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package main

import "fmt"

func main() {
    m := map[string]string{
        "name":   "hhh",
        "course": "go",
        "city":   "linfen",
    }
    m2 := make(map[string]int) // m2 == empty map
    var m3 map[string]int      // m3 == nil
    fmt.Println(m, m2, m3)
    fmt.Println("Travering map")
    for k, v := range m {
        fmt.Println(k, v)
    }
    fmt.Println("Getting values")
    name := m["name"]
    fmt.Println(name)
    lala := m["lala"]
    fmt.Println(lala)
    name2, ok := m["name"]
    fmt.Println(name2, ok)
    lala2, ok := m["lala"]
    fmt.Println(lala2, ok)

    if name, ok := m["name"]; ok {
        fmt.Println(name)
    } else {
        fmt.Println("key does not exist")
    }
    if lalala, ok := m["lalala"]; ok {
        fmt.Println(lalala)
    } else {
        fmt.Println("key does not exist")
    }

    fmt.Println("Deleting values")
    delete(m, "name")
    name3, ok := m["name"]
    fmt.Println(name3, ok)
}
// map[city:linfen course:go name:hhh] map[] map[]
// Travering map
// name hhh
// course go
// city linfen
// Getting values
// hhh

// hhh true
//  false
// hhh
// key does not exist
// Deleting values
//  false

map 不保证遍历顺序,如需顺序,需手动对 key 排序
使用 len 获取元素个数

map 使用哈希表,必须可以比较相等
除了 slice, map, function 的内建类型都可以作为 key

Struct类型不包括上述字段,也可作为 key

安全

在迭代期间删除或新增键值是安全的

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package main

import "fmt"

func main() {
    m := make(map[int]int)

    for i := 0; i < 10; i++ {
        m[i] = i + 10
    }

    for k := range m {
        if k == 5 {
            m[100] = 1000
        }

        delete(m, k)
        fmt.Println(k, m)
    }
}

就此例而言,不能保证迭代操作会删除新增的键值

运行时会对字典并发操作做出检测。
如果某个任务正在对字典进行写操作,那么其他任务就不能对该字典执行并发操作(读、写、删除),否则会导致进程崩溃。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main

import (
    "time"
)

func main() {
    m := make(map[string]int)

    go func() {
        for {
            m["a"] += 1 // 写操作
            time.Sleep(time.Microsecond)
        }
    }()

    go func() {
        for {
            _ = m["b"] // 读操作
            time.Sleep(time.Microsecond)
        }
    }()

    select {} // 阻止进程退出
}

输出:

1
fatal error: concurrent map read and map write

可用 sync.RWMutex 实现同步,避免读写操作同时进行

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package main

import (
    "sync"
    "time"
)

func main() {
    var lock sync.RWMutex // 使用读写锁,以获得最佳性能

    m := make(map[string]int)

    go func() {
        for {
            lock.Lock()   // 注意锁的粒度
            m["a"] += 1   // 写操作
            lock.Unlock() // 不能使用 defer
            time.Sleep(time.Microsecond)
        }
    }()

    go func() {
        for {
            lock.RLock()
            _ = m["b"] // 读操作
            lock.RUnlock()
            time.Sleep(time.Microsecond)
        }
    }()

    select {} // 阻止进程退出
}

性能

字典对象本身就是指针包装,传参时无序再次取地址

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import (
    "fmt"
    "unsafe"
)

func test(x map[string]int) {
    fmt.Printf("x: %p\n", x)
}

func main() {
    m := make(map[string]int)
    test(m)

    fmt.Printf("m: %p, %d\n", m, unsafe.Sizeof(m))

    m2 := map[string]int{}
    test(m2)
    fmt.Printf("m2: %p, %d\n", m2, unsafe.Sizeof(m2))
}

输出:

1
2
3
4
x: 0xc000098180
m: 0xc000098180, 8
x: 0xc0000981b0
m2: 0xc0000981b0, 8

在创建时预先准备足够空间有助于提升性能,减少扩张时的内存分配和重新哈希操作。

对于海量小对象,应直接用字典存储键值数据拷贝,而非指针。
这有助于减少需要扫描的对象数量,大幅缩短垃圾回收时间。
另外,字典不会收缩内存,所以适当替换成新对象是必要的

注意

map中的元素并不是一个变量,因此我们不能对map的元素进行取址操作:

1
_ = &ages["bob"] // compile error: cannot take address of map element

禁止对map元素取址的原因是map可能随着元素数量的增长而重新分配更大的内存空间,从而可能导致之前的地址无效。