# 数组
数组是一段固定长度的连续内存区域。
在 Go 语言 中,数组一旦声明,那么大小就确定了。我们可以修改数组成员,但是不能改变大小。
# 定义数组
定义数组的格式如下:
var 数组变量名 [元素数量]数据类型
定义长度为3的整形数组:
package main
import "fmt"
func main() {
var arr [3]int
arr[0] = 1
arr[1] = 2
arr[2] = 3
fmt.Println(arr)
}
2
3
4
5
6
7
8
9
10
11
12
代码输出:
[1 2 3]
也可以定义数组的时候就初始化一组数据:
var arr = [3]int{1, 2, 3}
这种写法需要保证大括号内的元素数量与数组的大小一致。
也可以将定义数组大小的操作交给编译器,让编译器在编译时,根据元素的个数来确定大小:
var arr = [...]int{1, 2, 3}
...表示让编译器来确定数组大小。编译器会自动将这个数组的大小设置为 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)
}
}
2
3
4
5
6
7
8
9
10
11
代码输出:
index: 0, value: 1
index: 1, value: 2
index: 2, value: 3
2
3
# 切片(Slice)
# 什么是切片
切片和数组类似,都是数据集合。和数组不同的是,切片是一块动态分配大小的连续空间。它和 Java 语言中的 AarryList 集合类似。
# 声明切片
切片的声明格式如下:
var 变量名字 []数据类型
可以看出,切片声明方式跟数组差不多,只是少了[]中间的数字
示例:
package main
import "fmt"
func main() {
// 声明整型切片
var numList []int
// 声明字符串切片
var strList []string
// 声明一个空切片, {} 表示已经分配内存,但是切片里面的元素是空的
var numListEmpty = []int{}
// 输出3个切片
fmt.Println(numList, strList, numListEmpty)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
代码输出:
[] [] []
# 使用 make() 函数构造切片
除了上面那种方式,还可以使用make()创建切片:
make( []T, size, cap )
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))
}
2
3
4
5
6
7
8
9
10
11
代码输出如下:
[0 0 0 0] [0 0 0 0]
4 4
2
a 和 b 切片均为大小为 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])
}
}
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
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")
2
# 从数组或切片生成新的切片
从数组或切片生成新的切片是很常见的操作,格式如下:
slice [开始位置:结束位置]
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)的值
}
2
3
4
5
6
7
8
9
10
11
12
代码输出:
[1 2 3]
[2]
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[:])
}
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]
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])
}
2
3
4
5
6
7
8
9
10
11
12
13
14
代码输出:
[]
# 复制切片元素到另一个切片
Go 语言内置函数 copy() 可以将一个切片中的数据复制到另一个切片中,使用格式如下:
copy( destSlice, srcSlice []T) int
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)
}
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]
2
另外,我们还可以复制指定范围的数据:
// 将 srcSlice 分片中指定范围的数据复制到 destSlice 中
copy(destSlice, srcSlice[4:8])
fmt.Println(srcSlice)
fmt.Println(destSlice)
2
3
4
5
代码输出:
[0 1 2 3 4 5 6 7 8 9]
[4 5 6 7 0 0 0 0 0 0]
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)
}
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]
2
Go 语言中切片删除元素的本质即: 以被删除元素为分界点, 将前后两个部分的内存重新连接起来。
# 字典 (Map)
在 Go 语言 中提供的字典容器为 map。 map 使用散列表(hash)实现,跟JAVA的map类似。
定义字典 map 格式如下:
map [keyType]valueType
- keyType 表示键类型;
- valueType 表示键对应的值类型;
赋值跟数组基本一样,只是[]的类型不限制为数字:
m[key] = value
注意:键和键对应的值总是以一对一的形式存在。
# make()声明方式
make(map[keyType]valueType)
- 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"])
}
2
3
4
5
6
7
8
9
10
11
12
13
代码输出:
b
2
如果从 map 中获取一个并不存在的键(key), 此时会输出值类型的默认值,字符串为空字串,整型的默认值为 0 。
# map 的另外一种初始化方式
字典 map 还存在另外一种初始化方式,
//不初始化值
var 变量名 = map[keyType](valueType){}
//初始化值
var 变量名 = map[keyType](valueType){
key:value,
key:value,
key:value,
}
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){}
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)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
代码输出:
b
# 遍历字段 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)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
代码输出:
key: b, value: bb
key: a, value: aa
2
如果只需要遍历值,也可以通过 匿名变量来实现:
for _, value := range m {
fmt.Printf("value: %s\n", value)
}
2
3
只遍历键时,通过下面这种方式:
for key := range m {
fmt.Printf("key: %d\n", key)
}
2
3
注意: 字典 map 是一种无序的数据结构,不要期望输出时按照一定顺序输出。如果需要按顺序输出,请使用切片来完成。
# 删除字典 map 中键值对
通过内置函数 delete() 来删除键值对,格式如下:
delete(map, 键)
- 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)
}
}
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
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 {
}
}
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
错误提示:因为并发的对 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
})
}
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
2
3
4
注意:
sync.Map没有提供获取map元素数量的方法,你需要自行遍历计数。
# list (列表)
列表 (list) 是一种非连续存储的容器,又多个节点组成,节点通过一些变量将彼此串联起来。列表(list)底层常见的数据结构有: 单链表、双链表等
在Go 语言中,列表的实现都在 container/list 包中,内部实现原理是双链表。
列表(list)能够方便高效地进行元素的删除、插入操作。
# 初始化 list (列表)
list 的初始化方法有两种:New 和 声明。两者的效果是一样的。
# 通过 container/list 包中的 New 方法来初始化 list
格式如下:
变量名 := list.New()
# 通过声明初始化 list
格式如下:
var 变量名 = list.List{}
# 列表和 map (字典)有什么区别?
相比较 map (字典),列表没有具体元素类型的限制,也就是说,你可以添加任意类型到 list 容器中,如字符串 、整型等。这带来了一些便利,但是也存在一些问题:给一个列表添加了非期望类型的值后,在取值时,将 interface{} 转换为期望类型时会发生宕机。
# 向 list (列表) 中添加元素
双链表支持往队列前面或后面添加元素,对应的方法分别是:
PushFront往队列前面添加元素PushBack往队列后面添加元素
示例代码如下:
l := list.New()
l.PushFront("a")
l.PushBack("b")
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)
}
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
# 遍历 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)
}
}
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
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)
}
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
2
3
如果明确知道是第几个元素,你还可以
l.Front().Next().Next().Next()这种链式调用