Go基础-容器

2022/12/6 go

# 数组

数组是一段固定长度的连续内存区域。

Go 语言 中,数组一旦声明,那么大小就确定了。我们可以修改数组成员,但是不能改变大小。

# 定义数组

定义数组的格式如下:

var 数组变量名 [元素数量]数据类型
1

定义长度为3的整形数组:

package main

import "fmt"

func main() {
	var arr [3]int
	arr[0] = 1
	arr[1] = 2
	arr[2] = 3
	fmt.Println(arr)
}

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

代码输出:

[1 2 3]
1

也可以定义数组的时候就初始化一组数据:

var arr = [3]int{1, 2, 3}
1

这种写法需要保证大括号内的元素数量与数组的大小一致。

也可以将定义数组大小的操作交给编译器,让编译器在编译时,根据元素的个数来确定大小:

var arr = [...]int{1, 2, 3}
1

...表示让编译器来确定数组大小。编译器会自动将这个数组的大小设置为 3

# 遍历数组

通过 for range 来遍历数组

package main

import "fmt"

func main() {
	var arr = [3]int{1, 2, 3}
	for index, v := range arr {
		fmt.Printf("index: %d, value: %d\n", index, v)
	}
}

1
2
3
4
5
6
7
8
9
10
11

代码输出:

index: 0, value: 1
index: 1, value: 2
index: 2, value: 3
1
2
3

# 切片(Slice)

# 什么是切片

切片和数组类似,都是数据集合。和数组不同的是,切片是一块动态分配大小的连续空间。它和 Java 语言中的 AarryList 集合类似。

# 声明切片

切片的声明格式如下:

var 变量名字 []数据类型
1

可以看出,切片声明方式跟数组差不多,只是少了[]中间的数字

示例:

package main

import "fmt"

func main() {
	// 声明整型切片
	var numList []int

	// 声明字符串切片
	var strList []string

	// 声明一个空切片, {} 表示已经分配内存,但是切片里面的元素是空的
	var numListEmpty = []int{}

	// 输出3个切片
	fmt.Println(numList, strList, numListEmpty)
}

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

代码输出:

[] [] []
1

# 使用 make() 函数构造切片

除了上面那种方式,还可以使用make()创建切片:

make( []T, size, cap )
1
  • T : 切片中元素的类型;
  • size : 表示为这个类型分配多少个元素;
  • cap : 预分配的元素数量,该值设定后不影响 size, 表示提前分配的空间,设置它主要用于降低动态扩容时,造成的性能问题。

示例代码如下:

package main

import "fmt"

func main()  {
	a := make([]int, 2)
	b := make([]int, 2, 10)

	fmt.Println(a, b)
	fmt.Println(len(a), len(b))
}
1
2
3
4
5
6
7
8
9
10
11

代码输出如下:

[0 0 0 0] [0 0 0 0]
4 4
1
2

ab 切片均为大小为 2, 不同的是 b 内存空间预分配了 10 个,但是实际只使用了 2 个元素。

len() 函数计算的是元素的个数,与切片容量无关。

# 添加元素

使用内置函数 append() 可以为切片动态添加元素:

package main

import "fmt"

func main() {
	// 声明一个字符串类型的切片
	var strList []string

	// 循环动态向 strList 切片中添加 20 个元素,并打印相关参数
	for i := 0; i < 10; i++ {
		line := fmt.Sprintf("item %d", i)
		strList = append(strList, line)
		// 打印长度、预分配数量、指针地址、值
		fmt.Printf("len: %d, cap: %d, pointer: %p, content: %s\n", len(strList), cap(strList), strList, strList[i])
	}
}

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

代码输出:

len: 1, cap: 1, pointer: 0xc000050250, content: item 0
len: 2, cap: 2, pointer: 0xc0000523e0, content: item 1
len: 3, cap: 4, pointer: 0xc00005c040, content: item 2
len: 4, cap: 4, pointer: 0xc00005c040, content: item 3
len: 5, cap: 8, pointer: 0xc000094000, content: item 4
len: 6, cap: 8, pointer: 0xc000094000, content: item 5
len: 7, cap: 8, pointer: 0xc000094000, content: item 6
len: 8, cap: 8, pointer: 0xc000094000, content: item 7
len: 9, cap: 16, pointer: 0xc000096000, content: item 8
len: 10, cap: 16, pointer: 0xc000096000, content: item 9
1
2
3
4
5
6
7
8
9
10

