Go入门和《Go语言实战》的笔记,本篇总结了基础的数组、切片和哈希表的内容。

数组

数组是切片和映射的基础数据结构,因此了解数组的工作原理有助于理解切片和映射。
和C语言一样,在go中数组也是一段连续长度固定用于存储同一类型元素的连续块。

声明和初始化

数组的声明和初始化,和其他类型差不多。声明的原则是:

  1. 指明存储数据的类型。
  2. 存储元素的数量,也就是数组长度。
    1
    var array [5]int

声明变量时,总会使用对应类型的灵芝累对变量进行初始化,如上面的代码声明了一个数组array,但我们还没有对他进行初始化,此时这个数组内的值,就是对应类型的零值=> 这里的对应类型时int,因此改数组目前为5个0 [0,0,0,0,0]
由于数组初始化后长度是固定的,如果需要存储更多的元素则需要进行扩容。也就是需要再创建一个更长的数组,再把原来的数组复制到新数组里面。

上面的数组仅仅只是声明,go还可以很方便的初始化并声明

1
array := [5]int{1, 2, 3, 4, 5}

上面的这段代码相当于下面:

1
2
var array [5]int
array = [5]int{1, 2, 3, 4, 5}

除此之外,go语言还能自动计算声明数组的长度,也就是根据内容,自动分配长度

1
array := [...]int{1,2,3,4,5,6}

有的时候我们已知数组的长度,但内容只知道个别几个,我们可以用下面这种方式:

1
array := [5]int{1: 10, 3: 30}

这样我们声明了一个长度为5的数组,并且初始化索引为13的元素

使用数组

数组使用上和别的语言没有太大的差异,主要就是通过下标访问。值得关心的是,Go语言的指针数组十分的好用
将一个指针数组赋值给另一个:

1
2
3
4
5
6
7
8
9
var arr1 [3]*string

arr2 := [3]*string{new(string), new(string), new(string)}

*arr2[0] = "Red"
*arr2[1] = "Blue"
*arr2[2] = "Green"
// 复制
arr1 = arr2

此时复制后的两个数组则指向同一组字符串了。

函数间传递数组

在函数间传递变量时,总是以值的方式传递(也就是值传递)。因此在函数间传递数组是一个开销很大的操作–比如有个占用8M内存的数组,那么每次调用这个函数的时候go都会在栈上分配8MB的内存,试想一下同时调用100次这个函数,占用的内存会多么的惊人。
虽然Go自己会处理复制的这个操作,但还有一种更优雅的方法来处理这个操作,这个方法在C中十分的常见->传入指向数组的指针

1
2
3
4
5
6
7
8
// 分配一个8MB的数组
var arr [le6]int

foo(&arr)

fun foo(arr *[le6]int){
...
}

这是传递数组的指针的例子,会发现数组被修改了。所以这种情况虽然节省了复制的内存,但是要谨慎使用,因为一不小心,就会修改原数组,导致不必要的问题。

这里注意,数组的指针和指针数组是两个概念,数组的指针是*[5]int,指针数组是[5]*int,注意*的位置。

针对函数间传递数组的问题,比如复制问题,比如大小僵化问题,都有更好的解决办法,这个就是切片,它更灵活。

切片(Slice)

切片是一种数组结构,它是围绕动态数组的概念构建的(⚠和python的切片不完全相同)。切片可以按需自动增长和缩小,因为切片底层内存也是在连续的块中分配的,所以切片还有索引、迭代以及垃圾回收等好处

内部实现

切片的底层是数组,切片本身非常的小,它是对底层数组进行了抽象。切片有3个字段的数据结构,包含了Go需要操作数组的元数据。
这三个字段分别是指向底层数组的指针长度(切片能访问元素的个数)切片总体的容量(真实容量)

为了解决数组长度不可变,切片实际上就是提前声明了一个更长的数组(即切片的容量),而切片的长度表示当前切片内能访问的元素的数量。

因此切片有这样一条公式:长度<=容量

声明&初始化&使用

1. make和切片字面量

使用make函数时,需要传入一个参数,指定切片的长度

1
2
// 创建一个长度和容量都是5的字符串切片
slice := make([]string, 5)

前面说到,切片的长度和容量是两个不一样的概念,因此创建的时候也可以指定长度容量

