Go 边看边练 -《Go 学习笔记》系列(八)

本贴最后更新于 3250 天前,其中的信息可能已经东海扬尘

上一篇: https://hacpai.com/article/1438260619759


ToC


4.1 Array

和以往认知的数组有很大不同。

  • 数组是值类型,赋值和传参会复制整个数组,而不是指针。
  • 数组长度必须是常量,且是类型的组成部分。[2]int[3]int 是不同类型。
  • 支持 "=="、"!=" 操作符,因为内存总是被初始化过的。
  • 指针数组 [n]*T,数组指针 *[n]T

可用复合语句初始化。

a := [3]int{1, 2} // 未初始化元素值为 0。
b := [...]int{1, 2, 3, 4} // 通过初始化值确定数组长度。
c := [5]int{2: 100, 4:200} // 使用索引号初始化元素。

d := [...]struct {
	name string
	age uint8
}{
	{"user1", 10}, // 可省略元素类型。
	{"user2", 20}, // 别忘了最后一行的逗号。
}

支持多维数组。

a := [2][3]int{{1, 2, 3}, {4, 5, 6}}
b := [...][2]int{{1, 1}, {2, 2}, {3, 3}} // 第 2 纬度不能用 "..."。

值拷贝行为会造成性能问题,通常会建议使用 slice,或数组指针。

内置函数 lencap 都返回数组长度 (元素数量)。

a := [2]int{}
println(len(a), cap(a)) // 2, 2

4.2 Slice

需要说明,slice 并不是数组或数组指针。它通过内部指针和相关属性引用数组片段,以实现变长方案。

runtime.h

struct Slice
{ // must not move anything
	byte* array; // actual data
	uintgo len; // number of elements
	uintgo cap; // allocated number of elements
};
  • 引用类型。但自身是结构体,值拷贝传递。

  • 属性 len 表示可用元素数量,读写操作不能超过该限制。

  • 属性 cap 表示最大扩张容量,不能超出数组限制。

  • 如果 slice == nil,那么 lencap 结果都等于 0

    data := [...]int{0, 1, 2, 3, 4, 5, 6}
    slice := data[1:4:5] // [low : high : max]

创建表达式使用的是元素索引号,而非数量。

读写操作实际目标是底层数组,只需注意索引号的差别。

data := [...]int{0, 1, 2, 3, 4, 5}

s := data[2:4]
s[0] += 100
s[1] += 200

fmt.Println(s)
fmt.Println(data)

输出:

[102 203]
[0 1 102 203 4 5]

可直接创建 slice 对象,自动分配底层数组。

s1 := []int{0, 1, 2, 3, 8: 100} // 通过初始化表达式构造,可使用索引号。
fmt.Println(s1, len(s1), cap(s1))

s2 := make([]int, 6, 8) // 使用 make 创建,指定 len 和 cap 值。
fmt.Println(s2, len(s2), cap(s2))

s3 := make([]int, 6) // 省略 cap,相当于 cap = len。
fmt.Println(s3, len(s3), cap(s3))

输出:

[0 1 2 3 0 0 0 0 100] 9 9
[0 0 0 0 0 0]              6 8
[0 0 0 0 0 0]              6 6

使用 make 动态创建 slice,避免了数组必须用常量做长度的麻烦。还可用指针直接访问底层数组,退化成普通数组操作。

s := []int{0, 1, 2, 3}

p := &s[2] // *int, 获取底层数组元素指针。
*p += 100

fmt.Println(s)

输出:

[0 1 102 3]

至于 [][]T,是指元素类型为 []T

data := [][]int{
	[]int{1, 2, 3},
	[]int{100, 200},
	[]int{11, 22, 33, 44},
}

可直接修改 struct array/slice 成员。

d := [5]struct {
	x int
}{}

s := d[:]

d[1].x = 10
s[2].x = 20

fmt.Println(d)
fmt.Printf("%p, %p\n", &d, &d[0])

输出:
[{0} {10} {20} {0} {0}]
0x20819c180, 0x20819c180

4.2.1 reslice

所谓 reslice,是基于已有 slice 创建新 slice 对象,以便在 cap 允许范围内调整属性。

s := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}

s1 := s[2:5] // [2 3 4]
s2 := s1[2:6:7] // [4 5 6 7]
s3 := s2[3:6] // Error

新对象依旧指向原底层数组。

s := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}

s1 := s[2:5] // [2 3 4]
s1[2] = 100

s2 := s1[2:6] // [100 5 6 7]
s2[3] = 200

