11《Go语言入门》数组和切片

本文内容较多,请泡个茶,做好准备!

重要知识点:

  • Go中没有foreach关键字,但是range 关键字可用于for 循环中迭代数组(array)、切片(slice)、通道(channel)或集合(map)的元素。
  • _ 表示占位,用于替代不使用的变量。
  • 数据截取返回的是切片。
  • 切片是一个共享存储结构,是引用类型。
  • slice的len大于cap时,会出发扩容。slice的cap增长是当len在1024之前是双倍增长的,而1024以后则是先增长25%以后再调整这个值为系统需要的最小值,因此这个值是约等于cap + cap/4。

⚡️这是我纯手写的《Go语言入门》,源码+文章,手把手教你入门Go。看了你就会!
⚡️文章中所有的代码我都放到了 github.com/GanZhiXiong/go_learning 这个仓库中!
⚡️看文章时,对照仓库中代码学习效果更佳哦!

数组

数组的声明

  • 声明指定长度数组,并初始化为默认0值。

    1
    2
    var a [3]int
    a[0] = 1
  • 声明指定长度数组,同时初始化

    1
    2
    3
    4
    // 一维数组
    b := [3]int{1, 2, 3}
    // 二维数组
    c := [2][2]int{{1, 2}, {3, 4}}
  • 声明初始化数组时不写长度

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

遍历数组

上一篇我们学习知道了Go仅支持循环关键字for
那么遍历数组肯定是for。

for循环

1
2
3
for i := 0; i < len(a); i++ {
t.Log(a[i])
}

foreach循环

虽然Go中没有foreach关键字,但是也可以利用for关键字来实现类型foreach循环。

1
2
3
for index, e := range a {
t.Log(index, e)
}

需要注意是索引不能省略,但是可以“_”来表示占位,这样就能即使不使用index,也不会报错。

1
2
3
for _, e := range a {
t.Log(e)
}

下面我用代码来演示for循环和for实现的foreach循环:

11_array_test.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func TestArrayEach(t *testing.T) {
a := [3]int{1, 2, 3}
for i := 0; i < len(a); i++ {
t.Log(a[i])
}

// 那有没有类型Java或C#中的foreach: foreach(int i in a),也是有的
for index, e := range a {
t.Log(index, e)
}

// 那我们可不可以省略index?
for e := range a {
// 通过输出结果可以看到e是索引
t.Log(e)
}

// 好吧,那我把index写上,但是我不用它行不?这样肯定不行啊,会报错:Unused variable 'index'
// Go中可以“_”表示占位,不使用它也不会报错
for _, e := range a {
t.Log(e)
}
}

数组截取

这个很好理解,请直接代码:

11_array_test.go
1
2
3
4
5
6
7
8
9
10
11
func TestArraySection(t *testing.T) {
a := [...]int{1, 2, 3, 4, 5}
t.Log(a, reflect.TypeOf(a))

// 数组截取:a[开始索引(包含), 不包含(不包含)]
b := a[1:2]
t.Log(b, reflect.TypeOf(b)) // 2
t.Log(a[1:3]) // 2 3
t.Log(a[1:]) // 2 3 4 5
t.Log(a[:3]) // 1 2 3
}
1
2
3
4
5
6
7
=== RUN   TestArraySection
11_array_test.go:52: [1 2 3 4 5] [5]int
11_array_test.go:56: [2] []int
11_array_test.go:57: [2 3] []int
11_array_test.go:58: [2 3 4 5]
11_array_test.go:59: [1 2 3]
--- PASS: TestArraySection (0.00s)

这里我们需要注意是,数组截取后就变成了slice切片了。

切片

切片也是一个连续存储的数据结构。

切片内部结构

它本质上是一个结构体。
该结构体包括三个基本元素,分别是ptr、len、cap:

  • ptr指针:指向连续的存储空间,也就是一个数组。
  • len 存了多少个元素个数。
  • cap 即capacity,指可以存多少个元素。指针指向的后端这个数组空间长度,比如5个元素的数组,这个cap可以是5。

概念似乎很难理解,下面我用代码来讲解怎么创建切片以及len和cap的区别:

