Go基础—2.容器

TOC

一、Go数组

数组的声明

var 数组变量名 [元素数量]Type

数组的简单操作

创建数组

var a [3]int = [3]int{1, 2, 3}  // 初始化一个三个整数的数组

打印数组中的元素

fmt.Println(a[0])        // 打印第一个元素
fmt.Println(a[len(a)-1]) // 打印最后一个元素
// 打印数组中的所有索引和元素
for i, v := range a {
    fmt.Printf("index: %d, value: %d\n", i, v)
}
// 只打印数组中的所有元素
for _, v := range a {
    fmt.Printf("%d\n", v)
}

数组的赋值

var fruit [3]string
fruit[0] = "apple"
fruit[1] = "watermelon"
fruit[2] = "strawberry"

for index, name := range fruit {
    fmt.Println(index, name)
}
/* 运行结果如下
0 apple
1 watermelon
2 strawberry
*/

数组需要注意的特性:
默认情况下,数组的每个元素都会被初始化为元素类型对应的零值,对于数字类型来说就是 0。例子如下所示:

var b [3]int = [3]int{1, 2}
fmt.Println(b[2])
/* 运行结果如下
0
*/

在数组的定义中,如果在数组长度的位置出现“…”省略号,则表示数组的长度是根据初始化值的个数来计算,所以我们初始化数组时可以写法如下:

c := [...]int{1, 2, 3}

数组的长度不同的情况下定义为两个类型不同的数组类型,比如[3]int和[4]int,比如我定义一个长度为3的数组,然后不能重新定义这个数组长度为4,例子如下所示:

d := [3]int{1, 2, 3}
d = [4]int{1, 2, 3, 4}  // 报错编译错误:无法将 [4]int 赋给 [3]int

数组的比较

数组之间比较必须数组类型一致,哪怕数组数量不一样也不能比较,例子如下所示:

a := [2]int{1, 2}
b := [3]int{1, 2, 3}
c := [...]int{1, 2}
d := [3]int{1, 2}
fmt.Println(a == b, a == c, b == c)
/* 运行结果如下
true false false
*/
fmt.Println(b == d) // 编译错误,因为数组类型不一致,不能进行比较

数组切片

切片就是截取数组中的一段元素。
创建一个数组,然后再进行切片操作,例子如下所示:

var list_a [30]int

for i := 0; i < 30; i++ {
	list_a[i] = i + 1
}
// 表示原有的切片
fmt.Println("原有的切片:", list_a[:])
// 清空所有拥有的元素
fmt.Println("清空后的切片:", list_a[0:0])
// 开头到中间的所有元素
fmt.Println("开头到中间的所有元素:", list_a[:5])
// 获取中间段元素
fmt.Println("获取中间段元素:", list_a[10:15])
// 中间到结尾的所有元素
fmt.Println("中间到结尾的所有元素:", list_a[20:])
// 输出切片的大小
fmt.Println("list_a切片大小为:", len(list_a))
/* 运行结果如下
原有的切片: [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]
清空后的切片: []
开头到中间的所有元素: [1 2 3 4 5]
获取中间段元素: [11 12 13 14 15]
中间到结尾的所有元素: [21 22 23 24 25 26 27 28 29 30]
list_a切片大小为: 30
*/

添加元素

append()方法可以为切片动态添加元素。

在切片的结尾添加元素

,具体代码如下所示:

var a []int
a = append(a, 1) // 追加1个元素
a = append(a, 1, 2, 3) // 追加多个元素, 手写解包方式
a = append(a, []int{1,2,3}...) // 追加一个切片, 切片需要解包

注意:在使用append()函数动态添加元素时,空间不足以容纳足够多的元素,切片就会进行“扩容”,此时新切片的长度会发生改变。

在切片的开头添加元素

var a = []int{1,2,3}
a = append([]int{0}, a...) // 在开头添加1个元素
a = append([]int{-3,-2,-1}, a...) // 在开头添加1个切片

注意:在切片开头添加元素一般都会导致内存的重新分配,从切片的开头添加元素的性能要比从尾部追加元素的性能差很多。