fmt.Println(s)

输出:

[0 1 2 3 100 5 6 200 8 9]

4.2.2 append

slice 尾部添加数据,返回新的 slice 对象。

s := make([]int, 0, 5)
fmt.Printf("%p\n", &s)

s2 := append(s, 1)
fmt.Printf("%p\n", &s2)

fmt.Println(s, s2)

输出:

0x210230000
0x210230040
[] [1]

简单点说,就是在 array[slice.high] 写数据。

data := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}

s := data[:3]
s2 := append(s, 100, 200) // 添加多个值。

fmt.Println(data)
fmt.Println(s)
fmt.Println(s2)

输出:

[0 1 2 100 200 5 6 7 8 9]
[0 1 2]
[0 1 2 100 200]

一旦超出原 slice.cap 限制,就会重新分配底层数组,即便原数组并未填满。

data := [...]int{0, 1, 2, 3, 4, 10: 0}
s := data[:2:3]

s = append(s, 100, 200) // 一次 append 两个值,超出 s.cap 限制。

fmt.Println(s, data) // 重新分配底层数组,与原数组无关。
fmt.Println(&s[0], &data[0]) // 比对底层数组起始指针。

输出:

[0 1 100 200] [0 1 2 3 4 0 0 0 0 0 0]
0x20819c180 0x20817c0c0

从输出结果可以看出,append 后的 s 重新分配了底层数组,并复制数据。如果只追加一个值,则不会超过 s.cap 限制,也就不会重新分配。

通常以 2 倍容量重新分配底层数组。在大批量添加数据时,建议一次性分配足够大的空间,以减少内存分配和数据复制开销。或初始化足够长的 len 属性,改用索引号进行操作。及时释放不再使用的 slice 对象,避免持有过期数组,造成 GC 无法回收。

s := make([]int, 0, 1)
c := cap(s)

for i := 0; i < 50; i++ {
	s = append(s, i)
	if n := cap(s); n > c {
		fmt.Printf("cap: %d -> %d\n", c, n)
		c = n
	}
}

输出:

cap: 1 -> 2
cap: 2 -> 4
cap: 4 -> 8
cap: 8 -> 16
cap: 16 -> 32
cap: 32 -> 64

4.2.3 copy

函数 copy 在两个 slice 间复制数据,复制长度以 len 小的为准。两个 slice 可指向同一底层数组,允许元素区间重叠。

应及时将所需数据 copy 到较小的 slice,以便释放超大号底层数组内存。

下一篇: https://hacpai.com/article/1438596722873



社区小贴士

  • 关注标签 [golang] 可以方便查看 Go 相关帖子
  • 关注作者后如有新帖将会收到通知
打赏 50 积分后可见
50 积分
  • golang

    Go 语言是 Google 推出的一种全新的编程语言,可以在不损失应用程序性能的情况下降低代码的复杂性。谷歌首席软件工程师罗布派克(Rob Pike)说:我们之所以开发 Go,是因为过去 10 多年间软件开发的难度令人沮丧。Go 是谷歌 2009 发布的第二款编程语言。

    497 引用 • 1387 回帖 • 284 关注
  • 教程
    143 引用 • 602 回帖 • 8 关注
  • 雨痕
    14 引用 • 68 回帖 • 4 关注

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...
  • mysusheng

    s := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}

    s1 := s[2:5] // [2 3 4]
    s2 := s1[2:6:7] // [4 5 6 7]

    没明白 s2 为什么是[4 5 6 7] 在 s1 基础上也没有 567 啊?

    1 回复
  • 88250
    s := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
    s1 := s[:2]
    s2 := s1[5:10]
    
    fmt.Println(s, len(s), cap(s))    // [0 1 2 3 4 5 6 7 8 9] 10 10
    fmt.Println(s1, len(s1), cap(s1)) // [0 1] 2 10
    fmt.Println(s2, len(s2), cap(s2)) // [5 6 7 8 9] 5 5
    

    分片操作的时候是值拷贝,虽然通过索引操作会越界,但实际上 cap 以内的内存都已经被拷贝初始化,在 cap 以内做再分片是没问题的。

  • belite

    s := make([]int, 0, 5)
    fmt.Printf("%p\n", &s)
    s2 := append(s, 1)
    fmt.Printf("%p\n", &s2)
    fmt.Println(s, s2)

    这个测试没意义,slice 结构永远是不同的, 上面两个 slice 所指向的 数组地址是一样