跳至主要內容

容器类型

LincZero大约 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:非常更常用,类型例如:[]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)

(详见 “协程” 一章)