前言
本文记录训练营语法基础课部分的相关内容,对于课程中讲的不够充分的地方,结合了go入门指南中的详细介绍进行了补充。笔记同步更新在我的博客
本篇讲述了array,slice和map。
数组
概述
go在数组和切片的设计上明显收到python的影响。
以 [] 符号标识的数组类型几乎在所有的编程语言中都是一个基本主力。Go 语言中的数组也是类似的,只是有一些特点。Go 没有 C 那么灵活,但是拥有切片(slice)类型。这是一种建立在 Go 语言数组类型之上的抽象,要想理解切片我们必须先理解数组。数组有特定的用处,但是却有一些呆板,所以在 Go 语言的代码里并不是特别常见。相对的,切片确实随处可见的。它们构建在数组之上并且提供更强大的能力和便捷
数组是具有相同 唯一类型 的一组已编号且长度固定的数据项序列(这是一种同构的数据结构);这种类型可以是任意的原始类型例如整型、字符串或者自定义类型。数组长度必须是一个常量表达式,并且必须是一个非负整数。数组长度也是数组类型的一部分,所以[5]int [10]int是属于不同类型的。数组的编译时值初始化是按照数组顺序完成的。
数组元素可以通过 索引(位置)来读取(或者修改),索引从 0 开始,第一个元素索引为 0,第二个索引为 1,以此类推。(数组以 0 开始在所有类 C 语言中是相似的)。元素的数目,也称为长度或者数组大小必须是固定的并且在声明该数组时就给出(编译时需要知道数组长度以便分配内存);数组长度最大内存用量 2Gb (256MB , 大约2^28个int32)
遍历
可以使用 for的两种基本写法进行数组遍历。
数组变量的类型
var arr1 = new([5]int)
arr1的类型是 *[5]int , 以c++的方式理解,是个指针(引用)类型。
var arr2 [5]int
arr2的类型是 [5]int , 是一种值类型。
深浅拷贝
浅拷贝
非常好理解,因为拷贝了指向数组组的引用。
var arr1 = new([5]int)
arr1[3] = 100
var arr2 = arr1 // shallow copy
arr2[3] = 99
fmt.Println("%d %d", arr1[3], arr2[3])
深拷贝
var arr3 [5]int = [...]int{1, 2, 3, 4, 5}
arr3[3] = 100
var arr4 = arr3 // deep copy
arr4[3] = 99
fmt.Println("%d %d", arr3[3], arr4[3])
参数传递
package main
import "fmt"
func f(a [3]int) { fmt.Println(a) }
func fp(a *[3]int) { fmt.Println(a) }
func main() {
var ar [3]int
f(ar) // passes a copy of ar
fp(&ar) // passes a pointer to ar
}
初始化
写法一
var arrAge = [5]int{18, 20, 15, 22, 16}
支持部分初始化,类似[10]int {1 , 2 , 3} 未初始化的位置都为零。
写法二
var arrLazy = [...]int{5, 6, 7, 8, 22}
类似于一种解包操作。
写法三
var arrKeyValue = [5]string{3: "Chris", 4: "Ron"}
key-value语法,赋值特定的位置。
切片
切片(slice)是对数组一个连续片段的引用(该数组我们称之为相关数组,通常是匿名的),所以切片是一个引用类型(因此更类似于 C/C++ 中的数组类型,或者 Python 中的 list 类型)。这个片段可以是整个数组,或者是由起始和终止索引标识的一些项的子集。需要注意的是,终止索引标识的项不包括在切片内。切片提供了一个相关数组的动态窗口。
切片是可索引的,并且可以由 len() 函数获取长度。
给定项的切片索引可能比相关数组的相同元素的索引小。和数组不同的是,切片的长度可以在运行时修改,最小为 0 最大为相关数组的长度。
声明
var identifier []type (不需要说明长度)。
初始化
var slice1 []type = arr1[start:end] (左闭右开)。
通过数组创建切片
s := [3]int{1,2,3}[:] , s := []int{1,2,3} , var x = []int{2, 3, 5, 7, 11} 这些写法是等价的。
切片的增长
切片只能向后移动
slice1 = slice1[0:cap(slice1)] // 从原来的起始位置增长到原始数组的末尾
slice1 = slice1[1:] // 将slice的头部往前移动一位,尾部不变
slice1 = slice1[1:len(slice1)+1] // 将slice向后滑动一位 可能会溢出
传递参数
如果你有一个函数需要对数组做操作,你可能总是需要把参数声明为切片。当你调用该函数时,把数组分片,创建为一个 切片引用并传递给该函数。这里有一个计算数组元素和的方法:
func sum(a []int) int {
s := 0
for i := 0; i < len(a); i++ {
s += a[i]
}
return s
}
func main() {
var arr = [5]int{0, 1, 2, 3, 4}
sum(arr[:])
}
使用make创造切片
slice1 := make([]type , len)
这里len是底层数组长度,也是slice的初始长度。
slice1 := make([]type , len , cap)
len是切片长度,cap是底层数组长度, 切片的首个元素将和数组的首个元素对齐。
底层数组是在堆上开辟的,不会直接暴露为变量。
make() 和 new()
二者都是在堆上分配内存,但是它们的行为不同,也适用于不同的类型。
make()
- 用于创建 slice、map 和 channel 这三种引用类型的数据结构。
make()返回的是已初始化、内存已分配的数据结构。make()接受两个参数,第一个参数是类型,第二个参数是长度、容量等初始化参数。make()会初始化数据结构,并根据需要分配内存。
例子
slice := make([]int , 5, 10)
new()
- 用于分配内存,返回的是指向零值的指针。
new()只有一个参数,是类型,返回一个指向该类型的零值的指针。new()只分配内存,不进行初始化,返回的指针指向零值。
例子
ptr := new(int)
map
概述
map,go中的哈希表。
key 是可哈希的内置对象(string、int、float),或者实现了hash()方法的自定义对象。
value可以是任意类型。
性能
map 传递给函数的代价很小:在 32 位机器上占 4 个字节,64 位机器上占 8 个字节,无论实际上存储了多少数据。通过 key 在 map 中寻找值是很快的,比线性查找快得多,但是仍然比从数组和切片的索引中直接读取要慢 100 倍。
创建
map 是 引用类型 的: 内存用 make 方法来分配。
map 的初始化:var map1 = make(map[keytype]valuetype)。
或者简写为:map1 := make(map[keytype]valuetype)。
用法示例
package main
import "fmt"
func main() {
m := make(map[string]int) // create
m["one"] = 1 // insert
m["two"] = 2
fmt.Println(m) // formart printer
fmt.Println(m["one"], m["two"]) // retrieve
fmt.Println(m["unknown"]) // 0
r, ok := m["unknown"]
fmt.Println(r == ok) // false
delete(m, "one")
m2 := map[string]int{"one": 1, "two": 2}
var m3 = map[string]int{"one": 1, "two": 2}
fmt.Println(m2, m3)
// traverse
for item, idx := range m2 {
fmt.Println(item, idx)
}
}
输出
map[one:1 two:2]
1 2
0
false
map[one:1 two:2] map[one:1 two:2]
map 容量
和数组不同,map 可以根据新增的 key-value 对动态的伸缩,因此它不存在固定长度或者最大限制。但是你也可以选择标明 map 的初始容量 capacity,就像这样:make(map[keytype]valuetype, cap)。例如:
map2 := make(map[string]float32, 100)
当 map 增长到容量上限的时候,如果再增加新的 key-value 对,map 的大小会自动加 1。所以出于性能的考虑,对于大的 map 或者会快速扩张的 map,即使只是大概知道容量,也最好先标明。
用切片作为 map 的值
mp1 := make(map[int][]int)
mp2 := make(map[int]*[]int)
处理一个健对应多个值的情况。
遍历
map支持遍历,可以和for-range配合使用,遍历的顺序是随机的。
- 遍历桶:遍历时,Go 首先会遍历
map中的每个桶。遍历的顺序并不是线性的,而是随机的,以避免程序依赖遍历顺序。 - 遍历桶内元素:在每个桶内,Go 遍历存储的键值对。如果一个桶有溢出桶,则继续遍历溢出桶中的键值对。
- 随机化遍历顺序:每次遍历时,Go 会使用一个随机的顺序。这是为了避免程序依赖遍历顺序,确保遍历顺序不会随着键的插入顺序改变而固定。
map的切片
假设我们想获取一个 map 类型的切片,我们必须使用两次 make() 函数,第一次分配切片,第二次分配 切片中每个 map 元素。
这样理解这件事情,比如需要在C++中分配一个vector<int>数组。写法为
vector<int> va[10];
数组部分的内存
std::vector<int> va[10];
这段代码创建了一个长度为 10 的数组 va,其中每个元素是一个 std::vector<int> 类型的对象。这个数组本身是在栈上分配的,因此,va 数组的内存是固定且静态的,大小是 10。
va数组中每个元素(即std::vector<int>)是一个对象,它的内存会在栈上分配,类似于普通的对象,但它是一个 包含指向动态内存的指针的类。
vector<int> 内部的内存管理
std::vector<int> 是一个动态数组,它的内部机制是动态分配内存来存储元素。具体来说,std::vector<int> 存储的数据是通过 堆 动态分配的,而不是直接存储在栈上。因此,虽然 va 数组本身在栈上分配内存,但是每个 std::vector<int> 内部的数据是动态分配的。
再回头看map的切片,也是两次构造的过程。首先需要为切片的底层数组分配内存并且创建切片,再为切片中的每个map分配内存。这里map是个引用类型,这种方式的内存分配方式和`vector是一致的。
package main
import "fmt"
func main() {
// Version A:
items := make([]map[int]int, 5)
for i:= range items {
items[i] = make(map[int]int, 1)
items[i][1] = 2
}
fmt.Printf("Version A: Value of items: %v\n", items)
// Version B: NOT GOOD!
items2 := make([]map[int]int, 5)
for _, item := range items2 {
item = make(map[int]int, 1) // item is only a copy of the slice element.
item[1] = 2 // This 'item' will be lost on the next iteration.
}
fmt.Printf("Version B: Value of items: %v\n", items2)
输出
Version A: Value of items: [map[1:2] map[1:2] map[1:2] map[1:2] map[1:2]]
Version B: Value of items: [map[] map[] map[] map[] map[]]
这里range 函数的特性是返回元素的拷贝,所以是创建行为B是不成功的。
map的排序
map 默认是无序的,不管是按照 key 还是按照 value 默认都不排序。
如果想为 map 排序,需要将 key 拷贝到一个切片,再对切片排序。