[TOC]

0X00 Go语言基础之数组

描述: 本文主要介绍Go语言中数组(array)及它的基本使用。

Q: 什么是Array(数组)?

答: 学过C语言的同学都知道,数组是同一种数据类型元素的集合。 在Go语言中,数组从声明时就确定,使用时可以修改数组成员,但是数组大小不可变化。

1.数组定义

基本语法:

1
2
3
4
var 数组变量名 [元素数量]T

// 定义一个长度为3元素类型为int的数组a
var a [3]int

比如:var a [5]int, 数组的长度必须是常量,并且长度是数组类型的一部分。一旦定义,长度不能变。注意: [5]int和[10]int是不同的类型。

1
2
3
var a [3]int
var b [4]int
a = b //不可以这样做,因为此时a和b是不同的类型

Tips :数组可以通过下标进行访问,下标是从0开始,最后一个元素下标是:len-1,访问越界(下标在合法范围之外),则触发访问越界,会panic。

2.数组初始化

数组的初始化也有很多方式。

1.方法一

初始化数组时可以使用初始化列表来设置数组元素的值。

1
2
3
4
5
6
7
8
func main() {
var testArray [3]int //数组会初始化为int类型的零值
var numArray = [3]int{1, 2} //使用指定的初始值完成初始化
var cityArray = [3]string{"北京", "上海", "深圳"} //使用指定的初始值完成初始化
fmt.Println(testArray) //[0 0 0]
fmt.Println(numArray) //[1 2 0]
fmt.Println(cityArray) //[北京 上海 深圳]
}

2.方法二

按照上面的方法每次都要确保提供的初始值和数组长度一致,一般情况下我们可以让编译器根据初始值的个数自行推断数组的长度,例如:

1
2
3
4
5
6
7
8
9
10
func main() {
var testArray [3]int
var numArray = [...]int{1, 2}
var cityArray = [...]string{"北京", "上海", "深圳"}
fmt.Println(testArray) //[0 0 0]
fmt.Println(numArray) //[1 2]
fmt.Printf("type of numArray:%T\n", numArray) //type of numArray:[2]int
fmt.Println(cityArray) //[北京 上海 深圳]
fmt.Printf("type of cityArray:%T\n", cityArray) //type of cityArray:[3]string
}

3.方法三(非常值得学习)

我们还可以使用指定索引值的方式来初始化数组,例如:

1
2
3
4
5
6
func main() {
a := [...]int{1: 1, 3: 5}
b := [...]int{1:100,9:200} // [0 100 0 0 0 0 0 0 200 ]
fmt.Println(a) // [0 1 0 5]
fmt.Printf("type of a:%T\n", a) // type of a:[4]int
}

3.数组的遍历

遍历数组a有以下两种方法:

1
2
3
4
5
6
7
8
9
10
11
12
func main() {
var a = [...]string{"北京", "上海", "深圳"}
// 方法1:for循环遍历
for i := 0; i < len(a); i++ {
fmt.Println(a[i])
}

// 方法2:for range遍历
for index, value := range a {
fmt.Println(index, value)
}
}

4.多维数组

Go语言是支持多维数组的,我们这里以二维数组为例(数组中又嵌套数组)。

二维数组的定义

1
2
3
4
5
6
7
8
9
func main() {
a := [3][2]string{
{"北京", "上海"},
{"广州", "深圳"},
{"成都", "重庆"},
}
fmt.Println(a) //[[北京 上海] [广州 深圳] [成都 重庆]]
fmt.Println(a[2][1]) //支持索引取值:重庆
}

注意: 多维数组只有第一层可以使用...来让编译器推导数组长度。例如:

1
2
3
4
5
6
7
8
9
10
11
12
//支持的写法
a := [...][2]string{
{"北京", "上海"},
{"广州", "深圳"},
{"成都", "重庆"},
}
//不支持多维数组的内层使用...
b := [3][...]string{
{"北京", "上海"},
{"广州", "深圳"},
{"成都", "重庆"},
}


二维数组的遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func main() {
a := [3][2]string{
{"北京", "上海"},
{"广州", "深圳"},
{"成都", "重庆"},
}

// 方式1. for range 方式
for _, v1 := range a {
for _, v2 := range v1 {
fmt.Printf("%s\t", v2)
}
fmt.Println()
}
}

输出:

1
2
3
北京	上海	
广州 深圳
成都 重庆


5.数组是值类型

描述: 数组是值类型,赋值和传参会复制整个数组。因此改变副本的值,不会改变本身的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// go 语言中默认传参是值传递(拷贝原变量的值即 Ctrl+c 、Ctrl+v )
func modifyArray(x [3]int) {
x[0] = 100
}

func modifyArray2(x [3][2]int) {
x[2][0] = 100
}
func main() {
a := [3]int{10, 20, 30}
modifyArray(a) //在modify中修改的是a的副本x,不会更改数组a的元素
fmt.Println(a) //[10 20 30]

b := [3][2]int{
{1, 1},
{1, 1},
{1, 1},
}
modifyArray2(b) //在modify中修改的是b的副本x,不会更改数组b的元素
fmt.Println(b) //[[1 1] [1 1] [1 1]]
}

