Go语言容器(container)

Posted 知其黑、受其白

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Go语言容器(container)相关的知识,希望对你有一定的参考价值。

阅读目录

Go语言容器(container)

变量在一定程度上能满足函数及代码要求。

如果编写一些复杂算法、结构和逻辑,就需要更复杂的类型来实现。

这类复杂类型一般情况下具有各种形式的存储和处理数据的功能,将它们称为“容器(container)”。

在很多语言里,容器是以标准库的方式提供,你可以随时查看这些标准库的代码,了解如何创建,删除,维护内存。

详细介绍 数组、切片、映射,以及列表的增加、删除、修改和遍历的使用方法

Go语言数组详解

数组是一个由固定长度的特定类型元素组成的序列,一个数组可以由零个或多个元素组成。

因为数组的长度是固定的,所以在Go语言中很少直接使用数组。

和数组对应的类型是 Slice(切片),Slice 是可以增长和收缩的动态序列,功能也更灵活,但是想要理解 slice 工作原理的话需要先理解数组。

Go语言数组的声明

数组的声明语法如下:

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

语法说明如下所示:

  • 数组变量名:数组声明及使用时的变量名。
  • 元素数量:数组的元素数量,可以是一个表达式,但最终通过编译期计算的结果必须是整型数值,元素数量不能含有到运行时才能确认大小的数值。
  • Type:可以是任意基本类型,包括数组本身,类型为数组本身时,可以实现多维数组。

数组的每个元素都可以通过索引下标来访问,索引下标的范围是从 0 开始到数组长度减 1 的位置,内置函数 len() 可以返回数组中元素的个数。

package main

import (
	"fmt"
)

func main() 
	// 定义三个整数的数组
	var a [3]int
	fmt.Println(a[0])
	// 打印第一个元素

	fmt.Println(a[len(a)-1])
	// 打印最后一个元素

	// 打印索引和元素
	for i, v := range a 
		fmt.Printf("%d %d\\n", i, v)
	
	// 仅打印元素
	for _, v := range a 
		fmt.Printf("%d\\n", v)
	

E:\\go_test>go run main.go
0
0
0 0
1 0
2 0
0
0
0

E:\\go_test>

默认情况下,数组的每个元素都会被初始化为元素类型对应的零值,对于数字类型来说就是 0,同时也可以使用数组字面值语法,用一组值来初始化数组:

var q [3]int = [3]int1, 2, 3
var r [3]int = [3]int1, 2
fmt.Println(r[2]) // "0"

在数组的定义中,如果在数组长度的位置出现 “...” 省略号,则表示数组的长度是根据初始化值的个数来计算,因此,上面数组 q 的定义可以简化为:

q := [...]int1, 2, 3
fmt.Printf("%T\\n", q) // "[3]int"

数组的长度是数组类型的一个组成部分,因此 [3]int [4]int 是两种不同的数组类型,数组的长度必须是常量表达式,因为数组的长度需要在编译阶段确定。

q := [3]int1, 2, 3
q = [4]int1, 2, 3, 4 
// 编译错误:无法将 [4]int 赋给 [3]int

比较两个数组是否相等

如果两个数组类型相同(包括数组的长度,数组中元素的类型)的情况下,我们可以直接通过较运算符(==和!=)来判断两个数组是否相等,只有当两个数组的所有元素都是相等的时候数组才是相等的,不能比较两个类型不同的数组,否则程序将无法完成编译。

a := [2]int1, 2
b := [...]int1, 2
c := [2]int1, 3
fmt.Println(a == b, a == c, b == c)
// "true false false"

d := [3]int1, 2
fmt.Println(a == d)
// 编译错误:无法比较 [2]int == [3]int

遍历数组—访问每一个数组元素

遍历数组也和遍历切片类似,代码如下所示:

var team [3]string
team[0] = "hammer"
team[1] = "soldier"
team[2] = "mum"

for k, v := range team 
    fmt.Println(k, v)

0 hammer
1 soldier
2 mum

Go语言多维数组简述

Go语言中允许使用多维数组,因为数组属于值类型,所以多维数组的所有维度都会在创建时自动初始化零值,多维数组尤其适合管理具有父子关系或者与坐标系相关联的数据。

声明多维数组的语法如下所示:

var array_name [size1][size2]...[sizen] array_type

其中,array_name 为数组的名字,array_type 为数组的类型,size1、size2 等等为数组每一维度的长度。

二维数组是最简单的多维数组,二维数组本质上是由多个一维数组组成的。

【示例 1】声明二维数组

// 声明一个二维整型数组,两个维度的长度分别是 4 和 2
var array [4][2]int

