Go开发 之 容器(数组Array、切片slice、映射map、列表list)

文章目录

0、唠唠叨叨

哈喽,我是沙师弟。如果不是初学者,不用过细的看此篇文章,可把其当作学习资料保存查询。我争取把这篇文章写的很全很详细,读这一篇就可以知道Go的容器的所有知识。在很多语言里,容器都是以标准库的方式提供。你可以随时查看这些标准库的代码,了解如何创建,删除,维护内存。此篇文章将详细介绍数组、切片、映射,以及列表的增加、删除、修改和遍历的使用方法。

1、数组-Array

1.1、什么是数组

数组是一个由固定长度的特定类型元素组成的序列,一个数组可以由零个或多个元素组成。因为数组的长度是固定的,所以在Go语言中很少直接使用数组。和数组对应的类型是切片,切片是可以增长和收缩的动态序列,功能也更灵活,但是想要理解切片工作原理的话需要先理解数组。

1.1.1、数组的声明

语法:var 数组变量名 [元素数量]Type

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

1.1.2、比较两个数组是否相等

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

1.1.3、示例

1.1.3.1、通过索引下标访问元素

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)
}

1.1.3.2、数组每个元素都会被初始化为元素类型对应的零值

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

1.1.3.3、在数组长度出现“…”,表示长度是根据初始化值的个数来计算

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

1.1.3.4、数组的长度需要在编译阶段确定

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

1.1.3.5、数组比较

a := [2]int{1, 2}
b := [...]int{1, 2}
c := [2]int{1, 3}
fmt.Println(a == b, a == c, b == c) // "true false false"
d := [3]int{1, 2}
fmt.Println(a == d) // 编译错误:无法比较 [2]int == [3]int

1.1.3.6、遍历数组(访问每一个数组元素)

var team [3]string
team[0] = "hammer"
team[1] = "soldier"
team[2] = "mum"
for k, v := range team { // 使用 for 循环,遍历 team 数组,遍历出的键 k 为数组的索引,值 v 为数组的每个元素值
    fmt.Println(k, v)
}

结果:

0 hammer
1 soldier
2 mum

1.2、多维数组

1.2.1、概念

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

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

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

1.2.2、示例

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

1.2.2.1、声明二维数组

// 声明一个二维整型数组,两个维度的长度分别是 4 和 2
var array [4][2]int
// 使用数组字面量来声明并初始化一个二维整型数组
array = [4][2]int{{10, 11}, {20, 21}, {30, 31}, {40, 41}}
// 声明并初始化数组中索引为 1 和 3 的元素
array = [4][2]int{1: {20, 21}, 3: {40, 41}}
// 声明并初始化数组中指定的元素
array = [4][2]int{1: {0: 20}, 3: {1: 41}}

1.2.2.2、为二维数组的每个元素赋值

// 声明一个 2×2 的二维整型数组
var array [2][2]int
// 设置每个元素的整型值
array[0][0] = 10
array[0][1] = 20
array[1][0] = 30
array[1][1] = 40

1.2.2.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

1.2.2.4、使用索引为多维数组赋值

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

2、切片-slice

2.1、切片详解

2.1.1、概念

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

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

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

切片默认指向一段连续内存区域,可以是数组,也可以是切片本身。
从连续内存区域生成切片是常见的操作,格式:slice [开始位置 : 结束位置]。语法说明如下:

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

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

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

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

2.1.2.1、从数组生成切片

var a  = [3]int{1, 2, 3}
fmt.Println(a, a[1:2]) // [1 2 3]  [2]

2.1.2.2、从指定范围中生成切片

var highRiseBuilding [30]int
for i := 0; i < 30; i++ {
        highRiseBuilding[i] = i + 1
}
// 区间
fmt.Println(highRiseBuilding[10:15]) // [11 12 13 14 15]
// 中间到尾部的所有元素
fmt.Println(highRiseBuilding[20:]) // [21 22 23 24 25 26 27 28 29 30]
// 开头到中间指定位置的所有元素
fmt.Println(highRiseBuilding[:2]) // [1 2]

2.1.2.3、 表示原有的切片

a := []int{1, 2, 3}
fmt.Println(a[:]) // [1 2 3]

2.1.2.4、重置切片,清空拥有的元素

a := []int{1, 2, 3}
fmt.Println(a[0:0]) // []

2.1.3、直接声明新的切片