通过上面的代码输出,会发现 len() 并不等于 cap。这是因为当切片空间不足以容纳足够多的元素时,切片会自动进行扩容操作, 扩容规律按切片容量的 2 倍进行扩容,如 1、2、4、8、16 ....

PS: 扩容一般发生在 append() 函数调用时。

另外,append() 函数除了添加一个元素外,还能一次性添加多个元素:

var strList []string
strList = append(strList, "a", "b", "3")
1
2

# 从数组或切片生成新的切片

从数组或切片生成新的切片是很常见的操作,格式如下:

slice [开始位置:结束位置]
1
  • slice 表示切片目标;
  • 开始位置和结束位置对应目标切片的下标。
  • 不包含结束位置

从数组中生成切片:

package main

import "fmt"

func main() {
	// 声明整形数组,值为1,2,3
	var arr = [3]int{1, 2, 3}

	fmt.Println(arr)
	fmt.Println(arr[1:2]) // 截取下标1到下标2(不包含下标2)的值
}

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

代码输出:

[1 2 3]
[2]
1
2

# 从指定范围中生成切片

package main

import "fmt"

func main()  {
	var arr = [20]int{}

	// 向数组中添加元素
	for i := 0; i < 20; i++ {
		arr[i] = i + 1
	}

	// 指定区间
	fmt.Println(arr[8:15])

	// 中间到尾部所有元素
	fmt.Println(arr[10:])

	// 开头到中间所有元素
	fmt.Println(arr[:10])

	// 切片本身
	fmt.Println(arr[:])
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

代码输出:

[9 10 11 12 13 14 15]
[11 12 13 14 15 16 17 18 19 20]
[1 2 3 4 5 6 7 8 9 10]
[1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20]
1
2
3
4
  • 若不填写结束位置,如 arr[10:], 则表示从下标 10 置到数组的结束位置。
  • 若不填写开始位置,如 arr[:10],则表示从 0 到下标 10 的位置。
  • 若开始位置和结束位置都不填写,如 arr[:], 则会生成一个和原有切片一样的切片。

# 重置切片

若把切片的开始位置和结束位置都设置为 0, 则会生成一个空的切片:

package main

import "fmt"

func main()  {
	var arr = [20]int{}

	// 向数组中添加元素
	for i := 0; i < 20; i++ {
		arr[i] = i + 1
	}

	fmt.Println(arr[0:0])
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

代码输出:

[]
1

# 复制切片元素到另一个切片

Go 语言内置函数 copy() 可以将一个切片中的数据复制到另一个切片中,使用格式如下:

copy( destSlice, srcSlice []T) int
1
  • srcSlice 代表源切片;
  • destSlice 代表目标切片。注意,目标切片必须有足够的空间来装载源切片的元素个数。返回值为整型,表示实际发生复制的元素个数。
package main

import "fmt"

func main()  {
	// 设置元素数量为 10
	const count = 10

	// 源分片
	srcSlice := make([]int, count)

	// 给源分片赋值
	for i := 0; i < count; i++ {
		srcSlice[i] = i
	}

	// 目标分片
	destSlice := make([]int, count)

	// 将 srcSlice 分片的数据复制到 destSlice 中
	copy(destSlice, srcSlice)

	fmt.Println(srcSlice)
	fmt.Println(destSlice)
}
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

代码输出:

[0 1 2 3 4 5 6 7 8 9]
[0 1 2 3 4 5 6 7 8 9]
1
2

另外,我们还可以复制指定范围的数据:

// 将 srcSlice 分片中指定范围的数据复制到 destSlice 中
copy(destSlice, srcSlice[4:8])

fmt.Println(srcSlice)
fmt.Println(destSlice)
1
2
3
4
5

代码输出:

[0 1 2 3 4 5 6 7 8 9]
[4 5 6 7 0 0 0 0 0 0]
1
2

# 从切片中删除元素

Go 语言中并没有提供特定的函数来删除切片中元素,但是可以利用切片的特性来达到目的:

package main

import "fmt"

func main() {
	// 声明一个字符串类型的切片
	arr := []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j"}

	// 指定删除位置,也就是 b 元素
	index := 1

	// 打印删除位置之前和之后的元素, arr[:index] 表示的是被删除元素的前面部分数据,arr[index+1:] 表示的是被删除元素后面的数据
	fmt.Println(arr[:index], arr[index+1:])

	// 将删除点前后的元素拼接起来
	arr = append(arr[:index], arr[index+1:]...)

	fmt.Println(arr)
}

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

代码输出如下:

[q] [a n x i a o h a]
[q a n x i a o h a]
1
2

Go 语言中切片删除元素的本质即: 以被删除元素为分界点, 将前后两个部分的内存重新连接起来。

# 字典 (Map)

在 Go 语言 中提供的字典容器为 mapmap 使用散列表(hash)实现,跟JAVAmap类似。

定义字典 map 格式如下:

map [keyType]valueType
1
  • keyType 表示键类型;
  • valueType 表示键对应的值类型;

赋值跟数组基本一样,只是[]的类型不限制为数字

m[key] = value
1

注意:键和键对应的值总是以一对一的形式存在。

# make()声明方式

make(map[keyType]valueType)
1
  • keyType 表示键类型;
  • valueType 表示键对应的值类型;
package main

import "fmt"

func main() {
	// 定义一个键类型和值类型为字符串的 map
	m := make(map[string]string)
	//向 map 中添加一个键为 “a”,值为 b 的映射关系
	m["a"] = "b"
	fmt.Println(m["a"])
	// 如果不存在的key会输出该类型的默认值,字符串为空字串,整型为0
	fmt.Println(m["b"])
}
1
2
3
4
5
6
7
8
9
10
11
12
13

代码输出:

b

1
2

如果从 map 中获取一个并不存在的键(key), 此时会输出值类型的默认值,字符串为空字串,整型的默认值为 0 。

# map 的另外一种初始化方式

字典 map 还存在另外一种初始化方式,

//不初始化值
var 变量名 = map[keyType](valueType){}
//初始化值
var 变量名 = map[keyType](valueType){
    key:value,
    key:value,
    key:value,
}
1
2
3
4
5
6
7
8
  • keyType 表示键类型;
  • valueType 表示键对应的值类型;
  • key 表示键值对的键
  • value 表示键值对的值

代码如下:

//声明一个map并赋初始值
m := map[int](string){
		1: "a",
		2: "b",
		3: "c",
}

// 声明一个空的map
m := map[int](string){}
1
2
3
4
5
6
7
8
9

上面的这段代码并没有使用 make(), 而是通过大括号的方式来初始化字典 map, 有点像 JSON 格式一样,冒号左边的是键(key) , 右边的是值(value) ,键值对之间使用逗号分隔。

# 判断key是否存在

当我们需要明确知道 map 中是否存在某个键(key)时,可以使用下面这种写法:

package main

import "fmt"

func main() {
	// 定义一个键类型和值类型为字符串的 map
	m := make(map[string]string)
	//向 map 中添加一个键为 “a”,值为 b 的映射关系
	m["a"] = "b"
	// 声明一个 flag 变量,用来接收对应键是否存在于 map 中
	val, flag := m["a"]
	//存在,输出值
	if flag {
		fmt.Printf(val)
	}

}

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

代码输出:

b
1

# 遍历字段 map

map 的遍历需要使用 for range 循环,代码如下:

package main

import "fmt"

func main() {
	// 定义一个键类型和值类型为字符串的 map
	m := make(map[string]string)
	//向 map 中添加元素
	m["a"] = "aa"
	m["b"] = "bb"
	// 通过 for range 遍历, 获取 key, val 值并打印
	for key, val := range m {
		fmt.Printf("key: %s, value: %s \n", key, val)
	}

}

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

代码输出:

key: b, value: bb
key: a, value: aa
1
2

如果只需要遍历值,也可以通过 匿名变量来实现:

for _, value := range m {
		fmt.Printf("value: %s\n", value)
}
1
2
3

只遍历键时,通过下面这种方式:

for key := range m {
		fmt.Printf("key: %d\n", key)
}
1
2
3

注意: 字典 map 是一种无序的数据结构,不要期望输出时按照一定顺序输出。如果需要按顺序输出,请使用切片来完成。

# 删除字典 map 中键值对

通过内置函数 delete() 来删除键值对,格式如下:

delete(map,)
1
  • map 表示要删除的目标 map 对象;
  • 键表示要删除的 map 中 key 键。

示例代码如下:

package main

import "fmt"

func main() {
	// 定义一个键类型和值类型为字符串的 map
	m := make(map[string]string)
	//向 map 中添加元素
	m["a"] = "aa"
	m["b"] = "bb"
	m["c"] = "cc"
	// 删除 键为b的键值对
	delete(m, "b")
	// 通过 for range 遍历, 获取 key, val 值并打印
	for key, val := range m {
		fmt.Printf("key: %s, value: %s \n", key, val)
	}

}

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

代码输出如下:

key: c, value: cc
key: a, value: aa
1
2

# sync.Map

map 在并发环境下,只读是线程安全的,同时读写是线程不安全的

下面这段代码演示了并发环境下读写 map 会出现的问题,代码如下:

package main

func main()  {
	// 初始化一个键为整型,值也为整型的 map
	m := make(map[int]int)

	// 开启一段并发代码
	go func() {
		// 无限循环往 map 里写值
		for {
			m[1] = 1
		}
	}()

	// 开启一段并发代码
	go func() {
		// 无限循环读取 map 数据
		for {
			_ = m[1]
		}
	}()

	// 死循环,让上面的并发代码在后台执行
	for {
	}
}
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

运行上面的代码,会报错如下:

fatal error: concurrent map read and map write
1

错误提示:因为并发的对 map 进行读写。两个并发函数不断的对 map 进行读写发生了竞态问题。map 内部会对这种并发操作进行检查并提前发现。

正常情况下,针对并发读写的场景,是需要加锁处理的。但是加锁就意味了性能不高。Go 语言在 1.9 版本中提供了一种高效率的并发安全的 sync.Map

sync.Map 有以下特性:

  • 无需初始化,直接声明即可;
  • sync.Map不能使用 map 的方式进行取值、设置等操作,而是使用 sync.Map提供的方法进行调用,如:Store 表示存储,Load 表示获取,Delete 表示删除。
  • 针对遍历操作,需要使用 Range 配合一个回调函数,回调函数会返回内部遍历出来的值。Range 参数中的回调函数返回值功能是: 需要继续遍历时,返回 true;终止遍历时,返回 false
package main

import (
	"fmt"
	"sync"
)

func main() {
	// 定义一个键类型和值类型为字符串的 map
	m := sync.Map{}
	m.Store("a", "aa")
	m.Store("b", "bb")
	m.Store("c", "cc")
	m.Store("d", "dd")
	// 从 sync.Map 中获取键为 2 的值
	fmt.Println(m.Load("b"))

	// 删除键值对
	m.Delete("c")
	// 遍历 sync.Map 中的键值对
	m.Range(func(key, value interface{}) bool {
		fmt.Printf("key: %s, value: %s\n", key, value)
		return true
	})

}

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

代码输出:

bb true
key: a, value: aa
key: b, value: bb
key: d, value: dd
1
2
3
4

注意: sync.Map 没有提供获取 map 元素数量的方法,你需要自行遍历计数。

# list (列表)

列表 (list) 是一种非连续存储的容器,又多个节点组成,节点通过一些变量将彼此串联起来。列表(list)底层常见的数据结构有: 单链表、双链表等

在Go 语言中,列表的实现都在 container/list 包中,内部实现原理是双链表。

列表(list)能够方便高效地进行元素的删除、插入操作。

# 初始化 list (列表)

list 的初始化方法有两种:New 和 声明。两者的效果是一样的。

# 通过 container/list 包中的 New 方法来初始化 list

格式如下:

变量名 := list.New()
1

# 通过声明初始化 list

格式如下:

var 变量名 = list.List{}
1

# 列表和 map (字典)有什么区别?

相比较 map (字典),列表没有具体元素类型的限制,也就是说,你可以添加任意类型到 list 容器中,如字符串整型等。这带来了一些便利,但是也存在一些问题:给一个列表添加了非期望类型的值后,在取值时,将 interface{} 转换为期望类型时会发生宕机。

# 向 list (列表) 中添加元素

双链表支持往队列前面或后面添加元素,对应的方法分别是:

  • PushFront 往队列前面添加元素
  • PushBack 往队列后面添加元素

示例代码如下:

l := list.New()

l.PushFront("a")
l.PushBack("b")
1
2
3
4

关于 list (列表) 插入元素的方法,如下表所示:

方法 参数说明 功能
InsertAfter(v interface{}, mark *Element) *Element v interface{} : 插入的值,mark *Element : 当前元素节点 在 mark 点后面插入元素
InsertBefore(v interface{}, mark *Element) *Element v interface{} : 插入的值,mark *Element : 当前元素节点 在 mark 点前面插入元素
PushFrontList(other *List) other *List: 列表(list) 添加 other 列表中的元素到头部
PushBackList(other *List) other *List: 列表(list) other *List: 列表(list)添加 other 列表中的元素到尾部

mark *Element 在操作每个节点后都会返回,比如 var element = list.PushFront("a")

# 从 list (列表) 中删除元素

list (列表) 的插入函数的返回值是一个 *list.Element 结构,通过它来完成对列表元素的删除:

package main

import (
	"container/list"
)

func main()  {
	l := list.New()

	// 头部添加字符串
	l.PushFront("犬小哈教程")
	
	// 尾部添加字符串
	l.PushBack("www.quanxiaoha.com")

	// 尾部添加一个整型,并保持元素句柄
	element := l.PushBack(1)
	
	// 在 1 之后添加字符串 2
	l.InsertAfter("2", element)

	// 在 1 之前添加字符串 0
	l.InsertBefore("0", element)
	
	// 删除 element 对应的元素
	l.Remove(element)
}
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

最终队列中保存的元素有:

a b 0 2
1

# 遍历 list (列表)

遍历 list (列表) 需要搭配 Front() 函数获取头元素,遍历过程中,只要元素不为空则可继续调用 Next 函数往下遍历:

package main

import (
	"container/list"
	"fmt"
)

func main() {
	l := list.New()

	// 头部添加字符串
	l.PushFront("a")

	// 尾部添加字符串
	l.PushBack("b")

	// 尾部添加一个整型,并保持元素句柄
	element := l.PushBack(1)

	// 在 1 之后添加字符串 2
	l.InsertAfter("2", element)

	// 在 1 之前添加字符串 0
	l.InsertBefore("0", element)

	// 删除 element 对应的元素
	l.Remove(element)

	// 添加 l2 列表中的元素到头部
	l2 := list.New()
	l2.PushBack("aa")
	l2.PushBack("bb")
	l.PushFrontList(l2)

	// 添加 l3 列表中的元素到尾部
	l3 := list.New()
	l3.PushBack("ee")
	l3.PushBack("ff")
	l.PushBackList(l3)

	//遍历
	for i := l.Front(); i != nil; i = i.Next() {
		fmt.Println(i.Value)
	}
}

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

注意,在 for 语句遍历中:

  • 其中 i := l.Front() 表示初始赋值,用来获取列表的头部下标;
  • 然后每次会循环会判断 i != nil,若等于空,则会退出循环,否则执行 i.Next()继续循环下一个元素;

代码输出:

aa
bb
a
b
0
2
ee
ff
1
2
3
4
5
6
7
8

也可以一个个取:

package main

import (
	"container/list"
	"fmt"
)

func main() {
	l := list.New()
	l.PushBack("a")
	l.PushBack("b")
	l.PushBack("c")
	//获取list头部
	front := l.Front()
	fmt.Println(front.Value)
	//获取list下一个元素
	next := front.Next()
	fmt.Println(next.Value)
	//获取list下一个元素
	next = next.Next()
	fmt.Println(next.Value)

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

代码输出:

a
b
c
1
2
3

如果明确知道是第几个元素,你还可以 l.Front().Next().Next().Next()这种链式调用