添加元素后的容量测试

在切片进行扩容时,容量是按照容量的2倍进行扩充,比如:1、2、4、8、16…。
以下我们来打印出扩展后的容量来看下其中规律,代码如下所示:

var numbers []int

for i := 0; i < 10; i++ {
    numbers = append(numbers, i)
    fmt.Printf("id: %d  len: %d  cap: %d pointer: %p\n", i, len(numbers), cap(numbers), numbers)
}
/* 输出结果如下
id: 0 len: 1 cap: 1 pointer: 0x14000104020
id: 1 len: 2 cap: 2 pointer: 0x14000104040
id: 2 len: 3 cap: 4 pointer: 0x1400013a020
id: 3 len: 4 cap: 4 pointer: 0x1400013a020
id: 4 len: 5 cap: 8 pointer: 0x14000116080
id: 5 len: 6 cap: 8 pointer: 0x14000116080
id: 6 len: 7 cap: 8 pointer: 0x14000116080
id: 7 len: 8 cap: 8 pointer: 0x14000116080
id: 8 len: 9 cap: 16 pointer: 0x1400013c000
id: 9 len: 10 cap: 16 pointer: 0x1400013c000
*/

删除元素

Go语言并没有删除切片元素的专用语法,需要使用切片本身的特性来删除元素(截取需要的数重新赋值)。

从开头位置删除

移动数据指针方式:

a = []int{1, 2, 3, 4}
a = a[1:] // 删除开头1个元素
a = a[N:] // 删除开头N个元素

数据指针不变方式:
使用append原地完成,就是在原有的切片数据对应的内存区间内完成,不会改变内存空间结构。

a = []int{1, 2, 3, 4}
// append()方法实现
a = append(a[:0], a[1:]...) // 删除开头1个元素
a = append(a[:0], a[N:]...) // 删除开头N个元素
// copy()方法实现
a = a[:copy(a, a[1:])] // 删除开头1个元素
a = a[:copy(a, a[N:])] // 删除开头N个元素

从中间位置删除

可以用 append 或 copy 原地完成。

a = []int{1, 2, 3, 4, ...}
// append()方法实现
a = append(a[:i], a[i+1:]...) // 删除中间1个元素,i代表元素的索引号
a = append(a[:i], a[i+N:]...) // 删除中间N个元素,i代表元素的索引号
// copy()方法实现
a = a[:i+copy(a[i:], a[i+1:])] // 删除中间1个元素,i代表元素的索引号
a = a[:i+copy(a[i:], a[i+N:])] // 删除中间N个元素,i代表元素的索引号

删除指定位置的元素。
【示例】代码如下所示:

package main

import "fmt"

func main() {
	seq := []string{"a", "b", "c", "d", "e"}
	// 指定删除位置
	index := 2
	
	// 查看删除位置之前的元素和之后的元素
	fmt.Println("删除位置之前的元素:", seq[:index], "删除位置之后的元素:", seq[index+1:])
	// 将删除点前后的元素连接起来
	seq = append(seq[:index], seq[index+1:]...)
	fmt.Println("删除指定位置元素之后的结果:", seq)
}
/* 运行结果如下
删除位置之前的元素: [a b] 删除位置之后的元素: [d e]
删除指定位置元素之后的结果: [a b d e]
*/

从尾部位置删除

跟从开头位置删除方法相似。

a = []int{1, 2, 3, 4}
a = a[:len(a)-1] // 删除尾部1个元素
a = a[:len(a)N] // 删除尾部N个元素

复制切片

Go语言中内置函数copy()可以复制数组切片,如果加入的两个数组切片不一样大,就会按照其中较小的那个数组切片的元素个数进行复制。
copy()使用方法如下所示:

copy(destSlice, srcSlice)

copy() 函数的第一个参数是要复制的目标,第二个是源目标,两个数组可以共享同一个底层数组,甚至有重叠的部分也没有关系。
示例如下所示:

data1 := []int{1, 2, 3, 4, 5}
data2 := []int{6, 7, 8}
data3 := []int{5, 4, 3}
data4 := []int{0, 1, 2}
copy(data2, data1) //data1复制到data2切片中,只会复制data1的前3个元素到data2中
fmt.Println("复制之后的data2:", data2)
copy(data4, data3) //data3复制到data4切片中,只会复制data3的3个元素到data4的前3个位置
fmt.Println("复制之后的data4:", data4)
/* 运行结果如下
复制之后的data2: [1 2 3]
复制之后的data4: [5 4 3]
*/

下面示例演示了切片引用和复制操作,对切片元素数据的影响。
【示例】代码如下所示:

package main

import "fmt"

func main() {
	// 设置元素数量为1000
	const elementCount = 1000
	// 创建源数组srcData并赋值0-999
	srcData := make([]int, elementCount)
	for i := 0; i < elementCount; i++ {
		srcData[i] = i
	}
	// 初始refData变量引用切片srcData的数据
	refData := srcData

	// 创建复制数组copyData,并复制srcData的数据
	copyData := make([]int, elementCount)
	copy(copyData, srcData)

	// 修改原始数据的第一个元素
	srcData[0] = 999
	// 打印引用切片的第一个元素
	fmt.Println("引用的切片第一个元素:", refData[0])
	// 打印复制切片的第一个和最后一个元素
	fmt.Println("复制的切片第一个元素:", copyData[0], ",复制的切片最后一个元素:", copyData[elementCount-1])

	// 复制原始数组切片从4到5的数据,并打印复制切片1到5位的元素
	copy(copyData, srcData[4:6])
	fmt.Printf("copyData从1到5位的值: ")
	for i := 0; i < 5; i++ {
		fmt.Printf("%d ", copyData[i])
	}
}

最后得出,引用切片在改变源目标切片数据的同时会影响数据,复制切片则不会发生变化。

make()函数构造切片

如果需要动态创建一个切片,可以使用make()函数。
注意:size 指的是为这个类型分配多少个元素,cap 为预分配的元素数量,这个值设定后不影响 size,只是能提前分配空间,降低多次分配空间造成的性能问题。
make()方法如下:

make([]Type, size, cap)

示例如下所示:

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

fmt.Println("a:", a, "a的长度大小:", len(a))
fmt.Println("b:", b, "b的长度大小:", len(b))
/* 运行结果如下
a: [0 0] a的长度大小: 2
b: [0 0] b的长度大小: 2
*/

多维数组

多维数组的声明

var 数组变量名 [元素数量1][元素数量2]...[元素数量...]Type

以下例子来演示多维数组的运用。
【示例】代码如下所示:

// 声明一个二维整型数组,两个维度的长度分别是 4 和 2
var array [4][2]int
// 初始化一个二维整型数组的元素
array = [4][2]int{{1, 3}, {22, 14}, {29, 31}, {30, 41}}
fmt.Println("数组元素:", array)
// 声明并初始化数组中索引为 1 和 3 的元素
array = [4][2]int{1: {20, 21}, 3: {40, 41}}
fmt.Println("第1次更改:", array)
// 再次声明并初始化数组中指定的元素
array = [4][2]int{1: {0: 20}, 3: {1: 41}}
fmt.Println("第2次更改:", array)
/* 运行结果如下
数组元素: [[1 3] [22 14] [29 31] [30 41]]
第1次更改: [[0 0] [20 21] [0 0] [40 41]]
第2次更改: [[0 0] [20 0] [0 0] [0 41]]
*/

多维数组之间的赋值和取值操作。
【示例】代码如下所示:

var array [2][2]int
// 设置每个元素的整型值
array[0][0] = 10
array[0][1] = 20
array[1][0] = 30
array[1][1] = 40
fmt.Println("array的值:", array)
// 将 array1 的值复制给 array2
var array1 [2][2]int
var array2 [2][2]int
array1 = [2][2]int{{8, 17}, {16, 23}}
array2 = array1
fmt.Println("array2的值:", array2)
// 在多维数组中取单个维度的值,以及单个维度其中单个元素的值
var array3 [2]int = array1[1]
var value int = array1[1][0]
fmt.Println("array1[1]的值:", array3, "array1[1][0]的值:", value)
/* 运行结果如下
array的值: [[10 20] [30 40]]
array2的值: [[8 17] [16 23]]
array1[1]的值: [16 23] array1[1][0]的值: 16
*/