除了可以从原有的数组或者切片中生成切片外,也可以声明一个新的切片,每一种类型都可以拥有其切片类型,表示多个相同类型元素的连续集合,因此切片类型也可以被声明,切片类型声明格式:var name []Type。其中 name 表示切片的变量名,Type 表示切片对应的元素类型。

// 声明字符串切片
var strList []string
// 声明整型切片
var numList []int
// 声明一个空切片
var numListEmpty = []int{}
// 输出3个切片
fmt.Println(strList, numList, numListEmpty) // [] [] []
// 输出3个切片大小
fmt.Println(len(strList), len(numList), len(numListEmpty)) // 0 0 0
// 切片判定空的结果
fmt.Println(strList == nil) // true
fmt.Println(numList == nil) // true
fmt.Println(numListEmpty == nil) // false,注:numListEmpty 已经被分配到了内存,但没有元素,因此和 nil 比较时是 false。

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

2.1.4、使用 make() 函数构造切片

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

a := make([]int, 2)
b := make([]int, 2, 10)
fmt.Println(a, b) // [0 0] [0 0]
fmt.Println(len(a), len(b)) // 2 2

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

2.2、为切片添加元素(append())

2.2.1、为切片动态添加元素

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

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

2.2.2、切片在扩容时,容量的扩展规律是按容量的 2 倍数进行扩充

例如 1、2、4、8、16……,代码如下:

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)
}

代码输出如下:
在这里插入图片描述

2.2.3、在切片的开头添加元素

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

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

2.2.4、在切片中间插入元素

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

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

2.3、切片复制(copy())

2.3.1、概念

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

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

2.3.2、用 copy() 将一个切片复制到另一个切片

slice1 := []int{1, 2, 3, 4, 5}
slice2 := []int{5, 4, 3}
copy(slice2, slice1) // 只会复制slice1的前3个元素到slice2中
copy(slice1, slice2) // 只会复制slice2的3个元素到slice1的前3个位置

2.3.3、切片的引用和复制操作后对切片元素的影响

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])
    // 打印复制切片的第一个和最后一个元素
    fmt.Println(copyData[0], copyData[elementCount-1])
    // 复制原始数据从4到6(不包含)
    copy(copyData, srcData[4:6])
    for i := 0; i < 5; i++ {
        fmt.Printf("%d ", copyData[i])
    }
}

结果如下:
在这里插入图片描述

2.4、从切片中删除元素

Go语言并没有对删除切片元素提供专用的语法或者接口,需要使用切片本身的特性来删除元素,根据要删除元素的位置有三种情况,分别是从开头位置删除、从中间位置删除和从尾部删除,其中删除切片尾部的元素速度最快

2.4.1、从开头位置删除

从开头位置删除有三个方法:移动数据指针、append原地完成、copy函数

2.4.1.1、直接移动数据指针

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

2.4.1.2、用 append 原地完成

所谓原地完成是指在原有的切片数据对应的内存区间内完成,不会导致内存空间结构的变化

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

2.4.1.3、copy() 函数

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

2.4.2、从中间位置删除

对于删除中间的元素,需要对剩余的元素进行一次整体挪动,同样可以用 append 或 copy 原地完成:

a = []int{1, 2, 3, ...}
a = append(a[:i], a[i+1:]...) // 删除中间1个元素
a = append(a[:i], a[i+N:]...) // 删除中间N个元素
a = a[:i+copy(a[i:], a[i+1:])] // 删除中间1个元素
a = a[:i+copy(a[i:], a[i+N:])] // 删除中间N个元素

2.4.3、从尾部删除

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

2.4.4、删除切片指定位置的元素

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

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

连续容器的元素删除无论在任何语言中,都要将删除点前后的元素移动到新的位置,随着元素的增加,这个过程将会变得极为耗时,因此,当业务需要大量、频繁地从一个切片中删除元素时,如果对性能要求较高的话,就需要考虑更换其他的容器了(如双链表等能快速从删除点删除元素)。

2.5、循环迭代切片(range())

2.5.1、基础使用方法

它可以配合关键字 for 来迭代切片里的每一个元素,如下所示:

// 创建一个整型切片,并赋值
slice := []int{10, 20, 30, 40}
// 迭代每一个元素,并显示其值
for index, value := range slice {
    fmt.Printf("Index: %d Value: %d\n", index, value)
}

结果:
在这里插入图片描述

2.5.2、range 提供了每个元素的副本

// 创建一个整型切片,并赋值
slice := []int{10, 20, 30, 40}
// 迭代每个元素,并显示值和地址
for index, value := range slice {
    fmt.Printf("Value: %d Value-Addr: %X ElemAddr: %X\n", value, &value, &slice[index])
}

