Go语言上手(三) | 青训营笔记

117 阅读6分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的的第3篇笔记

复合数据类型

数组、slice、map、结构体

数组

数组长度是固定的,因此在Go中很少直接使用数组。和数组对应的是slice(切片),它可以增长和收缩动态序列,且功能更加灵活,但要理解slice工作原理的话要先理解数组。

Go语言可以使用...表示数组的长度根据初始化个数计算。

q := [...]int{1, 2, 3}
fmt.Println("%T\n", q)  //"[3]int"
💡 数组的长度是数组类型的一个组成部分,即`[3]int`和`[4]int`是两种不同的数组类型。

数组的长度必须是常量表达式,因为数组的长度需要在编译阶段确定。

Go语言中数组的初始化可以不按顺序,如

r := [...]int{99: -1}

定义了一个含有100个元素的数组,最后一个元素初始化为-1,其余值全为0.

Go的函数调用

函数参数变量的接收是一个复制的副本,并不是原始的变量。即使是数组,也是发生在复制的数组上,所以对大型数组不友好(这点与其它编程语言隐式将数组作为引用或指针对象传入不同)。

当然,对于数组可以显式地传入数组指针,这样修改会直接作用到调用者。如

func zero(ptr *[32]byte) {
	for i := range ptr {
		ptr[i] = 0
	}
}

即使数组可以通过指针传递参数,但依然是僵化的类型。除了像SHA256这类需要处理特定大小数组的特例外,数组很少用作函数参数,而是使用slice代替数组。

Slice

slice代表变长序列,序列中每个元素都有相同的类型。

一个slice类型通常写作[]T,其中T代表slice中元素的类型。

slice语法和数组很像,只是没有固定长度而已。

slice切片操作底层共享的是之前的底层数组,因此切片操作都是常量时间复杂度。

slice的结构

slice是轻量级的数据结构,提供访问数组子序列全部功能,而且slice底层确实引用一个数组对象。

一个slice由三个部分构成:指针、长度、容量。

指针指向第一个slice元素对应的底层数组元素的地址。

💡 注意:第一个slice元素并不一定就是数组的第一个元素

长度对应slice中元素的数目。

容量是从slice的开始位置到底层数据的结尾位置。长度一般不能超过容量。

内置的len()返回长度,内置的cap()返回容量。

slice值包含指向第一个slice元素的指针。复制一个slice只是对底层的数组创建了一个新的slice别名。即直接修改原切片。

💡 切片和数组不同,无法进行比较。 对于字节型的slice,可以通过`bytes.Equal` 函数判断是否相等。 对于其他类型的slice,必须展开每个元素进行比较

为什么slice不直接支持比较运算符呢?

  1. 一个slice的元素是间接引用的,甚至可以包含自身。
  2. 一个固定的slice值(指slice本身,而不是元素的值)在不同时刻可能包含不同的元素。
  3. slice唯一合法的比较是和nil比较。一个零值的slice等于nil。

append函数

append用于向slice追加元素。

append的操作逻辑

每次使用函数,先检测slice底层数组是否有足够的容量来保存新添加的元素。

如果有足够空间,直接扩展slice,并返回。这时前后共享相同的底层数组。

如果没有足够空间,会先分配一个足够大的slice用于保存新的结果,这时前后将引用不同的底层数组。

因此append返回slice,要用等号赋给原slice,以保证指向扩展后的底层数组。

append函数的功能

append可以向slice追加一个元素、多个元素,甚至一个slice。

Map

一个map就是一个哈希表的引用。

map[K]V ,其中K必须是支持== 比较运算符的数据类型,可以通过测试key是否相等来判断是否已经存在。

map的创建、删除元素等操作

内置的make函数可以创建一个map。

mp := make(map[string]int)

我们也可以用map字面值的语法创建map,同时可以指定一些初始值。

mp := map[string]int{}

mp := map[string]int {
	"alice": 31,
	"charlie":34,
}

map可以通过内置的delete()删除元素。

delete(mp, "alice")

map中一些操作的情况说明

删除元素这个操作是安全的,即使这些元素不在map中也没关系。

同样,查找元素的操作也是安全的,当查找的元素不在map中时,将返回零值。

禁止对map元素取址操作,因为map可能随着元素数量的增长而重新分配更大的内存空间,从而导致之前的地址无效。

map的迭代遍历操作的顺序是不确定的,并且不同的额哈希函数实现可能导致不同的遍历顺序。这是故意设置的,每次使用随机的遍历顺序可以强制要求程序不会依赖具体的哈希函数实现。

如何实现对map的顺序遍历呢?

必须显式地对key进行排序,可是使用sort包中Strings函数对字符串slice进行排序。即先用slice存储排好序的key,再通过遍历方式取出map中的value。

ages := make(map[string]int)
var names []string
for name := range ages {   //此处忽略了第二个循环变量
	names = append(names, name)
}

sort.Strings(names)
for _, name := range names {  //此处忽略了第一个循环变量
	fmt.Printf("%s\t%d\n", name, ages[name])

map的零值

map的零值是nil。

如果知识定义了map,而没有创建,那map的值就是零值

var ages map[string]int  //map的值是零值

在值为nil的map上进行查找、删除、len、range操作都是安全的。但如果存入元素,就会导致panic异常。因此,在向map存数据前必须先创建map(用make或字面值语法)。

map的返回值

如果key存在,返回对应的value;如果key不存在,返回零值。

但只返回零值,不好确定是否有key。为了确定是否真的有key,mp[key] 会返回第二个值,这是个布尔值。

age, ok := ages["bob"]

经常结合起来使用:

//如果bob不是key的分支
if age, ok := ages["bob"]; !ok { ... }

map之间的比较

和slice一样,map之间也不能进行相等比较。唯一的例外是和nil进行比较。要判断两个map是否包含相同的key和value,通过循环实现。