容器类型
大约 9 分钟
容器类型
数组
声明定义、初始化、赋值、使用
与其他语言不同:吐槽,Go这个 []
前置的语法丑得想吐,而且糖多得想吐
package main
import "fmt"
func main() {
// 方式1: 声明定义,并赋值
var scores [5]int // 自动初始化对应类型的零值,这里全0
scores[0] = 95
scores[1] = 91
scores[2] = 39
scores[3] = 60
scores[4] = 21
// 方式2: 上面也可以改写成初始化的形式
var arr1 [3]int = [3]int {3, 6, 9} // 方式2.1
var arr2 = [3]int {3, 6, 9} // 方式2.2
var arr3 = [...]int {3, 6, 9} // 方式2.3
var arr4 = [...]int {2:66, 0:33, 1:99} // 方式2.4
// 求和
sum := 0
for i := 0; i < len(scores); i++ {
sum += scores[i]
}
// 平均数
avg := sum / len(scores)
}
方法
遍历
package main
import "fmt"
func main() {
var scores [5]int // 自动初始化对应类型的零值,这里全0
// 存入成绩
for i := 0; i < len(scores); i++ {
fmt.Printf("录入第%d个学生的成绩", i+1)
fmt.Scanln(&scores[i])
}
// 打印 - for 遍历版
for i := 0; i < len(scores); i++ {
fmt.Printf("第%d个学生的成绩: %d\n", i+1, scores[i])
}
// 打印 - for range 遍历版
for key, value := range scores { // key和value的名字可以换,如果不需要key可以替换成`_`
fmt.Printf("第%d个学生的成绩: %d\n", key+1, value)
}
}
原理
与其他语言不同:
内存分析
和其他语言一样,略
注意事项 - Go数组的长度是类型的一部分
var arr1 = [3]int {3,6,9}
fmt.Printf("数组类型为: %T", arr1) // 打印:[3]int
注意事项 - Go数组属于值类型,默认值传递
func main() {
var arr3 = [3]int {3,6,9}
test1(arr3)
fmt.Println(arr3) // [3,6,9]
}
func test1(arr [3]int) {
arr[0] = 7
}
如有需要使用指针传递
func main() {
var arr3 = [3]int {3,6,9}
test1(&arr3)
fmt.Println(arr3) // [7,6,9]
}
func test1(arr *[3]int) {
(*arr)arr[0] = 7
}
扩展 - 二维数组
和其他语言基本相同,略
切片 (Slice)
与其他语言不同:
- 在其他语言C/C++/Java等中并不常见
- 在Python也有但似乎不是一个独立的类型,最常用的还是是列表
- 在Go中是非常常用的,主要使用的容器类型就是切片而非数组。
声明定义、初始化、赋值、使用
创建方式非常多样灵活
直接创建空切片 (注意:无存值空间)
// 若不初始化,则相当于内部的内容指针为nil
slice := []int
fmt.Println(slice) // 这里会报错
切片不可声明后直接使用 (内容指针nil)。需要让切片引用一个内存 (方式很多):初始化、make创建、数组或切片转切片、切片修改方法等
原因:数组默认初始化的结果是值均0,切片默认初始化的结果是内容指针值为nil
初始化写法
// 区分:`[]`类型表示的是切片,可变长。而`[...]`会自动填数字,有数字的表示的是数组,定长
slice := []int {1, 4, 7} // 其实这种方式本质上也相当于引用了一个数组
list := [...]int {3, 6, 9}
用make创建切片
package main
import "fmt"
func main() {
slice := make([]int, 4, 20) // 类型[]int,长度4,容量20
fmt.Println(slice, len(slice), cap(slice)) // 打印:值[0,0,0,0], 长度4, 容量20
slice[0] = 66
slice[1] = 88
}
从数组创建切片
package main
import "fmt"
func main() {
// 数组
var intArr [6]int = [6]int {3, 6, 9, 1, 4, 7}
fmt.Printf("值: %v", intArr) // 值: [3, 6, 9, 1, 4, 7]
fmt.Printf("6值对应的地址: %p", &intArr[1]) // 0xc308
// 切片
var slice []int = intArr[1:3]
fmt.Println("值: %v, 个数: %v, 容量: %v", // 值 [6, 9], 个数 2, 容量 5; 容量的值比较接近个数的两倍
slice, len(slice), cap(slice))
fmt.Printf("6值对应的地址: %p", &slice[0]) // 0xc308 (同数组下标1的值)
// 语法糖
var slice = arr[0:b] 等同 var slice = arr[:b]
var slice = arr[a:len(arr)] 等同 var slice = arr[a:]
var slice = arr[0:len(arr)] 等同 var slice = arr[:]
}
从切片创建切片
例如
slice2 := slice[1:2]
方法
遍历
都差不多
slice := make([]int, 4, 20)
slice[0] = 66
// 方式1: 普通for
for i:=0; i<len(slice); i++ {
fmt.Print("slice[%v] == %v\n", i, slice[i])
}
// 方式2: for range
for i, v := range slice {
fmt.Printf("slice[%v] == %v\n", i, v)
}
增删改查
// 改
// 切片 -> 切片
slice2 := append(slice, 88, 50) // 追加。底层会创建一个新数组+新切片。也可以赋值回给自己,但此时该切片不再能修改原数组
slice3 := append(slice, slice2...) // ^ 这里三个点类似TypeScript的用法,必须写
slice2 := slice[1:2] // 减少
copy(slice2, slice1) // 拷贝。将slice1数组复制到slice2的数组
// 其他 -> 切片
var slice []int = intArr[1:3] // 数组转切片
原理
(详见 ”数组 vs 切片 vs 其他容器“,本质是一个结构体实现的信息头,共用存值地址,可以用切片修改原数组的值)
数组 vs 切片 vs 其他非键值容器
对比 Go 中各种容器:
Malloc空间
- Go:不太确定,写到这里的时候还没学到,new 函数是类似的作用?但感觉 Go 一般不这样做,用得不多
- 本质
- 长度不可变
- 内存:无额外内存占用
- 实现:依赖系统接口,操作虚拟内存。需要注意不是真的无额外内存占用,而是语言层面上无 (获取不到,系统自动处理)。虚拟内存分配空闲段时头部是有长度等元信息的。以便仅需头指针即可释放内存
数组
- Go:没切片那么常用,类型例如:
[n]int
或[...]int
- 本质
- 长度不可变
- 内存:无额外内存占用
- 实现:非常普通的连续内存,最简单且占用最小的容器
- 与其他语言不同:
- 注意Go数组有几个与其他语言不同的特殊点:
- 基本类型
- 值传递
- 不同长度数组是不同的类型
- 遍历 / len() 的本质:
- C 数组:传递时往往要传递头指针+长度
- C++ 容器:可以直接范围循环 (容器头存储了len信息)
- Go数组:由于我们能用
len()
获取数组长度,自然也能直接遍历。而len()
的本质类似C++的常量表达式,反正是编译时获取的 (毕竟定长,和获取int/byte等其他基本类似的长度没什么区别。所以说 Go 的数组为什么是基本类型)
- 注意Go数组有几个与其他语言不同的特殊点:
切片
Go:非常更常用,类型例如:
[]int
,使用例如:可变数量参数的本质就是切片、r := []rune(str)
遍历经常先转再遍历切片本质
仅元信息/信息头。是在数组类型上的抽象,对数组一个连续片段的引用 (整个数组/子集),提供了一个数组相关的动态窗口
内存:仅信息头,切片类型自身不包含存值内存。
特别的:切片不可声明后直接使用 (内容指针nil)。需要让切片引用一个内存 (方式很多):初始化、make创建、数组或切片转切片、切片修改方法等
实现:除存值内存外,还有信息头,信息头是包含这些内容的结构体:
- 长度(切片长度,等于结束下标减头下标,包括头下标而不包括结束下标对应的值)
- 容量(可以动态改变)
- 内容头指针(直接创建则为nil,需要由数组创建以与原数组共用存值的内存,或用make创建对应内存)
再高级的其他容器
- Go: 还没学到
- 本质
- 往往都有额外占用来存储数据,且地址与值内存分离 (若要支持扩容这一点是必须的)
- 一般即有元信息/信息头,又有存值内存的所有权
映射 Map (键值对)
key、value 可以是:bool、数字、string、指针、channel、前面对应的接口、结构体、数组。不过key通常为int、string(slice、map、function 不可以)
与其他语言不同:
不同语言的键值对类型:
- C++:std::unordered_map (无序)、std::map (有序)
- Java:HashMap (无序)、TreeMap (有序)
- Python <= 3.6:dict (无序)、collections.OrderedDict (有序)
- Python >= 3.7:无 (无序)、dict (有序)
- Go:map (无序)、第三方库或自定义结构 (有序)
底层实现:
- 有序键值对
- 平衡二叉树 (AVL树)
- 平衡二叉树 (红黑树),(Java、C++ 用)
- 排序数组 (不常见,增删慢,查找二分快)
- 不常用
- B/B+树
- 前缀树 (Trie)
- 无序键值对
- 哈希表
- 链表
- 开发寻址表 (Open Addressing Table)
- 通用
- 布隆过滤器 (Bloom Filter)
声明定义、初始化、赋值、使用
和slice一样,直接声明是没分配空间的。
创建方式也是非常多的
直接创建空map (注意:无存值空间)
var 变量名 map[keyType]valueType
// 声明定义
var a map[int]string
初始化写法
c := map[int]string {
2021: "a",
2023: "b",
2024: "c",
}
用make创建map
func main() {
var a map[int]string
a = make(map[int]string, 10) // 可以存10个键值对
a[2012] = "a"
a[2013] = "b"
a[2014] = "c"
fmt.Println(a) // 打印:map[2013:b 2012:a 2014:c]
b := make(map[int]string) // 参数二可以不写自动分配
}
方法
遍历
用 for range 遍历
for k, v := range myMap {
fmt.Printf("key:%v, value:%v \t", k, v)
}
增删改查
// 查
value, bool = myMap["key"] // 查找。value为值, bool为是否返回
mapLen := len(myMap) // 获取长度。键值对的个数
// 增/改
myMap["key"] = "66" // 增改。很简单,如果没有对应的数据就会自动新增
// 删
delete(myMap, "key") // 删除。不存在不报错
myMap = make(...) // 清空。一种方式是遍历删除,另一种方式是make一个新的,让原来的成为垃圾被GC回收
扩展 - 嵌套map
value是map类型 (嵌套key) 的遍历:
a := make(map[string]map[int]string) // 所以说Go不加小括号有时挺难看的
a["班级1"] = make(map[int]string, 3)
a["班级1"]["张三"] = "21"
a["班级1"]["李四"] = "20"
a["班级2"] = make(map[int]string, 3)
a["班级2"]["王五"] = "19"
a["班级2"]["老六"] = "18"
for k1,v1 := range a {
fmt.Println(k1)
for k2,v2 := range v1 {
fmt.Printf("班级 %v, 名字 %v, 年龄 %v\t", k1, k2, v2)
}
}
管道 (Channel)
(详见 “协程” 一章)