结果:
在这里插入图片描述
因为迭代返回的变量是一个在迭代过程中根据切片依次赋值的新变量,所以 value 的地址总是相同的,要想获取每个元素的地址,需要使用切片变量和索引值(上面代码中的 &slice[index])。

2.5.3、使用空白标识符(下划线)来忽略索引值

// 创建一个整型切片,并赋值
slice := []int{10, 20, 30, 40}
// 迭代每个元素,并显示其值
for _, value := range slice {
    fmt.Printf("Value: %d\n", value)
}

结果:
在这里插入图片描述
关键字 range 总是会从切片头部开始迭代。如果想对迭代做更多的控制,则可以使用传统的 for 循环。

2.5.4、使用传统的 for 循环对切片进行迭代

// 创建一个整型切片,并赋值
slice := []int{10, 20, 30, 40}
// 从第三个元素开始迭代每个元素
for index := 2; index < len(slice); index++ {
    fmt.Printf("Index: %d Value: %d\n", index, slice[index])
}

结果:
在这里插入图片描述
当然,range 关键字不仅仅可以用来遍历切片,它还可以用来遍历数组、字符串、map 或者通道等。

2.6、多维切片

2.6.1、概念

Go语言中同样允许使用多维切片,声明一个多维数组的语法格式:var sliceName [][]...[]sliceType。其中,sliceName 为切片的名字,sliceType为切片的类型,每个[ ]代表着一个维度,切片有几个维度就需要几个[ ]。

2.6.2、二维切片使用示例

//声明一个二维切片
var slice [][]int
//为二维切片赋值
slice = [][]int{{10}, {100, 200}}

等价于:

// 声明一个二维整型切片并赋值
slice := [][]int{{10}, {100, 200}}

2.6.3、组合切片的切片

// 声明一个二维整型切片并赋值
slice := [][]int{{10}, {100, 200}}
// 为第一个切片追加值为 20 的元素
slice[0] = append(slice[0], 20)

Go语言里使用 append() 函数处理追加的方式很简明,先增长切片,再将新的整型切片赋值给外层切片的第一个元素,当上面代码中的操作完成后,再将切片复制到外层切片的索引为 0 的元素。

3、映射-Map

3.1、基础map

3.1.1、map 概念

Go语言中 map 是一种特殊的数据结构,一种元素对(pair)的无序集合,pair 对应一个 key(索引)和一个 value(值),所以这个结构也称为关联数组或字典,这是一种能够快速寻找值的理想结构,给定 key,就可以迅速找到对应的 value。map 这种数据结构在其他编程语言中也称为字典(Python)。

map 是引用类型,可以声明:var mapname map[keytype]valuetype,其中:

  • mapname 为 map 的变量名。
  • keytype 为键类型。
  • valuetype 是键对应的值类型。
  • [keytype] 和 valuetype 之间允许有空格。
  • 在声明的时不需要知道 map 的长度,因为 map 是可以动态增长的,未初始化的 map 的值是 nil,使用函数 len() 可以获取 map 中 pair 的数目。

示例:

package main
import "fmt"
func main() {
    var mapLit map[string]int
    //var mapCreated map[string]float32
    var mapAssigned map[string]int
    mapLit = map[string]int{"one": 1, "two": 2}
    mapCreated := make(map[string]float32)
    mapAssigned = mapLit
    mapCreated["key1"] = 4.5
    mapCreated["key2"] = 3.14159
    mapAssigned["two"] = 3
    fmt.Printf("Map literal at \"one\" is: %d\n", mapLit["one"]) // Map literal at "one" is: 1
    fmt.Printf("Map created at \"key2\" is: %f\n", mapCreated["key2"]) // Map created at "key2" is: 3.14159
    fmt.Printf("Map assigned at \"two\" is: %d\n", mapLit["two"]) // Map assigned at "two" is: 3
    fmt.Printf("Map literal at \"ten\" is: %d\n", mapLit["ten"]) // Map literal at "ten" is: 0
}

3.1.2、map 容量

和数组不同,map 可以根据新增的 key-value 动态的伸缩,因此它不存在固定长度或者最大限制,但是也可以选择标明 map 的初始容量 capacity,格式:make(map[keytype]valuetype, cap)
示例:

map2 := make(map[string]float, 100)

出于性能的考虑,对于大的 map 或者会快速扩张的 map,即使只是大概知道容量,也最好先标明。

音阶和对应的音频映射起来:

noteFrequency := map[string]float32 {
"C0": 16.35, "D0": 18.35, "E0": 20.60, "F0": 21.83,
"G0": 24.50, "A0": 27.50, "B0": 30.87, "A4": 440}

3.1.3、用切片作为 map 的值

既然一个 key 只能对应一个 value,而 value 又是一个原始类型,那么如果一个 key 要对应多个值呢?通过将 value 定义为 []int 类型或者其他类型的切片,就可以优雅的解决这个问题。示例:

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

3.2、遍历map

3.2.1、遍历用 for range 循环完成

注意:遍历输出元素的顺序与填充顺序无关,不能期望 map 在遍历时返回某种期望顺序的结果。

scene := make(map[string]int)
scene["route"] = 66
scene["brazil"] = 4
scene["china"] = 960
for k, v := range scene {
    fmt.Println(k, v)
}

3.2.2、如需要特定顺序的遍历结果,正确的做法是先排序

scene := make(map[string]int)
// 准备map数据
scene["route"] = 66
scene["brazil"] = 4
scene["china"] = 960
// 声明一个切片保存map数据
var sceneList []string
// 将map数据遍历复制到切片中
for k := range scene {
    sceneList = append(sceneList, k)
}
// 对切片进行排序
sort.Strings(sceneList) // sort.Strings 的作用是对传入的字符串切片进行字符串字符的升序排列
// 输出
fmt.Println(sceneList) // [brazil china route]

3.3、map元素的删除和清空

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

3.3.1、使用 delete() 函数从 map 中删除键值对

delete() 函数的格式:delete(map, 键)。其中 map 为要删除的 map 实例,键为要删除的 map 中键值对的键。
示例如下:

scene := make(map[string]int)
// 准备map数据
scene["route"] = 66
scene["brazil"] = 4
scene["china"] = 960
delete(scene, "brazil")
for k, v := range scene {
    fmt.Println(k, v)
}

结果如下:

route 66
china 960

3.3.2、清空 map 中的所有元素

有意思的是,Go语言中并没有为 map 提供任何清空所有元素的函数、方法,清空 map 的唯一办法就是重新 make 一个新的 map,不用担心垃圾回收的效率,Go语言中的并行垃圾回收效率比写一个清空函数要高效的多。

3.4、并发环境中的map(sync.Map)

sync.Map 有以下特性:

  • 无须初始化,直接声明即可。
  • sync.Map 不能使用 map 的方式进行取值和设置等操作,而是使用 sync.Map 的方法进行调用,Store 表示存储,Load 表示获取,Delete 表示删除。
  • 使用 Range 配合一个回调函数进行遍历操作,通过回调函数返回内部遍历出来的值,Range 参数中回调函数的返回值在需要继续迭代遍历时,返回 true,终止迭代遍历时,返回 false。

代码如下:

package main
import (
      "fmt"
      "sync"
)
func main() {
    var scene sync.Map
    // 将键值对保存到sync.Map
    scene.Store("greece", 97)
    scene.Store("london", 100)
    scene.Store("egypt", 200)
    // 从sync.Map中根据键取值
    fmt.Println(scene.Load("london"))
    // 根据键删除对应的键值对
    scene.Delete("london")
    // 遍历所有sync.Map中的键值对
    scene.Range(func(k, v interface{}) bool {
        fmt.Println("iterate:", k, v)
        return true
    })
}

结果:
在这里插入图片描述
sync.Map 没有提供获取 map 数量的方法,替代方法是在获取 sync.Map 时遍历自行计算数量,sync.Map 为了保证并发安全有一些性能损失,因此在非并发情况下,使用 map 相比使用 sync.Map 会有更好的性能。

4、列表 - List

列表是一种非连续的存储容器,由多个节点组成,节点通过一些变量记录彼此之间的关系,列表有多种实现方法,如单链表、双链表等。在Go语言中,列表使用 container/list 包来实现,内部的实现原理是双链表,列表能够高效地进行任意位置的元素插入和删除操作。

4.1、初始化列表

list 的初始化有两种方法:分别是使用 New() 函数和 var 关键字声明,两种方法的初始化效果都是一致的。

  • 通过 container/list 包的 New() 函数初始化 list: 变量名 := list.New()
  • 通过 var 关键字声明初始化 list: var 变量名 list.List

列表与切片和 map 不同的是,列表并没有具体元素类型的限制,因此,列表的元素可以是任意类型,这既带来了便利,也引来一些问题,例如给列表中放入了一个 interface{} 类型的值,取出值后,如果要将 interface{} 转换为其他类型将会发生宕机。