注意:

  1. 数组支持 “==“、”!=” 操作符,因为内存总是被初始化过的。
  2. [n]*T表示指针数组,*[n]T表示数组指针 。

示例演示:

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
package main

import "fmt"

func main() {
// 定义一个长度为3元素类型为int的数组a
var a [2]int // 默认为0
var a1 [2]string // 默认为空
var a2 [2]bool // 默认为false
var a3 [2]float64 // 默认为0

fmt.Printf("a 数组类型 %T , 元素: %v\n", a, a)
fmt.Printf("a1 数组类型 %T , 元素: %v\n", a1, a1)
fmt.Printf("a2 数组类型 %T , 元素: %v\n", a2, a2)
fmt.Printf("a3 数组类型 %T , 元素: %v\n", a3, a3)

// 数组初始化
// 方式1.使用初始化列表来设置数组元素的值
var b = [3]int{1, 2} // 三个元素,未指定下标元素的其值为 0
var c = [3]string{"Let's", "Go", "语言"}
// 方式2.根据初始值的个数自行推断数组的长度
var d = [...]float32{1.0, 2.0}
e := [...]bool{true, false, false}
// 方式3.使用指定索引值的方式来初始化数组
var f = [...]int{1: 1, 3: 8} // 只有 下标为1的其值为1,下标为3的其值为8,初开之外都为0
g := [...]string{"Weiyi", "Geek"}

fmt.Printf("b 数组类型 %T , 元素: %v\n", b, b)
fmt.Printf("c 数组类型 %T , 元素: %v\n", c, c)
fmt.Printf("d 数组类型 %T , 元素: %v\n", d, d)
fmt.Printf("e 数组类型 %T , 元素: %v\n", e, e)
fmt.Printf("f 数组类型 %T , 元素: %v\n", f, f)
fmt.Printf("f 数组类型 %T , 元素: %v\n", g, g)

// 数组指定元素获取
fmt.Println("c[1] 元素获取 : ", c[1])
// 数组遍历
// 方式1
alen := len(c)
for i := 0; i < alen; i++ {
fmt.Printf("c[%d]: %s ", i, c[i])
}
fmt.Println()
// 方式2
for i, v := range c {
fmt.Printf("c[%d]: %s ", i, v) // 注意如果是切片类型需要强转为string
}
fmt.Println()

// 多维数组
// 方式1
s1 := [3][2]string{
{"北京", "上海"},
{"广州", "深圳"},
{"成都", "重庆"},
}

// 方式2
s2 := [...][2]string{
{"Go", "C"},
{"PHP", "Python"},
{"Shell", "Groovy"},
}
fmt.Println(s1[2][1]) //支持索引取值:重庆
fmt.Println(len(s1), s1) //[[北京 上海] [广州 深圳] [成都 重庆]]
fmt.Println(len(s2), s2)

// 多维数组遍历
// 方式1
s1len := len(s1)
for i := 0; i < s1len; i++ {
s1length := len(s1[i])
for j := 0; j < s1length; j++ {
fmt.Printf("s1[%d][%d] = %v ", i, j, s1[i][j])
}
}
fmt.Println()

// 方式2 (推荐方式)
for i, v1 := range s2 {
for j, v2 := range v1 {
fmt.Printf("s2[%d][%d] = %v ", i, j, v2)
}
}
fmt.Println()

// 多维数组元素更改
s1[1][0] = "Test"
s1[1][1] = "Change"
fmt.Println(s1)
}