1
2
// 长度为3,容量为5
slice := make([]string, 3, 5)

除了使用make函数,我们还可以使用切片字面量来声明切片–指定初始化的值

1
slice := []int{1,2,3,4,5}

可以发现切片和创建数组非常像,只不过不用指定[]中的值。 注意此时切片的长度和容量是相等的,并且会根据我们指定额字面量推到出来,当然我们也可以只初始化某个索引的值:

1
slice := []int{2: 1}

2. 空切片和nil切片
有的时候我们需要声明一个值为nil的切片(nil切片)。只要在声明式不做初始化就可以了。

1
var slice []int

空切片和nil切片不同的地方在于,空切片的底层数组包含0个元素,也就是说没有分配任何存储空间。
但切片里面的指向底层数组的指针是有内容的,而nil切片指向底层数组的指针则为nil

1
slice := make([]int, 0)

3. 使用切片
go的切片用法上和python的类似,如下:

1
2
3
4
slice := []int{1,2,3,4,5}

newSlice := slice[1:3]
// newSlice -> 2,3

需要注意,第一个切片因为使用字面量的方式,因此它的长度和容量都为5。不过之后的newSlice就不一样了,对于newSlice来说其底层数组的容量只有4个元素,切片长度为2。根据下面的公式,可以计算任意切片的长度和容量:

1
2
3
对于底层数组容量为K的切片 slice[i:j] 
长度: j - i
容量: k - i

由于切片是在元切片的基础上的抽象,因此新的切片和旧切片实际上指向的是同一个数组,故修改同一个索引的内容时会导致原切片的内容发生改变

1
2
3
4
5
6
7
array := []int{1, 2, 3, 4, 5}
newSlice := array[1:3]
// array -> 1,2,3,4,5
// newSlice -> 2, 3
newSlice[1] = 233
// array -> 1,2,233,4,5
// newSlice -> 2, 233

三个索引的切片
创建切片时,第三个索引选项可以用来控制新切片的容量。⚠其目的并不是增加容量,而时限制容量。

1
2
3
4
source := []string{"Apple", "Orange", "Plum", "Banana", "Grape"}
// 数组: [2:3) ; 容量: [2, 4)
// 因此长度: 1; 容量: 2
slice := source[2:3:4]

第三个选项也不可以超出索引范围!!!

切片增长

按需增长可以说是切片的一个重要的特性。Go内置的append函数会处理增长长度时所有的操作。

1
2
3
slice := []int{1, 2, 3, 4, 5}
newSlice := slice[1:3]
newSlice = append(newSlice, 60)

因为newSlice在底层数组里还有额外的容量可用,append会将可用的元素合并到切片的长度,并对其进行赋值。由于和原始的slice共享同一个底层数组,slice中索引为3的元素的值也被改动了。
如果底层数组没有足够的可用容量,append会创建一个新的底层数组,将被引用的现有值复制到新数组里,再追加新的值。

append会智能地处理底层数组地容量增长,当切片容量小于1000个元素时总会成倍地增加容量,超过1000后容量的增长因子设为1.25(增长算法不恒定)

此外,通过...操作符,把一个切片追加到另一个切片里。

1
2
3
slice := []int{1, 2, 3, 4, 5}
newSlice := slice[1:2:3]
newSlice = append(newSlice, slice...)

迭代切片

切片是一个集合,我们就可以迭代其中的元素。与python类似,Go有个特殊的关键字range,它可以配合for来迭代切片里的元素。

1
2
3
4
slice := []int{1, 2, 3, 4, 5}
for i,v:=range slice{
fmt.Printf("index:%d, value:%d\n",i,v)
}

代码中可以看到,迭代的时候会返回两个值: indexvalue,这里的value是一个副本。
需要强调的是,range创建了每个元素的副本,而不是直接返回该元素的引用。
很多时候,我们使用迭代都不需要索引index,此时可以使用占位符_来忽略这个值:

1
2
3
4
slice := []int{1, 2, 3, 4, 5}
for _,v:=range slice{
fmt.Printf("value:%d\n", v)
}

range总是从头开始迭代。如果需要更多的控制,依旧可以使用传统的for循环:

1
2
3
4
5
6
7
slice := []int{1, 2, 3, 4}
for index := 2; index < len(slice); index++{
fmt.Printf("Index: %d Value: %d\n", index, slice[index])
}
// Output
Index: 2 Value: 3
Index: 3 Value: 4