4.2、在列表中插入元素

双链表支持从队列前方或后方插入元素,分别对应的方法是 PushFront 和 PushBack。这两个方法都会返回一个 *list.Element 结构,如果在以后的使用中需要删除插入的元素,则只能通过 *list.Element 配合 Remove() 方法进行删除,这种方法可以让删除更加效率化,同时也是双链表特性之一。下面代码展示如何给 list 添加元素:

l := list.New()
l.PushBack("fist")
l.PushFront(67)

列表插入元素的方法如下表所示。

方 法 功 能
InsertAfter(v interface {}, mark * Element) * Element 在 mark 点之后插入元素,mark 点由其他插入函数提供
InsertBefore(v interface {}, mark * Element) *Element 在 mark 点之前插入元素,mark 点由其他插入函数提供
PushBackList(other *List) 添加 other 列表元素到尾部
PushFrontList(other *List) 添加 other 列表元素到头部

4.3、从列表中删除元素

列表插入函数的返回值会提供一个 *list.Element 结构,这个结构记录着列表元素的值以及与其他节点之间的关系等信息,从列表中删除元素时,需要用到这个结构进行快速删除。

package main
import "container/list"
func main() {
    l := list.New()
    // 尾部添加
    l.PushBack("canon")
    // 头部添加
    l.PushFront(67)
    // 尾部添加后保存元素句柄
    element := l.PushBack("fist")
    // 在fist之后添加high
    l.InsertAfter("high", element)
    // 在fist之前添加noon
    l.InsertBefore("noon", element)
    // 使用
    l.Remove(element)
}

下表中展示了每次操作后列表的实际元素情况。

操作内容 列表元素
l.PushBack(“canon”) canon
l.PushFront(67) 67, canon
element := l.PushBack(“fist”) 67, canon, fist
l.InsertAfter(“high”, element) 67, canon, fist, high
l.InsertBefore(“noon”, element) 67, canon, noon, fist, high
l.Remove(element) 67, canon, noon, high

4.4、遍历列表(访问列表的每一个元素)

遍历双链表需要配合 Front() 函数获取头元素,遍历时只要元素不为空就可以继续进行,每一次遍历都会调用元素的 Next() 函数,代码如下:

l := list.New()
// 尾部添加
l.PushBack("canon")
// 头部添加
l.PushFront(67)
for i := l.Front(); i != nil; i = i.Next() {
    fmt.Println(i.Value)
}

结果:

67
canon

5、空值/零值 - nil

5.1、空值的概念

在Go语言中,布尔类型的零值(初始值)为 false,数值类型的零值为 0,字符串类型的零值为空字符串"",而指针、切片、映射、通道、函数和接口的零值则是 nil。

nil 是Go语言中一个预定义好的标识符,有过其他编程语言开发经验的开发者也许会把 nil 看作其他语言中的 null,其实这并不是完全正确的,因为Go语言中的 nil 和其他语言中的 null 有很多不同点。

5.2、nil 标识符是不能比较的

package main
import (
    "fmt"
)
func main() {
    fmt.Println(nil==nil)
}

结果:
在这里插入图片描述
从上面的运行结果不难看出,==对于 nil来说是一种未定义的操作。

5.3、nil 不是关键字或保留字

nil 并不是Go语言的关键字或者保留字,也就是说我们可以定义一个名称为 nil 的变量,比如:

var nil = errors.New("my god")

虽然上面的声明语句可以通过编译,但是并不提倡这么做。

5.4、nil 没有默认类型

package main
import (
    "fmt"
)
func main() {
    fmt.Printf("%T", nil)
    print(nil)
}

结果:
在这里插入图片描述

5.5、不同类型 nil 的指针是一样的

package main
import (
    "fmt"
)
func main() {
    var arr []int
    var num *int
    fmt.Printf("%p\n", arr)
    fmt.Printf("%p", num)
}

结果:
在这里插入图片描述
通过运行结果可以看出 arr 和 num 的指针都是 0x0。

5.6、不同类型的 nil 是不能比较的

package main
import (
    "fmt"
)
func main() {
    var m map[int]string
    var ptr *int
    fmt.Printf(m == ptr)
}

结果:
在这里插入图片描述

5.7、两个相同类型的 nil 值也可能无法比较

在Go语言中 map、slice 和 function 类型的 nil 值不能比较,比较两个无法比较类型的值是非法的,下面的语句无法编译:

package main
import (
    "fmt"
)
func main() {
    var s1 []int
    var s2 []int
    fmt.Printf(s1 == s2)
}

结果:
在这里插入图片描述
能够将上述不可比较类型的空值直接与 nil 标识符进行比较,如下所示:

package main
import (
    "fmt"
)
func main() {
    var s1 []int
    fmt.Println(s1 == nil)
}

结果:
在这里插入图片描述

5.8、nil 是 map、slice、pointer、channel、func、interface 的零值

直接看代码:

package main
import (
    "fmt"
)
func main() {
    var m map[int]string
    var ptr *int
    var c chan int
    var sl []int
    var f func()
    var i interface{}
    fmt.Printf("%#v\n", m)
    fmt.Printf("%#v\n", ptr)
    fmt.Printf("%#v\n", c)
    fmt.Printf("%#v\n", sl)
    fmt.Printf("%#v\n", f)
    fmt.Printf("%#v\n", i)
}

结果:
在这里插入图片描述
零值是Go语言中变量在声明之后但是未初始化被赋予的该类型的一个默认值。

5.9、不同类型的 nil 值占用的内存大小可能是不一样的

一个类型的所有的值的内存布局都是一样的,nil 也不例外,nil 的大小与同类型中的非 nil 类型的大小是一样的。但是不同类型的 nil 值的大小可能不同。

package main
import (
    "fmt"
    "unsafe"
)
func main() {
    var p *struct{}
    fmt.Println( unsafe.Sizeof( p ) ) // 8
    var s []int
    fmt.Println( unsafe.Sizeof( s ) ) // 24
    var m map[int]bool
    fmt.Println( unsafe.Sizeof( m ) ) // 8
    var c chan string
    fmt.Println( unsafe.Sizeof( c ) ) // 8
    var f func()
    fmt.Println( unsafe.Sizeof( f ) ) // 8
    var i interface{}
    fmt.Println( unsafe.Sizeof( i ) ) // 16
}

结果:
在这里插入图片描述
具体的大小取决于编译器和架构,上面打印的结果是在 32 位架构和标准编译器下完成的,对应 64 位的架构的,打印的大小将是现在的两倍。

6、关键字make和new的区别及实现原理

Go语言中,make 关键字的主要作用是初始化内置的数据结构,也就是我们在前面提到的数组、切片、映射 和 协程Channel的初始化,而当我们想要获取指向某个类型的指针时可以使用 new 关键字。new 主要用于结构体的初始化。

6.1、概述

make 在Go语言中只能用于初始化语言中的基本类型,例如:

slice := make([]int, 0, 100)
hash := make(map[int]bool, 10)
ch := make(chan int, 5)

这些基本类型都是语言提供的,但这三者返回了不同类型的数据结构:

  • slice 是一个包含 data、cap 和 len 的结构体;
  • hash 是一个指向 hmap 结构体的指针;
  • ch 是一个指向 hchan 结构体的指针。

make 用于创建切片、哈希表和管道等内置数据结构,new 用于分配并创建一个指向对应类型的指针。

6.2、实现原理

users:=make([10]int);
msg:=make(chan int);

var p *[]int = new([]int)
p := new([]int)

new会分配结构空间,并初始化为清空为零,不进一步初始化
new之后需要一个指针来指向这个结构
make会分配结构空间及其附属空间,并完成其间的指针初始化
make返回这个结构空间,不另外分配一个指针

以上分配了一个slice结构,但是结构中的应该指向底层数组的ptr指针为空,故实际不能往这个slice里面存取数据
同时分配了一个指针p,也即(在32位系统中)占4个字节并存放slice结构的地址

var v []int = make([]int, 0)
v := make([]int, 0)

以上分配了一个slice结构,且结构中的应该指向底层数组的ptr指针已经指向了某个底层数组,这个底层数组应该已经分配了,slice已经可以使用了。注意v就是这个slice结构,而不是一个指向slice的指针。

一般使用时都会明确长度和容量:v := make([]int, 10, 50)

6.3、总结

用new来分配slice的意义不大,因为没有恰当的初始化,无法直接使用。有附带空间的结构,使用make来初始化,可以完成内部指针初始化,其后可以立即使用。
make 关键字的主要作用是创建切片、哈希表和 Channel 等内置的数据结构,而 new 的主要作用是为类型申请一片内存空间,并返回指向这片内存的指针。

发布了264 篇原创文章 · 获赞 691 · 访问量 204万+

猜你喜欢

转载自blog.csdn.net/u014597198/article/details/103251976