执行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
a 数组类型 [2]int , 元素: [0 0]
a1 数组类型 [2]string , 元素: [ ]
a2 数组类型 [2]bool , 元素: [false false]
a3 数组类型 [2]float64 , 元素: [0 0]
b 数组类型 [3]int , 元素: [1 2 0]
c 数组类型 [3]string , 元素: [Let's Go 语言]
d 数组类型 [2]float32 , 元素: [1 2]
e 数组类型 [3]bool , 元素: [true false false]
f 数组类型 [4]int , 元素: [0 1 0 8]
f 数组类型 [2]string , 元素: [Weiyi Geek]
c[1] 元素获取 : Go
c[0]: Let's c[1]: Go c[2]: 语言
c[0]: Let's c[1]: Go c[2]: 语言
s1[2][1] 元素获取 : 重庆
3 [[北京 上海] [广州 深圳] [成都 重庆]]
3 [[Go C] [PHP Python] [Shell Groovy]]
s1[0][0] = 北京 s1[0][1] = 上海 s1[1][0] = 广州 s1[1][1] = 深圳 s1[2][0] = 成都 s1[2][1] = 重庆
s2[0][0] = Go s2[0][1] = C s2[1][0] = PHP s2[1][1] = Python s2[2][0] = Shell s2[2][1] = Groovy
[[北京 上海] [Test Change] [成都 重庆]]

0X01 Go语言基础之切片

描述: 本文主要介绍Go语言中切片(slice)及它的基本使用。

Q: 为什么要引入切片这个特性?
描述: 因为数组的长度是固定的并且数组长度属于类型的一部分,所以数组有很多的局限性。

例如:

1
2
3
4
5
6
7
func arraySum(x [3]int) int{
sum := 0
for _, v := range x{
sum = sum + v
}
return sum
}

这个求和函数只能接受[3]int类型,其他的都不支持。 再比如,

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

数组a中已经有三个元素了,我们不能再继续往数组a中添加新元素了, 所以为了解决上述问题我们引入了Python一样切片的编程语言特性。


1.切片定义

描述: 切片(Slice)是一个拥有相同类型元素的可变长度的序列。它是基于数组类型做的一层封装。

特点:

  • 切片它非常灵活,支持自动扩容。
  • 切片是一个引用类型,它的内部结构包含地址、长度和容量。切片一般用于快速地操作一块数据集合。

声明切片类型的基本语法如下:

1
2
3
4
5
var name []T

// 关键字解析
- name:表示变量名
- T:表示切片中的元素类型

Tips : 在定义时可看出与数组定义var array [number]T间的区别,其不需要设置元素个数。

举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func main() {
// 声明切片类型
var a []string //声明一个字符串切片
var b = []int{} //声明一个整型切片并初始化
var c = []bool{false, true} //声明一个布尔切片并初始化
var d = []bool{false, true} //声明一个布尔切片并初始化
fmt.Println(a) //[]
fmt.Println(b) //[]
fmt.Println(c) //[false true]
fmt.Println(a == nil) //true
fmt.Println(b == nil) //false
fmt.Println(c == nil) //false
// fmt.Println(c == d) //切片是引用类型,不支持直接比较,只能和nil比较
}


2.切片长度与容量

描述: 切片拥有自己的长度和容量,我们可以通过使用内置的len()函数求长度,使用内置的cap()函数求切片的容量。

1
2
3
4
5
// 切片长度与容量
var lth = []int{}
var lth64 = []float64{1, 2, 3}
fmt.Println("切片长度", len(lth), ",切片容量", cap(lth)) // 切片长度 0 ,切片容量 0
fmt.Println("切片长度", len(lth64), ",切片容量", cap(lth64)) // 切片长度 3 ,切片容量 3


3.切片表达式

描述: 切片表达式从字符串、数组、指向数组或切片的指针构造子字符串或切片。

它有两种变体:一种指定low和high两个索引界限值的简单的形式,另一种是除了low和high索引界限值外还指定容量的完整的形式。

简单切片表达式

描述: 切片的底层就是一个数组,所以我们可以基于数组通过切片表达式得到切片。 切片表达式中的low和high表示一个索引范围(左包含,右不包含),也就是下面代码中从数组a中选出1<=索引值<4的元素组成切片s,得到的切片长度=high-low,容量等于得到的切片的底层数组的容量。

1
2
3
4
5
func main() {
a := [5]int{1, 2, 3, 4, 5}
s := a[1:3] // s := a[low:high]
fmt.Printf("s:%v len(s):%v cap(s):%v\n", s, len(s), cap(s)) // 5 - 1
}

输出:

1
s:[2 3] len(s):2 cap(s):4

为了方便起见,可以省略切片表达式中的任何索引。省略了low则默认为0;省略了high则默认为切片操作数的长度:

1
2
3
a[2:]  // 等同于 a[2:len(a)]
a[:3] // 等同于 a[0:3]
a[:] // 等同于 a[0:len(a)]

注意:对于数组或字符串,如果0 <= low <= high <= len(a),则索引合法,否则就会索引越界(out of range)。

Tips : 对切片再执行切片表达式时(切片再切片),high的上限边界是切片的容量cap(a),而不是长度。

常量索引必须是非负的,并且可以用int类型的值表示;对于数组或常量字符串,常量索引也必须在有效范围内。如果low和high两个指标都是常数,它们必须满足low <= high。如果索引在运行时超出范围,就会发生运行时panic。

1
2
3
4
5
6
7
func main() {
a := [5]int{1, 2, 3, 4, 5}
s1 := a[1:3] // s1 := a[low:high]
fmt.Printf("s1:%v len(s1):%v cap(s1):%v\n", s1, len(s1), cap(s1))
s2 := s[3:4] // 索引的上限是cap(s)而不是len(s)
fmt.Printf("s2:%v len(s2):%v cap(s2):%v\n", s2, len(s2), cap(s2))
}

输出:

1
2
s:[2 3] len(s):2 cap(s):4
s2:[5] len(s2):1 cap(s2):1


完整切片表达式

描述: 对于数组,指向数组的指针,或切片a(注意不能是字符串)支持完整切片表达式:

1
a[low : high : max]

描述: 上面的代码会构造与简单切片表达式a[low: high]相同类型、相同长度和元素的切片。另外它会将得到的结果切片的容量设置为max-low。在完整切片表达式中只有第一个索引值(low)可以省,它默认为0。

1
2
3
4
5
func main() {
a := [5]int{1, 2, 3, 4, 5}
t := a[1:3:5]
fmt.Printf("t:%v len(t):%v cap(t):%v\n", t, len(t), cap(t))
}

输出结果:

1
t:[2 3] len(t):2 cap(t):4

Tips : 完整切片表达式需要满足的条件是0 <= low <= high <= max <= cap(a),其他条件和简单切片表达式相同。


4.切片遍历

描述: 切片的遍历方式和数组是一致的,支持索引遍历和for range遍历。

1
2
3
4
5
6
7
8
9
func main() {
s := []int{1, 3, 5}
for i := 0; i < len(s); i++ {
fmt.Println(i, s[i])
}
for index, value := range s {
fmt.Println(index, value)
}
}


5.切片的本质

描述: 切片的本质就是对底层数组的封装,它包含了三个信息:底层数组的指针、切片的长度(len)和切片的容量(cap)。

举个例子,现在有一个数组a := [8]int{0, 1, 2, 3, 4, 5, 6, 7},切片s1 := a[:5],相应示意图如下。

WeiyiGeek.slice_01

WeiyiGeek.slice_01

切片s2 := a[3:6],相应示意图如下:

WeiyiGeek.slice_02

Tips : 由上面两图可知切片的容量是数组长度 - 切片数组起始索引下标,例如 a[1:] = 8 - 1 其容量为7


6.make() 方法构造切片

描述: 我们上面都是基于数组来创建的切片,如果需要动态的创建一个切片,我们就需要使用内置的make()函数,格式如下:

格式说明:

1
2
3
4
5
6
make([]T, size, cap)

# 参数说明
- T:切片的元素类型
- size:切片中元素的数量
- cap:切片的容量

举个例子:

1
2
3
4
5
6
func main() {
a := make([]int, 2, 10)
fmt.Println(a) //[0 0]
fmt.Println(len(a)) //2
fmt.Println(cap(a)) //10
}

上面代码中a的内部存储空间已经分配了10个,但实际上只用了2个。 容量并不会影响当前元素的个数,所以len(a)返回2,cap(a)则返回该切片的容量。


7.append() 方法切片添加元素

描述: Go语言的内建函数append()可以为切片动态添加元素。 可以一次添加一个元素,可以添加多个元素,也可以添加另一个切片中的元素(后面加…)。

1
2
3
4
5
6
7
func main(){
var s []int
s = append(s, 1) // [1]
s = append(s, 2, 3, 4) // [1 2 3 4]
s2 := []int{5, 6, 7}
s = append(s, s2...) // [1 2 3 4 5 6 7]
}

注意: 通过var声明的零值切片可以在append()函数直接使用,无需初始化。

1
2
var s []int
s = append(s, 1, 2, 3)

注意: 没有必要像下面的代码一样初始化一个切片再传入append()函数使用,

1
2
3
4
5
s := []int{}  // 没有必要初始化
s = append(s, 1, 2, 3)

var s = make([]int) // 没有必要初始化
s = append(s, 1, 2, 3)

描述: 每个切片会指向一个底层数组,这个数组的容量够用就添加新增元素。当底层数组不能容纳新增的元素时,切片就会自动按照一定的策略进行“扩容”,此时该切片指向的底层数组就会更换。“扩容”操作往往发生在append()函数调用时,所以我们通常都需要用原变量接收append函数的返回值。

举个例子:

1
2
3
4
5
6
7
8
func main() {
//append()添加元素和切片扩容
var numSlice []int
for i := 0; i < 10; i++ {
numSlice = append(numSlice, i)
fmt.Printf("%v len:%d cap:%d ptr:%p\n", numSlice, len(numSlice), cap(numSlice), numSlice)
}
}

输出情况:

1
2
3
4
5
6
7
8
9
10
[0]  len:1  cap:1  ptr:0xc0000a8000
[0 1] len:2 cap:2 ptr:0xc0000a8040
[0 1 2] len:3 cap:4 ptr:0xc0000b2020
[0 1 2 3] len:4 cap:4 ptr:0xc0000b2020
[0 1 2 3 4] len:5 cap:8 ptr:0xc0000b6000
[0 1 2 3 4 5] len:6 cap:8 ptr:0xc0000b6000
[0 1 2 3 4 5 6] len:7 cap:8 ptr:0xc0000b6000
[0 1 2 3 4 5 6 7] len:8 cap:8 ptr:0xc0000b6000
[0 1 2 3 4 5 6 7 8] len:9 cap:16 ptr:0xc0000b8000
[0 1 2 3 4 5 6 7 8 9] len:10 cap:16 ptr:0xc0000b8000

从上面的结果可以看出:

  1. append()函数将元素追加到切片的最后并返回该切片。
  2. 切片numSlice的容量按照1,2,4,8,16这样的规则自动进行扩容,每次扩容后都是扩容前的2倍。


append() 函数还支持一次性追加多个元素,例如:

1
2
3
4
5
6
7
8
9
var citySlice []string
// 追加一个元素
citySlice = append(citySlice, "北京")
// 追加多个元素
citySlice = append(citySlice, "上海", "广州", "深圳")
// 追加切片
a := []string{"成都", "重庆"}
citySlice = append(citySlice, a...)
fmt.Println(citySlice) // [北京 上海 广州 深圳 成都 重庆]


8.copy()方法复制切片

描述: 首先我们来看一个问题

1
2
3
4
5
6
7
8
9
func main() {
a := []int{1, 2, 3, 4, 5}
b := a
fmt.Println(a) //[1 2 3 4 5]
fmt.Println(b) //[1 2 3 4 5]
b[0] = 1000
fmt.Println(a) //[1000 2 3 4 5]
fmt.Println(b) //[1000 2 3 4 5]
}

Tips : 由于切片是引用类型,所以a和b其实都指向了同一块内存地址。修改b的同时a的值也会发生变化。

Go语言内建的copy()函数可以迅速地将一个切片的数据复制到另外一个切片空间中,copy()函数的使用格式如下:

1
2
3
4
5
copy(destSlice, srcSlice []T)

# 参数:
- srcSlice: 数据来源切片
- destSlice: 目标切片


举个例子:

1
2
3
4
5
6
7
8
9
10
11
func main() {
// copy()复制切片
a := []int{1, 2, 3, 4, 5}
c := make([]int, 5, 5)
copy(c, a) //使用copy()函数将切片a中的元素复制到切片c
fmt.Println(a) //[1 2 3 4 5]
fmt.Println(c) //[1 2 3 4 5]
c[0] = 1000
fmt.Println(a) //[1 2 3 4 5]
fmt.Println(c) //[1000 2 3 4 5]
}


9.从切片中删除元素

描述: Go语言中并没有删除切片元素的专用方法,我们可以使用切片本身的特性来删除元素。 代码如下:

1
2
3
4
5
6
7
func main() {
// 从切片中删除元素
a := []int{30, 31, 32, 33, 34, 35, 36, 37}
// 要删除索引为2的元素
a = append(a[:2], a[3:]...)
fmt.Println(a) // [30 31 33 34 35 36 37]
}

总结一下就是:要从切片a中删除索引为index的元素,操作方法是a = append(a[:index], a[index+1:]...)


10.切片相关操作

判断切片是否为空

描述: 要检查切片是否为空,请始终使用len(s) == 0来判断,而不应该使用s == nil来判断。

1
2
3
4
5
d := [5]int{1, 2, 3, 4, 5}
// 判断切片是否为空
if len(d) != 0 {
fmt.Println("变量 d 切片不为空: ", d)
}


切片不能直接比较

描述: 切片之间是不能比较的,我们不能使用==操作符来判断两个切片是否含有全部相等元素。 切片唯一合法的比较操作是和nil比较。 一个nil值的切片并没有底层数组,一个nil值的切片的长度和容量都是0。

但是我们不能说一个长度和容量都是0的切片一定是nil,例如下面的示例:

1
2
3
var s1 []int         //len(s1)=0;cap(s1)=0;s1==nil
s2 := []int{} //len(s2)=0;cap(s2)=0;s2!=nil
s3 := make([]int, 0) //len(s3)=0;cap(s3)=0;s3!=nil

所以要判断一个切片是否是空的,要是用len(s) == 0来判断,不应该使用s == nil来判断。


切片的赋值拷贝

描述: 下面的代码中演示了拷贝前后两个变量共享底层数组,对一个切片的修改会影响另一个切片的内容,这点需要特别注意。

1
2
3
4
5
6
7
func main() {
s1 := make([]int, 3) //[0 0 0]
s2 := s1 //将s1直接赋值给s2,s1和s2共用一个底层数组
s2[0] = 100
fmt.Println(s1) //[100 0 0]
fmt.Println(s2) //[100 0 0]
}


切片的扩容策略

描述: 可以通过查看$GOROOT/src/runtime/slice.go源码,其中扩容相关代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
newcap = cap
} else {
if old.len < 1024 {
newcap = doublecap
} else {
// 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
}
}
}