有两个特殊的内置函数len可以用于处理数组、切片和通道。对于切片来说,len返回切片的长度,cap返回切片的容量。

函数间传递

在函数间传递切片的时候,就是要以值的方式传递切片,因为切片的尺寸很小,在函数间复制和传递切片成本也很低。(因为切片的数据结构只是一个指向数组的指针、长度和容量,不是把整个数组复制)

1
2
3
4
5
6
7
slice := make([]int, le6)
slice = foo(slice)

func foo(slice []int) []int{
...
return slice
}

映射(Map)

正如标题所示,在《Go语言实战》中Map翻译成映射,相比于翻译相信Map更广为人知。
Map是一种数据结构(哈希表 or 散列表),用来存储一系列的键值对,如果你学习过别的语言相信看到这你就明白Map是什么了。在python中这样的数据结构称为dict(字典)JavaScript中称为json(JavaScript Object Notation)

内部实现

Map是Go语言中哈希表的实现,因此我们每次迭代Map时打印的Key和Value时无序的,每次迭代都是不一样的。

Map的散列表中包含一组桶,在存储、删除或查找键值对的时候,所有操作都要线选择一个桶,如何选择桶?就是先把要查找的key传给哈希函数,从而生成一个索引,进而找到对应的桶。

因此随着映射的增加,索引会分布的越来越均匀,因此访问键值对的速度就越快。(参考哈希表相关内容)由于本文主要是学习Go基础,因此不再继续深入,只要记住Map是无序的

声明&初始化

Map的创建有如下几种方式:

  1. make函数声明
    1
    dict := make(map[string]int)
  2. map字面量
    1
    2
    3
    4
    5
    6
    // 不指定任何键值对->也就是一个空map
    dict := map[string]int{}
    // 赋予内容
    dict := map[string]int{"张三":43}
    // 多个内容
    dict := map[string]int{"张三":43,"李四":50}

Map的键可以是任何值,键的类型可以是内置的类型,也可以是结构类型,但是不管怎么样,这个键可以使用==运算符进行比较,所以像切片、函数以及含有切片的结构类型就不能用于Map的键了,因为他们具有引用的语义,不可比较。

总结: 对于Map的值来说没有什么限制,但切片这种类型在键里不能用的,可以用在值里

使用Map

Go语言的Map和别的语言都大同小异,使用非常简单和数组切片差不多

如果键张三存在,则对其值修改,如果不存在,则新增这个键值对

1
2
3
dict := make(map[string]int)
dict["张三"] = 43
age := dict["张三"]

很多时候我们都要判断Map中是否存在某个键值对.在Go Map中,如果我们获取一个不存在的键的值,也是可以的,返回的是值类型的零值,这样就会导致我们不知道是真的存在一个为零值的键值对呢,还是说这个键值对就不存在。对此,Map为我们提供了检测一个键值对是否存在的方法。

1
2
3
4
age, exist := dict["李四"]
if exist {
...
}

看这个例子,和获取键的值没有太大区别,只是多了一个返回值。第一个返回值是键的值;第二个返回值标记这个键是否存在,这是一个boolean类型的变量,我们判断它就知道该键是否存在了。这也是Go多值返回的好处。

如果我们想删除一个键值对,可以使用内置的delete函数, delete函数接受两个参数,第一个是要操作的Map,第二个是要删除的Map的键。

1
delete(dict,"张三")

delete函数删除不存在的键也是可以的,只是没有任何作用。

在Go中,我们可以使用range迭代Map,这和遍历切片是一样的。

1
2
3
4
dict := map[string]int{"张三": 43}
for key, value := range dict {
fmt.Println(key, value)
}

rang返回两个值,这和python是类似的,第一个是键,第二个是值。

在函数间传递Map

函数间传递Map是不会制造副本的,也就是说如果一个Map传递给一个函数,该函数对这个Map做了修改,那么这个Map的所有引用都会被修改。

1
2
3
4
5
6
7
8
9
func main() {
dict := map[string]int{"王五": 60, "张三": 43}
modify(dict)
fmt.Println(dict["张三"])
}

func modify(dict map[string]int) {
dict["张三"] = 10
}