range关键字

range关键字的作用就是循环迭代切片,可以通过range去循环迭代切片中的每个元素。
通过range配合for循环去迭代切片中每个元素
【示例】代码如下所示:

// 创建一个切片并初始化赋值
var slice []int = []int{10, 2, 33, 42, 15}
// 迭代每一个元素,并输出索引号和值
for index, value := range slice {
    fmt.Printf("索引序号: %d 元素的值: %d\n", index, value)
}
/* 运行结果如下
索引序号: 0 元素的值: 10
索引序号: 1 元素的值: 2
索引序号: 2 元素的值: 33
索引序号: 3 元素的值: 42
索引序号: 4 元素的值: 15
*/

二、Go映射(map)

Go语言中map是一种特殊的数据结构,一种元素对(pair)的无序集合,里面的数据对应着key(索引):value(值)这样的形式,这种结构也称为关联数组或字典。
map的声明语法,如下所示:

var map变量名 map[键类型]键对应的值类型

注意:在声明的时候不需要知道 map 的长度,因为 map 是可以动态增长的,未初始化的 map 的值是 nil

map简单操作

创建map

var a map[string]int
var b map[string]float32
c := make(map[string]string)

map赋值

// 对名为a的map进行赋值
a = map[string]int{"apple": 1, "orange": 2}
// 修改map对应key的值
a["apple"] = 3
a["orange"] = 2

打印map中的元素

// 打印整个map
fmt.Println(a)
fmt.Println(a["orange"])
/* 运行结果如下
map[apple:3 orange:2]
2
*/

map容量

和数组切片不同,map不存在固定长度或者最大限制,map可以根据key:value动态伸缩长度。但是可以标明map的初始容量capacity。
注意:当map增长到容量上限的时候,如果再增加新的key:value,map的大小会自动加1,所以出于性能的考虑,对于大的map或者会快速扩张的map,即使只是大概知道容量,也最好先标明。
示例如下所示:

// 语法格式
make(map[keytype]valuetype, cap)
// 示例如下
map := make(map[string]float, 100)

map中加入切片

map中可以使用切片当作value的值,我们只需要再定义map时通过将value的类型定义为[]int类型或者其他类型的切片就可以了。
示例如下所示:

mp1 := make(map[int][]int)
mp2 := make(map[int]*[]int)

循环遍历map

map 的遍历过程使用 for range 循环完成。
【示例】代码如下所示:

package main

import "fmt"

func main() {
	var fruit map[string]int
	fruit = map[string]int{"apple": 5, "orange": 2, "strawberry": 13, "watermelon": 6}

	// 只循环输出key的值
	for name := range fruit {
		fmt.Println(name)
	}
	// 循环输出key和value的值
	for name, price := range fruit {
		fmt.Println(name, ", ", price)
	}
	// 将不需要的键使用"_"改为匿名变量形式。
	for _, price := range fruit {
		fmt.Println(price)
	}
}

删除元素

Go语言提供了一个内置函数 delete(),用于删除容器内的元素。
语法如下:

delete(map名字, 键的值)

下面我们用一个示例来演示删除map中元素的方法。
【示例】代码如下所示:

// 创建一个map
var fruit map[string]int
fruit = map[string]int{"apple": 5, "orange": 2, "strawberry": 13, "watermelon": 6}

// 打印删除前的结果
fmt.Println("删除元素前:", fruit)
// 删除其中的元素
delete(fruit, "orange")
// 打印删除后的结果
fmt.Println("删除元素后:", fruit)
/* 运行结果如下
删除元素前: map[apple:5 orange:2 strawberry:13 watermelon:6]
删除元素后: map[apple:5 strawberry:13 watermelon:6]
*/

清空map中的所有元素
Go语言中并没有为map提供任何清空所有元素的函数、方法,清空map的唯一办法就是重新make一个新的map。