从上面的代码可以看出以下内容:

  • 首先判断,如果新申请容量(cap)大于2倍的旧容量(old.cap),最终容量(newcap)就是新申请的容量(cap)。
  • 否则判断,如果旧切片的长度小于1024,则最终容量(newcap)就是旧容量(old.cap)的两倍,即(newcap=doublecap),
  • 否则判断,如果旧切片的长度大于等于1024,则最终容量(newcap)从旧容量(old.cap)开始循环增加原来的1/4,即(newcap=old.cap,for {newcap += newcap/4})直到最终容量(newcap)大于等于新申请的容量(cap),即(newcap >= cap)
  • 如果最终容量(cap)计算值溢出,则最终容量(cap)就是新申请容量(cap)。

Tips : 需要注意的是,切片扩容还会根据切片中元素的类型不同而做不同的处理,比如int和string类型的处理方式就不一样。

示例演示:

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
package main

import "fmt"

func main() {
// 切片声明与定义
var a []string //声明一个字符串切片
var b = []int{} //声明一个整型切片并初始化
var c = []bool{false, true} //声明一个布尔切片并初始化

// - 切片 a 变量值为空/零值。
if a == nil {
fmt.Println("a 切片元素:", a)
}
fmt.Println("b 切片元素:", b)
fmt.Println("c 切片元素:", c)

// 切片长度与容量
var lth = []int{}
var lth64 = []float64{1, 2, 3}
fmt.Println("切片长度", len(lth), ",切片容量", cap(lth))
fmt.Println("切片长度", len(lth64), ",切片容量", cap(lth64))

// 切片表达式
d := [5]int{1, 2, 3, 4, 5}
s := [5]string{"Let", "'s", "Go", "语言", "学习"}
s1 := d[1:3] // s := d[low(包含):high(不包含)] == d[1] d[2]
s2 := d[2:] // 等同于 a[2:5] == d[2] d[3] d[4]
s3 := d[:3] // 等同于 a[0:3] == d[0] d[1] d[2]
s4 := d[:] // 等同于 a[0:5] == d[0] d[1] d[2] d[3] d[4]
s5 := s[1:4:5] // 等同于 s[1:4] == s[1] s[2] s[3]

fmt.Printf("s1:%v len(s1):%v cap(s1):%v\n", s1, len(s1), cap(s1)) // 注意此种情况 { 2 .. 5 容量为 4 }
fmt.Printf("s2:%v len(s2):%v cap(s2):%v\n", s2, len(s2), cap(s2)) // { 3 .. 5 容量为 3 }
fmt.Printf("s3:%v len(s3):%v cap(s3):%v\n", s3, len(s3), cap(s3)) // 注意此种情况 { 1 .. 5 容量为 5 }
fmt.Printf("s4:%v len(s4):%v cap(s4):%v\n", s4, len(s4), cap(s4)) // { 1 .. 5 容量为 5}
fmt.Printf("s5:%v len(s5):%v cap(s5):%v\n", s5, len(s5), cap(s5)) // s5:['s Go 语言] len(s5):3 cap(s5):4

// 判断切片是否为空
if len(d) != 0 {
fmt.Println("变量 d 切片不为空: ", d)
}

// 切片遍历
for i, v := range s {
fmt.Printf("i: %d, v: %v , 切片指针地址: %p \n", i, v, &v)
}
fmt.Println()

// make() 构造切片
e := make([]int, 2, 10)
fmt.Printf("e:%v len(e):%d cap(e):%d \n", e, len(e), cap(e)) // 长度 2,容量为 10

// append() 添加元素 {7,8,9}
f := append(e, 7, 8, 9) // f:[0 0 7 8 9] len(f):5 cap(f):10
f = append(f, e...) // 追加切片
fmt.Printf("f:%v len(f):%d cap(f):%d \n", f, len(f), cap(f)) // 长度 7,容量为 10

// copy() 复制切片
slice1 := []int{1, 2, 3, 4, 5}
slice2 := make([]int, 7, 7)
copy(slice2, slice1)
slice2[6] = 2048
fmt.Println("slice1 : ", slice1, "\t slice2 :", slice2)

// 切片赋值拷贝
slice3 := make([]int, 3)
slice4 := slice3
slice4[0] = 1024
slice4[2] = 4096
fmt.Printf("slice3 : %v, ptr : %p \n", slice3, slice3)
fmt.Printf("slice4 : %v, ptr : %p \n", slice4, slice4)
}

