Golang基础语法笔记 | 数组、切片&映射
Go入门和《Go语言实战》的笔记,本篇总结了基础的数组、切片和哈希表的内容。
数组
数组是切片和映射的基础数据结构,因此了解数组的工作原理有助于理解切片和映射。
和C语言一样,在go中数组也是一段连续、长度固定用于存储同一类型元素的连续块。
声明和初始化
数组的声明和初始化,和其他类型差不多。声明的原则是:
- 指明存储数据的类型。
- 存储元素的数量,也就是数组长度。
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 | var array [5]int |
除此之外,go语言还能自动计算声明数组的长度,也就是根据内容,自动分配长度
1 | array := [...]int{1,2,3,4,5,6} |
有的时候我们已知数组的长度,但内容只知道个别几个,我们可以用下面这种方式:
1 | array := [5]int{1: 10, 3: 30} |
这样我们声明了一个长度为5
的数组,并且初始化索引为1
和3
的元素
使用数组
数组使用上和别的语言没有太大的差异,主要就是通过下标访问。值得关心的是,Go语言的指针数组十分的好用
将一个指针数组赋值给另一个:
1 | var arr1 [3]*string |
此时复制后的两个数组则指向同一组字符串了。
函数间传递数组
在函数间传递变量时,总是以值的方式传递(也就是值传递)。因此在函数间传递数组是一个开销很大的操作–比如有个占用8M
内存的数组,那么每次调用这个函数的时候go都会在栈上分配8MB的内存,试想一下同时调用100次这个函数,占用的内存会多么的惊人。
虽然Go自己会处理复制的这个操作,但还有一种更优雅的方法来处理这个操作,这个方法在C中十分的常见->传入指向数组的指针
1 | // 分配一个8MB的数组 |
这是传递数组的指针的例子,会发现数组被修改了。所以这种情况虽然节省了复制的内存,但是要谨慎使用,因为一不小心,就会修改原数组,导致不必要的问题。
这里注意,数组的指针和指针数组是两个概念,数组的指针是
*[5]int
,指针数组是[5]*int
,注意*
的位置。
针对函数间传递数组的问题,比如复制问题,比如大小僵化问题,都有更好的解决办法,这个就是切片,它更灵活。
切片(Slice)
切片是一种数组结构,它是围绕动态数组的概念构建的(⚠和python的切片不完全相同)。切片可以按需自动增长和缩小,因为切片底层内存也是在连续的块中分配的,所以切片还有索引、迭代以及垃圾回收等好处
内部实现
切片的底层是数组,切片本身非常的小,它是对底层数组进行了抽象。切片有3个字段的数据结构,包含了Go需要操作数组的元数据。
这三个字段分别是指向底层数组的指针
、长度(切片能访问元素的个数)
和切片总体的容量(真实容量)
为了解决数组长度不可变,切片实际上就是提前声明了一个更长的数组(即切片的容量),而切片的长度表示当前切片内能访问的元素的数量。
因此切片有这样一条公式:长度<=容量
声明&初始化&使用
1. make和切片字面量
使用make
函数时,需要传入一个参数,指定切片的长度
1 | // 创建一个长度和容量都是5的字符串切片 |
前面说到,切片的长度和容量是两个不一样的概念,因此创建的时候也可以指定长度
和容量
1 | // 长度为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 | slice := []int{1,2,3,4,5} |
需要注意,第一个切片因为使用字面量的方式,因此它的长度和容量都为5。不过之后的newSlice
就不一样了,对于newSlice
来说其底层数组的容量只有4个元素,切片长度为2。根据下面的公式,可以计算任意切片的长度和容量:
1 | 对于底层数组容量为K的切片 slice[i:j] |
由于切片是在元切片的基础上的抽象,因此新的切片和旧切片实际上指向的是同一个数组,故修改同一个索引的内容时会导致原切片的内容发生改变
1 | array := []int{1, 2, 3, 4, 5} |
三个索引的切片
创建切片时,第三个索引选项可以用来控制新切片的容量。⚠其目的并不是增加容量,而时限制容量。
1 | source := []string{"Apple", "Orange", "Plum", "Banana", "Grape"} |
第三个选项也不可以超出索引范围!!!
切片增长
按需增长可以说是切片的一个重要的特性。Go内置的append
函数会处理增长长度时所有的操作。
1 | slice := []int{1, 2, 3, 4, 5} |
因为newSlice
在底层数组里还有额外的容量可用,append会将可用的元素合并到切片的长度,并对其进行赋值。由于和原始的slice
共享同一个底层数组,slice中索引为3的元素的值也被改动了。
如果底层数组没有足够的可用容量,append会创建一个新的底层数组,将被引用的现有值复制到新数组里,再追加新的值。
append会智能地处理底层数组地容量增长,当切片容量小于1000个元素时总会成倍地增加容量,超过1000后容量的增长因子设为1.25(增长算法不恒定)
此外,通过...
操作符,把一个切片追加到另一个切片里。
1 | slice := []int{1, 2, 3, 4, 5} |
迭代切片
切片是一个集合,我们就可以迭代其中的元素。与python类似,Go有个特殊的关键字range
,它可以配合for
来迭代切片里的元素。
1 | slice := []int{1, 2, 3, 4, 5} |
代码中可以看到,迭代的时候会返回两个值: index
和value
,这里的value
是一个副本。
需要强调的是,range创建了每个元素的副本,而不是直接返回该元素的引用。
很多时候,我们使用迭代都不需要索引index
,此时可以使用占位符_
来忽略这个值:
1 | slice := []int{1, 2, 3, 4, 5} |
range
总是从头开始迭代。如果需要更多的控制,依旧可以使用传统的for循环:
1 | slice := []int{1, 2, 3, 4} |
有两个特殊的内置函数len
可以用于处理数组、切片和通道。对于切片来说,len
返回切片的长度,cap
返回切片的容量。
函数间传递
在函数间传递切片的时候,就是要以值的方式传递切片,因为切片的尺寸很小,在函数间复制和传递切片成本也很低。(因为切片的数据结构只是一个指向数组的指针、长度和容量,不是把整个数组复制)
1 | slice := make([]int, le6) |
映射(Map)
正如标题所示,在《Go语言实战》中Map翻译成映射,相比于翻译相信Map更广为人知。
Map是一种数据结构(哈希表 or 散列表),用来存储一系列的键值对,如果你学习过别的语言相信看到这你就明白Map是什么了。在python
中这样的数据结构称为dict(字典)
、JavaScript
中称为json(JavaScript Object Notation)
内部实现
Map是Go语言中哈希表的实现,因此我们每次迭代Map时打印的Key和Value时无序的,每次迭代都是不一样的。
Map的散列表中包含一组桶,在存储、删除或查找键值对的时候,所有操作都要线选择一个桶,如何选择桶?就是先把要查找的key
传给哈希函数,从而生成一个索引,进而找到对应的桶。
因此随着映射的增加,索引会分布的越来越均匀,因此访问键值对的速度就越快。(参考哈希表相关内容)由于本文主要是学习Go基础,因此不再继续深入,只要记住Map是无序的
声明&初始化
Map的创建有如下几种方式:
make
函数声明1
dict := make(map[string]int)
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 | dict := make(map[string]int) |
很多时候我们都要判断Map中是否存在某个键值对.在Go Map中,如果我们获取一个不存在的键的值,也是可以的,返回的是值类型的零值,这样就会导致我们不知道是真的存在一个为零值的键值对呢,还是说这个键值对就不存在。对此,Map为我们提供了检测一个键值对是否存在的方法。
1 | age, exist := dict["李四"] |
看这个例子,和获取键的值没有太大区别,只是多了一个返回值。第一个返回值是键的值;第二个返回值标记这个键是否存在,这是一个boolean类型的变量,我们判断它就知道该键是否存在了。这也是Go多值返回的好处。
如果我们想删除一个键值对,可以使用内置的delete
函数, delete
函数接受两个参数,第一个是要操作的Map,第二个是要删除的Map的键。
1 | delete(dict,"张三") |
delete函数删除不存在的键也是可以的,只是没有任何作用。
在Go中,我们可以使用range
迭代Map,这和遍历切片是一样的。
1 | dict := map[string]int{"张三": 43} |
rang
返回两个值,这和python是类似的,第一个是键,第二个是值。
在函数间传递Map
函数间传递Map是不会制造副本的,也就是说如果一个Map传递给一个函数,该函数对这个Map做了修改,那么这个Map的所有引用都会被修改。
1 | func main() { |