11_slice_test.go
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
func TestSliceInit(t *testing.T) {
// 声明
var s0 []int
t.Log(s0, len(s0), cap(s0))
s0 = append(s0, 1)
t.Log(s0, len(s0), cap(s0))
t.Log("\r")

// 声明并初始化
s1 := []int{1, 2, 3, 4}
t.Log(s1, len(s1), cap(s1))
s1 = append(s1, 5)
t.Log(s1, len(s1), cap(s1))
t.Log("\r")

// 使用make创建
// 如果len大于cap则会报错:larger than cap in make([]int)
//s2 := make([]int, 3, 0)
s2 := make([]int, 3, 3)
t.Log(s2, len(s2), cap(s2))
// panic: runtime error: index out of range [3] with length 3 [recovered]
// panic: runtime error: index out of range [3] with length 3
//t.Log(s2[0], s2[1], s2[2], s2[3], s2[4])
for i := 0; i < 10; i++ {
s2 = append(s2, i)
t.Log(s2, len(s2), cap(s2))
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
=== RUN   TestSliceInit
11_slice_test.go:8: [] 0 0
11_slice_test.go:10: [1] 1 1
11_slice_test.go:11:
11_slice_test.go:15: [1 2 3 4] 4 4
11_slice_test.go:17: [1 2 3 4 5] 5 8
11_slice_test.go:18:
11_slice_test.go:24: [0 0 0] 3 3
11_slice_test.go:30: [0 0 0 0] 4 6
11_slice_test.go:30: [0 0 0 0 1] 5 6
11_slice_test.go:30: [0 0 0 0 1 2] 6 6
11_slice_test.go:30: [0 0 0 0 1 2 3] 7 12
11_slice_test.go:30: [0 0 0 0 1 2 3 4] 8 12
11_slice_test.go:30: [0 0 0 0 1 2 3 4 5] 9 12
11_slice_test.go:30: [0 0 0 0 1 2 3 4 5 6] 10 12
11_slice_test.go:30: [0 0 0 0 1 2 3 4 5 6 7] 11 12
11_slice_test.go:30: [0 0 0 0 1 2 3 4 5 6 7 8] 12 12
11_slice_test.go:30: [0 0 0 0 1 2 3 4 5 6 7 8 9] 13 24
--- PASS: TestSliceInit (0.00s)

从上面的代码的输出结果可以得出:

  • len为0,capacity也为0;
  • 初始化后的切片,capacity等于len
  • capacity必须大于或等于len;
  • 如果len增长了,capacity不一定增长;
  • 如果存放不下了元素,capacity则会以2倍增长。

下面用代码再演示下:

11_slice_test.go
1
2
3
4
5
6
7
func TestSliceGrowing(t *testing.T) {
s := []int{}
for i := 0; i < 10; i++ {
s = append(s, i)
t.Log(s, len(s), cap(s))
}
}
1
2
3
4
5
6
7
8
9
10
11
12
=== RUN   TestSliceGrowing
11_slice_test.go:38: [0] 1 1
11_slice_test.go:38: [0 1] 2 2
11_slice_test.go:38: [0 1 2] 3 4
11_slice_test.go:38: [0 1 2 3] 4 4
11_slice_test.go:38: [0 1 2 3 4] 5 8
11_slice_test.go:38: [0 1 2 3 4 5] 6 8
11_slice_test.go:38: [0 1 2 3 4 5 6] 7 8
11_slice_test.go:38: [0 1 2 3 4 5 6 7] 8 8
11_slice_test.go:38: [0 1 2 3 4 5 6 7 8] 9 16
11_slice_test.go:38: [0 1 2 3 4 5 6 7 8 9] 10 16
--- PASS: TestSliceGrowing (0.00s)

不知你们有没有想过这样一个问题:
如果一直以2倍增长,那样内存消耗将会越来越大,这样的增长肯定是不合理的。
下面我将通过Go的源码来分析capacity增长规则。

capacity增长规则

11_slice_test.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
func TestSliceCapGrowthRules(t *testing.T) {
s := make([]int, 0)
n := 0
for n < 1500 {
s = append(s, n)

t.Log(color.White, len(s), cap(s))
if len(s)+1 > cap(s) {
t.Log(color.Red, "下一个append将扩容")
}

n++
}
}

下面的输出,capacity是2倍增长,符合上一节的总结。

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
=== RUN   TestSliceCapGrowthRules
11_slice_test.go:54: 1 1
11_slice_test.go:56: 下一个append将扩容
11_slice_test.go:54: 2 2
11_slice_test.go:56: 下一个append将扩容
11_slice_test.go:54: 3 4
11_slice_test.go:54: 4 4
11_slice_test.go:56: 下一个append将扩容
11_slice_test.go:54: 5 8
11_slice_test.go:54: 6 8
11_slice_test.go:54: 7 8
11_slice_test.go:54: 8 8
11_slice_test.go:56: 下一个append将扩容
11_slice_test.go:54: 9 16
11_slice_test.go:54: 10 16
11_slice_test.go:54: 11 16
11_slice_test.go:54: 12 16
11_slice_test.go:54: 13 16
11_slice_test.go:54: 14 16
11_slice_test.go:54: 15 16
11_slice_test.go:54: 16 16
11_slice_test.go:56: 下一个append将扩容
11_slice_test.go:54: 17 32
11_slice_test.go:54: 18 32
11_slice_test.go:54: 19 32
11_slice_test.go:54: 20 32
...

但是当len大于1024的时候,capacity却变成了1280,不是两倍增长了。

1
2
3
4
5
6
7
8
9
10
11
12
11_slice_test.go:54:  1020 1024
11_slice_test.go:54: 1021 1024
11_slice_test.go:54: 1022 1024
11_slice_test.go:54: 1023 1024
11_slice_test.go:54: 1024 1024
11_slice_test.go:56: 下一个append将扩容
11_slice_test.go:54: 1025 1280
11_slice_test.go:54: 1026 1280
11_slice_test.go:54: 1027 1280
11_slice_test.go:54: 1028 1280
11_slice_test.go:54: 1029 1280
11_slice_test.go:54: 1030 1280

看来得看下Go的源码了。
打开$GOROOT/src/runtime/slice.go,在growslice函数可以看到计算 newcap的算法。
算法分为两步:

  1. 预估容量
  2. 内存对齐

第一步,预估新容量

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
// src/runtime/slice.go
/*
参数分析:
old 是老切片
cap 是新切片容量的最小值(即旧切片的容量加上新加入元素的数量)
*/
func growslice(et *_type, old slice, cap int) slice {

// 1. 预估容量
newcap := old.cap
doublecap := newcap + newcap
// 首先判断,如果新切片容量的最小值(cap)大于2倍的旧容量(old.cap),则cap为新容量(newcap)。
if cap > doublecap {
newcap = cap
} else {
// 如果旧切片的长度小于1024,则最新容量就是旧容量的两倍,即 newcap=doublecap。
if old.len < 1024 {
newcap = doublecap
} else {
// 否则新容量以旧容量每次增长25%,直到新容量(newcap)大于或等于新切片容量的最小值。
// Check 0 < newcap to detect overflow
// and prevent an infinite loop.
for 0 < newcap && newcap < cap {
newcap += newcap / 4
}

// 如果新容量计算值溢出,则新容量为新切片容量的最小值。
// Set newcap to the requested cap when
// the newcap calculation overflowed.
if newcap <= 0 {
newcap = cap
}
}
}

// 2. 内存对齐
roundupsize...
}

image-20210708161121535

预估容量只是预估的元素“个数”。
那需要占用多少内存呢?

第二步,内存对齐

内存占用 = 预估容量 * 元素类型大小。

不过,由于 Go 语言的内存分配是由其 runtime 来管理的,程序并不是直接和操作系统打交道。
在程序启动时,runtime 会提前向操作系统申请一批内存,按照不同的规格管理起来,如下所示(重点看 bytes/obj 这列):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// src/runtime/sizeclasses.go
// Code generated by mksizeclasses.go; DO NOT EDIT.
//go:generate go run mksizeclasses.go

package runtime

// class bytes/obj bytes/span objects tail waste max waste
// 1 8 8192 1024 0 87.50%
// 2 16 8192 512 0 43.75%
// 3 32 8192 256 0 46.88%
// 4 48 8192 170 32 31.52%
// 5 64 8192 128 0 23.44%
// 6 80 8192 102 32 19.07%
// 7 96 8192 85 32 15.95%
// 8 112 8192 73 16 13.56%
// 9 128 8192 64 0 11.72%
// 10 144 8192 56 128 11.82%
// 11 160 8192 51 32 9.73%
// ......
// 23 448 8192 18 128 8.37%
// 24 480 8192 17 32 6.82%
// 25 512 8192 16 0 6.05%
// 26 576 8192 14 128 12.33%
// ......

当程序向 runtime 申请内存时,它会匹配足够大,且最接近的规格。

示例1:元素为int32类型的切片

11_slice_test.go
1
2
3
4
5
6
7
func TestSliceCapGrowthRules1(t *testing.T) {
s := []int32{1, 2}
t.Log(len(s), cap(s))

s = append(s, 3, 4, 5)
t.Log(len(s), cap(s))
}
11_slice_test.go
1
2
3
4
=== RUN   TestSliceCapGrowthRules1
11_slice_test.go:65: 2 2
11_slice_test.go:68: 5 8
--- PASS: TestSliceCapGrowthRules1 (0.00s)
  1. cap(2 + 3) > doublecap(2 * 2) ,所以预估新容量为5。
  2. int32 占用 4 byte,总内存占用为 5 * 4=20 byte,则 runtime 实际分配的内存为 32 byte,最终的容量为 32 / 4(每个 int 32 占用大小) = 8。

示例2:元素为int64类型的切片

如果将上面例子的 int32 改为 int64,得到的结果会是怎样的呢?

  1. cap(2 + 3) > doublecap(2 * 2),所以预估新容量为5。
  2. int64 占用 8 byte,总内存 5 * 8 = 40 byte,runtime 实际分配 48 byte,48 / 8 = 6。

示例3:append另一个切片

11_slice_test.go
1
2
3
4
5
6
7
8
9
10
func TestSliceCapGrowthRules3(t *testing.T) {
a := make([]int, 20)
t.Log(len(a), cap(a))

b := make([]int, 42)
t.Log(len(b), cap(b))

a = append(a, b...)
t.Log(len(a), cap(a))
}
  1. 前面z总结过“初始化后的切片,capacity等于len”,cap(20 + 42) > doublecap(2 * 20),因此预估新容量为62。
  2. 由于我是在64位的机器上运行的,因此int为int64,int64占8个字节,所以总内存为 62 * 8 = 496,在内存规格中会选择512,所以新容量为 512 / 8 = 64。
1
2
3
4
5
=== RUN   TestSliceCapGrowthRules3
11_slice_test.go:81: 20 20
11_slice_test.go:84: 42 42
11_slice_test.go:87: 62 64
--- PASS: TestSliceCapGrowthRules3 (0.00s)

示例4:元素为string的切片

11_slice_test.go
1
2
3
4
5
6
7
8
9
10
func TestSliceCapGrowthRules4(t *testing.T) {
var s string
t.Log(unsafe.Sizeof(s))

a := []string{"My", "name", "is"}
t.Log(len(a), cap(a))

a = append(a, "jason")
t.Log(len(a), cap(a))
}
  1. cap(3 + 1) < doublecap(2 * 3),因此预估新容量为6。
  2. 64位下string占16个字节,总内存为 6 * 16 = 96 byte,匹配到的内存规格为96字节,所以最终扩容后容量为 96 / 16 = 6。
1
2
3
4
5
=== RUN   TestSliceCapGrowthRules4
11_slice_test.go:102: 16
11_slice_test.go:105: 3 3
11_slice_test.go:108: 4 6
--- PASS: TestSliceCapGrowthRules4 (0.00s)

总结

graph TB
a[新切片len > 旧切片cap] --> |是|b(扩容)
a --> |否|b1(不扩容)
b --> c[预估新容量newcap]

c --> d{新切片容量最小值 > 2倍旧容量}
d --> |是| e[newcap = 新切片容量最小值]
d --> |否| e1{旧切片长度小于1024}
e1 --> |是| f[newcap = 2倍旧容量]
e1 --> |否| f1[新切片在旧切片容量上每次增长25%
直到newcap >= 新切片容量的最小值] e --> g[内存对齐] f --> g f1 --> g g --> h[预估内存占用 = 预估容量 * 元素类型大小] h --> i[匹配到runtime申请的内存规格对应的内存] i --> j[newcap = 匹配到的内存 / 元素类型大小] classDef mainStep fill:#02d7f2,color:#000 class c,g mainStep

切片共享存储结构

切片是一个共享存储结构
还是用代码说话:

11_slice_test.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func TestSliceShareMemory(t *testing.T) {
year := []string{"Ja", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}
t.Log(year, len(year), cap(year))

summer := year[4:7]
// 为什么summer的capacity是8呢?
// 虽然截取到索引6为止,但是summer是指向连续的存储空间year,也就是从year索引4开始到year最后一个元素的这一段连续空间
// 也就是从May到Dec,总共8个,capacity也就是8。
// 如果还觉得很绕,可以先这样记,后面我也会讲到,到时候进一步消化。
t.Log(summer, len(summer), cap(summer))// [May Jun Jul] 3 8

summer[0] = "Unknown"
t.Log(summer, len(summer), cap(summer))
t.Log(year, len(year), cap(year))
}
1
2
3
4
5
6
=== RUN   TestSliceShareMemory
11_slice_test.go:46: [Ja Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec] 12 12
11_slice_test.go:53: [May Jun Jul] 3 8
11_slice_test.go:56: [Unknown Jun Jul] 3 8
11_slice_test.go:57: [Ja Feb Mar Apr Unknown Jun Jul Aug Sep Oct Nov Dec] 12 12
--- PASS: TestSliceShareMemory (0.00s)

通过输出结果可以得知:切片是引用类型

数组和切片的区别

容量 数组之间比较 类型
数组 不可伸缩 可比较 值类型
切片 可伸缩 不可比较 引用类型

为了防止篇幅过长,这里只用代码演示下切片的比较,后面我会抽时间具体讲解下。

1
2
3
4
5
6
7
8
9
func TestSliceComparing(t *testing.T) {
a := []int{1, 2, 3}
b := []int{1, 2, 3}
// 语法报错:Invalid operation: a == b (operator == is not defined on []int)
// 编译时报错:invalid operation: a == b (slice can only be compared to nil)
//if a == b {
// t.Log("equal")
//}
}

参考

11《Go语言入门》数组和切片

https://ganzhixiong.com/p/53ed3754/

Author

干志雄

Posted on

2021-01-19

Updated on

2021-01-19

Licensed under

Comments