执行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
a 切片元素: []
b 切片元素: []
c 切片元素: [false true]
切片长度 0 ,切片容量 0
切片长度 3 ,切片容量 3
s1:[2 3] len(s1):2 cap(s1):4
s2:[3 4 5] len(s2):3 cap(s2):3
s3:[1 2 3] len(s3):3 cap(s3):5
s4:[1 2 3 4 5] len(s4):5 cap(s4):5
s5:['s Go 语言] len(s5):3 cap(s5):4
变量 d 切片不为空: [1 2 3 4 5]
i: 0, v: Let , 切片指针地址: 0xc000010270
i: 1, v: 's , 切片指针地址: 0xc000010270
i: 2, v: Go , 切片指针地址: 0xc000010270
i: 3, v: 语言 , 切片指针地址: 0xc000010270
i: 4, v: 学习 , 切片指针地址: 0xc000010270

e:[0 0] len(e):2 cap(e):10
f:[0 0 7 8 9 0 0] len(f):7 cap(f):10
slice1 : [1 2 3 4 5] slice2 : [1 2 3 4 5 0 2048]
slice3 : [1024 0 4096], ptr : 0xc000018288
slice4 : [1024 0 4096], ptr : 0xc000018288

Tips 总结: 数组是值类型,且包含元素的类型和元素个数,需注意元素的个数(数组长度)属于数组类型的一部分。


0x02 Go语言基础之Map映射

描述: Go语言中提供的映射关系容器为map, 其内部使用散列表(hash)实现。

1.Map 声明定义

描述: Map 是一种无序的基于key-value的数据结构, 并且它是引用类型,所以必须初始化值周才能进行使用。

语法定义:

1
2
3
4
5
map[KeyType]ValueType

// # 参数说明:
- KeyType:表示键的类型。
- ValueType:表示键对应的值的类型。

Tips : map类型的变量默认初始值为nil,需要使用make()函数来分配内存。语法为:make(map[KeyType]ValueType, [cap]), 其中cap表示map的容量,该参数虽然不是必须的,但是我们应该在初始化map的时候就为其指定一个合适的容量。


2.Map 基础使用

描述:Map 中的数据都是成对出现的其Map的基本使用示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
// 1.采用Make初始化Map类型的变量。
scoreMap := make(map[string]int, 8)
scoreMap["小明"] = 100
fmt.Println(scoreMap["小明"])
fmt.Printf("type of a:%T\n", scoreMap)

// 2.在声明时填充元素。
userInfo := map[string]string{
"username": "WeiyiGeek",
"password": "123456",
}
fmt.Println(userInfo)


3.Map 键值遍历

描述: 在进行Map类型的变量遍历之前,我们先学习判断map中键是否存在。

(1) 键值判断
描述: 判断Map中某个键是否存在可以采用如下特殊写法: value, ok := map[key]

1
2
3
4
5
6
7
8
scoreMap := make(map[string]int)
scoreMap["小明"] = 100
value, ok := scoreMap["张三"]
if ok {
fmt.Println("scoreMap 存在该 '张三' 键")
} else {
fmt.Println("scoreMap 不存在该键值")
}


(2) 键值遍历
描述: Go 语言中不像Python语言一样有多种方式进行遍历, 大道至简就 for...range 遍历 Map 就可以搞定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
scoreMap := make(map[string]int)
scoreMap["Go"] = 90
scoreMap["Python"] = 100
scoreMap["C++"] = 60
// 遍历 k-v 写法
for k, v := range scoreMap {
fmt.Println(k, v)
}

// 遍历 k 写法
for k := range scoreMap {
fmt.Println(k)
}

// 遍历 v 写法
for _, v := range scoreMap {
fmt.Println(v)
}

Tips :遍历map时的元素顺序与添加键值对的顺序无关。


4.Map 键值删除

描述: 我们可使用 delete() 内建函数 从map中删除一组键值对, delete() 函数的格式如下: delete(map, key)

其中 map:表示要删除键值对的map, key: 表示要删除的键值对的键。

1
2
3
4
5
6
7
scoreMap := make(map[string]int)
scoreMap["张三"] = 90
scoreMap["小明"] = 100
delete(scoreMap, "小明" ) // 将`小明:100`从map中删除
for k,v := range scoreMap{
fmt.Println(k, v)
}


5.值为map类型的切片

描述: 第一次看到时可能比较绕,其实可以看做在切片中存放Map类型变量。

简单示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func demo3() {
var mapSlice = make([]map[string]string, 3)
for index, value := range mapSlice {
fmt.Printf("index:%d value:%v\n", index, value)
}
fmt.Println()
// 对切片中的map元素进行初始化
mapSlice[0] = make(map[string]string, 10)
mapSlice[1] = make(map[string]string, 10)
mapSlice[2] = make(map[string]string, 10)
mapSlice[0]["name"] = "WeiyiGeek"
mapSlice[0]["sex"] = "Man"
mapSlice[1]["姓名"] = "极客"
mapSlice[1]["性别"] = "男"
mapSlice[2]["hobby"] = "Computer"
mapSlice[2]["爱好"] = "电脑技术"
for i, v := range mapSlice {
//fmt.Printf("index:%d value:%v\n", i, v)
for _, value := range v {
fmt.Printf("index:%d value:%v\n", i, value)
}
}
}

执行结果:
1
2
3
4
5
6
7
8
9
10
index:0 value:map[]
index:1 value:map[]
index:2 value:map[]

index:0 value:Man
index:0 value:WeiyiGeek
index:1 value:极客
index:1 value:男
index:2 value:Computer
index:2 value:电脑技术


6.值为切片类型的map

描述: 同样在Map中存放切片类型的数据。

代码演示了map中值为切片类型的操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 值为切片类型的map
func demo4() {
var sliceMap = make(map[string][]string, 3)
var key = [2]string{"Country", "City"}
fmt.Println("初始化 sliceMap 其值 : ", sliceMap)

for _, v := range key {
// 判断键值是否存在如果不存在则初始化一个容量为2的切片
value, ok := sliceMap[v]
if !ok {
value = make([]string, 0, 2)
}
if v == "Country" {
value = append(value, "中国", "巴基斯坦")
} else {
value = append(value, "北京", "上海","台湾")
}
// 将切片值赋值给Map类型的变量
sliceMap[v] = value
}

执行结果:

1
2
初始化 sliceMap 其值 :  map[]
map[City:[北京 上海] Country:[中国 巴基斯坦]]

Tips : 非常重要、重要 Slice切片与Map 在使用时一定要做初始化操作(在内存空间申请地址)。


7.示例演示

1.Map类型的基础示例

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
func demo1() {
// 1.Map 定义
var a1 map[string]int8 // (未分配内存)
fmt.Println("Map 类型 的 a1 变量 :", a1)
if a1 == nil {
fmt.Println("默认初始化的Map类型的a1变量值: nil")
}

// 2.基本使用利用Make进行分配内存空间存储Map。
b1 := make(map[string]string, 8)
b1["姓名"] = "WeiyiGeek"
b1["性别"] = "男|man"
b1["爱好"] = "计算机技术"
b1["出生日期"] = "2021-08-08"
// 指定输出
fmt.Printf("b1['姓名'] = %v \n", b1["姓名"])
// 整体输出
fmt.Printf("Map b1 Type: %T , Map b1 Value: %v \n", b1, b1)

// 3.在声明时填充元素。
c1 := map[string]string{
"username": "WeiyiGeek",
"sex": "Man",
"hobby": "Computer",
}
// 指定输出
fmt.Printf("c1['username'] = %v \n", c1["username"])
// 整体输出
fmt.Printf("Map c1 Type: %T , Length : %d , Map c1 Value: %v \n", c1, len(c1), c1)

// 4.判断c1中的键值时候是否存在 sex Key.
value, ok := c1["sex"]
if ok {
fmt.Println("c1 Map 变量中存在 'sex' 键 = ", value)
} else {
fmt.Println("c1 Map 变量中不存在 sex 键")
}

// 5.遍历Map
for k, v := range b1 {
fmt.Println(k, "=", v)
}

// 6.删除指定键值对,例如删除c1中的hobby键值。
delete(c1, "hobby")
fmt.Printf("Map 现存在的键 : ")
for k := range c1 {
fmt.Print(k, " ")
}
}

执行结果:

1
2
3
4
5
6
7
8
9
10
11
12
Map 类型 的 a1 变量 : map[]
默认初始化的Map类型的a1变量值: nil
b1['姓名'] = WeiyiGeek
Map b1 Type: map[string]string , Map b1 Value: map[出生日期:2021-08-08 姓名:WeiyiGeek 性别:男|man 爱好:计算机技术]
c1['username'] = WeiyiGeek
Map c1 Type: map[string]string , Length : 3 , Map c1 Value: map[hobby:Computer sex:Man username:WeiyiGeek]
c1 Map 变量中存在 'sex' 键 = Man
出生日期 = 2021-08-08
姓名 = WeiyiGeek
性别 = 男|man
爱好 = 计算机技术
Map 现存在的键 : username sex


2.按照指定顺序遍历map

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
func demo2() {
rand.Seed(time.Now().UnixNano()) //初始化随机数种子

// 申请并初始化一个长度为 200 的 Map
var scoreMap = make(map[string]int, 200)
for i := 0; i < 20; i++ {
key := fmt.Sprintf("stu%02d", i) //生成stu开头的字符串
value := rand.Intn(100) //生成0~99的随机整数
scoreMap[key] = value
}

//取出map中的所有key存入切片keys
var keys = make([]string, 0, 200)
for key := range scoreMap {
keys = append(keys, key)
}

//对切片进行排序
sort.Strings(keys)

//按照排序后的key遍历map
for _, key := range keys {
fmt.Println(key, scoreMap[key])
}
}

执行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
stu00 4
stu01 48
stu02 66
stu03 18
stu04 13
stu05 89
stu06 80
stu07 16
stu08 11
stu09 26
stu10 42
stu11 45
stu12 24
stu13 47
stu14 92
stu15 77
stu16 12
stu17 16
stu18 17
stu19 76


Tips : 探究上述示例中Array 数组、Slice 切片、Map 映射有序与无序输出演示。

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
func demo5() {
// Array
var Arr = [...]int{1, 2, 6, 4, 5}
// Slice
var Sli = []int{1, 2, 6, 4, 5}
// Map
var Map = map[string]int{
"a1": 1,
"b2": 2,
"c3": 3,
"d6": 6,
"e5": 5,
}

fmt.Printf("Type : %T, Value : %v \n", Arr, Arr)
for _, A := range Arr {
fmt.Printf("%v ", A)
}
fmt.Println()
fmt.Printf("Type : %T, Value : %v \n", Sli, Sli)
for _, S := range Sli {
fmt.Printf("%v ", S)
}
fmt.Println()
fmt.Printf("Type : %T, Value : %v \n", Map, Map)
for _, M := range Map {
fmt.Printf("%v ", M)
}
}

执行结果:
1
2
3
4
5
6
Type : [5]int, Value : [1 2 6 4 5] 
1 2 6 4 5
Type : []int, Value : [1 2 6 4 5]
1 2 6 4 5
Type : map[string]int, Value : map[a1:1 b2:2 c3:3 d6:6 e5:5]
1 2 3 6 5