// 使用数组字面量来声明并初始化一个二维整型数组
array = [4][2]int10, 11, 20, 21, 30, 31, 40, 41

// 声明并初始化数组中索引为 1 和 3 的元素
array = [4][2]int1: 20, 21, 3: 40, 41

// 声明并初始化数组中指定的元素
array = [4][2]int1: 0: 20, 3: 1: 41

下图展示了上面示例中声明的二维数组在每次声明并初始化后包含的值。

为了访问单个元素,需要反复组合使用 [ ] 方括号,如下所示。

【示例 2】为二维数组的每个元素赋值

// 声明一个 2×2 的二维整型数组
var array [2][2]int

// 设置每个元素的整型值
array[0][0] = 10
array[0][1] = 20
array[1][0] = 30
array[1][1] = 40

只要类型一致,就可以将多维数组互相赋值,如下所示,多维数组的类型包括每一维度的长度以及存储在元素中数据的类型。

【示例 3】同样类型的多维数组赋值

// 声明两个二维整型数组
var array1 [2][2]int
var array2 [2][2]int
// 为array2的每个元素赋值
array2[0][0] = 10
array2[0][1] = 20
array2[1][0] = 30
array2[1][1] = 40
// 将 array2 的值复制给 array1
array1 = array2

fmt.Println(array1)
E:\\go_test>go run main.go
[[10 20] [30 40]]

因为数组中每个元素都是一个值,所以可以独立复制某个维度,如下所示。

【示例 4】使用索引为多维数组赋值

// 将 array1 的索引为 1 的维度复制到一个同类型的新数组里
var array3 [2]int = array1[1]
// 将数组中指定的整型值复制到新的整型变量里
var value int = array1[1][0]

Go语言切片详解

切片(slice)是对数组的一个连续片段的引用,所以切片是一个引用类型(因此更类似于 C/C++ 中的数组类型,或者 Python 中的 list 类型),这个片段可以是整个数组,也可以是由起始和终止索引标识的一些项的子集,需要注意的是,终止索引标识的项不包括在切片内。

Go语言中切片的内部结构包含地址、大小和容量,切片一般用于快速地操作一块数据集合,如果将数据集合比作切糕的话,切片就是你要的“那一块”,切的过程包含从哪里开始(切片的起始位置)及切多大(切片的大小),容量可以理解为装切片的口袋大小,如下图所示。

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

切片默认指向一段连续内存区域,可以是数组,也可以是切片本身。

从连续内存区域生成切片是常见的操作,格式如下:

slice [开始位置 : 结束位置]

语法说明如下:

  • slice:表示目标切片对象;
  • 开始位置:对应目标切片对象的索引;
  • 结束位置:对应目标切片的结束索引。

从数组生成切片,代码如下:

var a  = [3]int1, 2, 3
fmt.Println(a, a[1:2])

其中 a 是一个拥有 3 个整型元素的数组,被初始化为数值 1 到 3,使用 a[1:2] 可以生成一个新的切片,代码运行结果如下:

[1 2 3]  [2]

其中 [2] 就是 a[1:2] 切片操作的结果。

从数组或切片生成新的切片拥有如下特性:

  • 取出的元素数量为 结束位置 - 开始位置;
  • 取出元素不包含结束位置对应的索引,切片最后一个元素使用 slice[len(slice)] 获取;
  • 当缺省开始位置时,表示从连续区域开头到结束位置;
  • 当缺省结束位置时,表示从开始位置到整个连续区域末尾;
  • 两者同时缺省时,与切片本身等效;
  • 两者同时为 0 时,等效于空切片,一般用于切片复位。

根据索引位置取切片 slice 元素值时,取值范围是(0~len(slice)-1),超界会报运行时错误,生成切片时,结束位置可以填写 len(slice) 但不会报错。

下面通过实例来熟悉切片的特性。

1) 从指定范围中生成切片

切片和数组密不可分,如果将数组理解为一栋办公楼,那么切片就是把不同的连续楼层出租给使用者,出租的过程需要选择开始楼层和结束楼层,这个过程就会生成切片,示例代码如下:

var highRiseBuilding [30]int

for i := 0; i < 30; i++ 
	highRiseBuilding[i] = i + 1


// 区间
fmt.Println(highRiseBuilding[10:15])
// 中间到尾部的所有元素
fmt.Println(highRiseBuilding[20:])
// 开头到中间指定位置的所有元素
fmt.Println(highRiseBuilding[:2])
E:\\go_test>go run main.go
[11 12 13 14 15]
[21 22 23 24 25 26 27 28 29 30]
[1 2]

E:\\go_test>

代码中构建了一个 30 层的高层建筑,数组的元素值从 1 到 30,分别代表不同的独立楼层,输出的结果是不同的租售方案。

切片有点像C语言里的指针,指针可以做运算,但代价是内存操作越界,切片在指针的基础上增加了大小,约束了切片对应的内存区域,切片使用中无法对切片内部的地址和大小进行手动调整,因此切片比指针更安全、强大。

2) 表示原有的切片

生成切片的格式中,当开始和结束位置都被忽略时,生成的切片将表示和原切片一致的切片,并且生成的切片与原切片在数据内容上也是一致的,代码如下:

a := []int1, 2, 3
fmt.Println(a[:]) // [1 2 3]

a 是一个拥有 3 个元素的切片,将 a 切片使用 a[:] 进行操作后,得到的切片与 a 切片一致。

3) 重置切片,清空拥有的元素

把切片的开始和结束位置都设为 0 时,生成的切片将变空,代码如下:

a := []int1, 2, 3
fmt.Println(a[0:0]) // []

直接声明新的切片

除了可以从原有的数组或者切片中生成切片外,也可以声明一个新的切片,每一种类型都可以拥有其切片类型,表示多个相同类型元素的连续集合,因此切片类型也可以被声明,切片类型声明格式如下:

var name []Type

其中 name 表示切片的变量名,Type 表示切片对应的元素类型。

下面代码展示了切片声明的使用过程:

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

// 声明整型切片
var numList []int

// 声明一个空切片
var numListEmpty = []int

// 输出3个切片
fmt.Println(strList, numList, numListEmpty)
E:\\go_test>go run main.go
[] [] []

E:\\go_test>
// 输出3个切片大小
fmt.Println(len(strList), len(numList), len(numListEmpty))
E:\\go_test>go run main.go
0 0 0

E:\\go_test>
// 切片判定空的结果
fmt.Println(strList == nil)
fmt.Println(numList == nil)
fmt.Println(numListEmpty == nil)
E:\\go_test>go run main.go
true
true
false

E:\\go_test>

切片是动态结构,只能与 nil 判定相等,不能互相判定相等。声明新的切片后,可以使用 append() 函数向切片中添加元素。

使用 make() 函数构造切片

如果需要动态地创建一个切片,可以使用 make() 内建函数,格式如下:

make( [] Type, size, cap )

其中 Type 是指切片的元素类型,size 指的是为这个类型分配多少个元素,cap 为预分配的元素数量,这个值设定后不影响 size,只是能提前分配空间,降低多次分配空间造成的性能问题。

示例如下:

a := make([]int, 2)
b := make([]int, 2, 10)
fmt.Println(a, b)
fmt.Println(len(a), len(b))
E:\\go_test>go run main.go
[0 0] [0 0]
2 2

E:\\go_test>

其中 a 和 b 均是预分配 2 个元素的切片,只是 b 的内部存储空间已经分配了 10 个,但实际使用了 2 个元素。

容量不会影响当前的元素个数,因此 a 和 b 取 len 都是 2。

温馨提示

使用 make() 函数生成的切片一定发生了内存分配操作,但给定开始与结束位置(包括切片复位)的切片只是将新的切片结构指向已经分配好的内存区域,设定开始与结束位置,不会发生内存分配操作。

Go语言 append() 为切片添加元素

Go语言的内建函数 append() 可以为切片动态添加元素,代码如下所示:

var a []int

// 追加1个元素
a = append(a, 1) 

// 追加多个元素, 手写解包方式
a = append(a, 1, 2, 3) 

// 追加一个切片, 切片需要解包
a = append(a, []int1,2,3...) 

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

切片在扩容时,容量的扩展规律是按容量的 2 倍数进行扩充,例如 1、2、4、8、16……,代码如下:

package main

import "fmt"

func main() 
	var numbers []int
	for i := 0; i < 10; i++ 
		numbers = append(numbers, i)
		fmt.Printf("len: %d  cap: %d pointer: %p\\n",
			len(numbers), cap(numbers), numbers)
	

E:\\go_test>go run main.go
len: 1  cap: 1 pointer: 0xc00000e0b8
len: 2  cap: 2 pointer: 0xc00000e100
len: 3  cap: 4 pointer: 0xc0000161a0
len: 4  cap: 4 pointer: 0xc0000161a0
len: 5  cap: 8 pointer: 0xc0000143c0
len: 6  cap: 8 pointer: 0xc0000143c0
len: 7  cap: 8 pointer: 0xc0000143c0
len: 8  cap: 8 pointer: 0xc0000143c0
len: 9  cap: 16 pointer: 0xc00010a080
len: 10  cap: 16 pointer: 0xc00010a080

E:\\go_test>

通过查看代码输出,可以发现一个有意思的规律:
切片长度 len 并不等于切片的容量 cap。

往一个切片中不断添加元素的过程,类似于公司搬家,公司发展初期,资金紧张,人员很少,所以只需要很小的房间即可容纳所有的员工,随着业务的拓展和收入的增加就需要扩充工位,但是办公地的大小是固定的,无法改变,因此公司只能选择搬家,每次搬家就需要将所有的人员转移到新的办公点。

  • 员工和工位就是切片中的元素。
  • 办公地就是分配好的内存。
  • 搬家就是重新分配内存。
  • 无论搬多少次家,公司名称始终不会变,代表外部使用切片的变量名不会修改。
  • 由于搬家后地址发生变化,因此内存“地址”也会有修改。

除了在切片的尾部追加,我们还可以在切片的开头添加元素:

import "fmt"

func main() 
	var a = []int1, 2, 3

	// 在开头添加1个元素
	a = append([]int0, a...)
	fmt.Println(a)

	// 在开头添加1个切片
	a = append([]int-3, -2, -1, a...)
	fmt.Println(a)

E:\\go_test>go run main.go
[0 1 2 3]
[-3 -2 -1 0 1 2 3]

E:\\go_test>

在切片开头添加元素一般都会导致内存的重新分配,而且会导致已有元素全部被复制 1 次,因此,从切片的开头添加元素的性能要比从尾部追加元素的性能差很多。

因为 append 函数返回新切片的特性,所以切片也支持链式操作,我们可以将多个 append 操作组合起来,实现在切片中间插入元素:

var a []int
// 在第i个位置插入x
a = append(a[:i], append([]intx, a[i:]...)...)
// 在第i个位置插入切片
a = append(a[:i], append([]int1, 2, 3, a[i:]...)...)

每个添加操作中的第二个 append 调用都会创建一个临时切片,并将 a[i:] 的内容复制到新创建的切片中,然后将临时创建的切片再追加到 a[:i] 中。

Go语言 copy():切片复制(切片拷贝)

Go语言的内置函数 copy() 可以将一个数组切片复制到另一个数组切片中,如果加入的两个数组切片不一样大,就会按照其中较小的那个数组切片的元素个数进行复制。

copy() 函数的使用格式如下:

copy( destSlice, srcSlice [] T) int

其中 srcSlice 为数据来源切片,destSlice 为复制的目标(也就是将 srcSlice 复制到 destSlice),目标切片必须分配过空间且足够承载复制的元素个数,并且来源和目标的类型必须一致,copy() 函数的返回值表示实际发生复制的元素个数。

下面的代码展示了使用 copy() 函数将一个切片复制到另一个切片的过程:

slice1 := []int1, 2, 3, 4, 5
slice2 := []int5, 4, 3
copy(slice2, slice1)
// 只会复制slice1的前3个元素到slice2中

copy(slice1, slice2)
// 只会复制slice2的3个元素到slice1的前3个位置

虽然通过循环复制切片元素更直接,不过内置的 copy() 函数使用起来更加方便,copy() 函数的第一个参数是要复制的目标 slice,第二个参数是源 slice,两个 slice 可以共享同一个底层数组,甚至有重叠也没有问题。

【示例】通过代码演示对切片的引用和复制操作后对切片元素的影响。

package main

import (
	"fmt"
)

func main() 
	// 设置元素数量为1000
	const elementCount = 1000
	// 预分配足够多的元素切片
	srcData := make([]int, elementCount)
	// 将切片赋值
	for i := 0; i < elementCount; i++ 
		srcData[i] = i
	
	// 引用切片数据
	refData := srcData

	// 预分配足够多的元素切片
	copyData := make([]int, elementCount)
	// 将数据复制到新的切片空间中
	copy(copyData, srcData)

	// 修改原始数据的第一个元素
	srcData[0] = 999
	// 打印引用切片的第一个元素
	fmt.Println(refData[0])
	// os.Exit(1) 999

	// 打印复制切片的第一个和最后一个元素
	fmt.Println(copyData[0], copyData[elementCount-1])
	// os.Exit(1) 0 999

	// 复制原始数据从4到6(不包含)
	copy(copyData, srcData[4:6])
	for i := 0; i < 5; i++ 
		fmt.Printf("%d ", copyData[i])以上是关于Go语言容器(container)的主要内容,如果未能解决你的问题,请参考以下文章

go语言中container容器数据结构heaplistring

Go语言容器—list

Go语言容器—list

无法启动服务 prometheus:oci 运行时错误:container_linux.go:235:启动容器进程导致“容器初始化过早退出”

CannotStartContainerError:API 错误(400):OCI 运行时创建失败:container_linux.go:348:导致启动容器进程

redhat7.4 docker run启动容器报错container_linux.go:449