在很多情况下,我们需要保存大量的数据,或者说保存一组数据,单纯地使用普通的数据类型不再能满足需求。Go语言提供了内置的四种常用数据结构:数组、切片(slice)、字典(map)、列表(list),用来保存一组数据。

1. 数组 (array)

数组是一个由固定长度的特定类型元素组成的序列,一个数组可以由零个或多个元素组成,数组元素可以通过索引(位置)来读取(或者修改),索引从 0 开始,第一个元素索引为 0,第二个索引为 1,以此类推,数组一档定义后,长度不能更改。
因为数组的长度是固定的,所以在Go语言中很少直接使用数组,一般都是使用切片来代替数组, 切片(slice)是可以增长和收缩的动态序列,功能也更灵活,要理解切片工作原理的话需要先理解数组。

1.1. 数组的声明

语法格式:var name [size]type

  • var:定义数组变量使用的关键字。
  • name:数组声明及使用时的变量名。
  • size:数组的元素数量(数组长度),可以是一个表达式,但是一定要在编译时就能获得确定值,不能含有到运行时才能确认大小的数值。
  • type:可以是任意基本类型,包括数组本身,类型为数组本身时,可以实现多维数组。

示例:

  1. package main
  2. import "fmt"
  3. func main() {
  4. var stringarr [2]string
  5. stringarr[0] = "hello"
  6. stringarr[1] = "Golang"
  7. fmt.Println("stringarr = ", stringarr)
  8. }
  9. //stringarr = [hello Golang]

1.2. 初始化数组

  1. var arr = [3]int{1, 2, 3}
  2. //或
  3. arr := [3]int{1, 2, 3}

如果数组长度不确定,可以使用...代替数组长度,编译器会自动根据元素个数确定数组的长度:

  1. var arr = [...]int{1, 2, 3, 4, 5}
  2. //或
  3. arr := [...]int{1, 2, 3, 4, 5}

如果指定了数组长度,还可以对指定位置的元素进行初始化:

  1. func main() {
  2. arr := [5]float64{0: 1.0, 4: 5.0}
  3. fmt.Println("arr = ", arr)
  4. }
  5. //arr = [1 0 0 0 5]

1.3. 访问数组元素

使用下标索引的方式,即可对数组进行访问或者修改指定的数组元素的值:

  1. func main() {
  2. arr := [2]string{"Hello", "Golang!"}
  3. fmt.Println("arr[0] = ", arr[0])
  4. fmt.Println("arr[1] = ", arr[1])
  5. }
  6. //arr[0] = Hello
  7. //arr[1] = Golang!

1.4. 获取数组的长度

将数组作为参数传递给len()函数,可以获得数组的长度:

  1. func main() {
  2. arr := [2]string{"Hello", "Golang!"}
  3. fmt.Println("arr[0] = ", arr[0])
  4. fmt.Println("arr[1] = ", arr[1])
  5. fmt.Println("数组长度为:", len(arr))
  6. }
  7. //arr[0] = Hello
  8. //arr[1] = Golang!
  9. //数组长度为: 2

1.5. 数组比较

Go语言的数组比较,是使用 == 的方式,如果数组的元素个数不相同或者元素类型不相同,那么不能比较数组。
在两个数组类型相同(包括数组的长度,数组中元素的类型)的情况下,可以直接通过较运算符(==和!=)来判断两个数组是否相等,只有当两个数组的所有元素都是相等的时候数组才是相等的,不能比较两个类型不同的数组,否则程序将无法完成编译。

  1. func main() {
  2. a := [2]int{1, 2}
  3. b := [...]int{1, 2}
  4. c := [3]int{1, 2, 3}
  5. fmt.Println(a == b) // true
  6. fmt.Println(a == c) // invalid operation: a == c (mismatched types [2]int and [3]int)
  7. }

1.6. 多维数组

Go同样支持多维数组,声明多维数组的语法格式为:
var name [size1][size2]...[sizen]type

  1. // 声明一个4行2列的数组
  2. var array [4][2]int
  3. // 声明并初始化一个4行2列的数组
  4. array1 := [4][2]int{{1, 1}, {2, 2}, {3, 3}, {4, 4}}
  5. // 三维数组
  6. array2 := [3][3]int{
  7. {0, 1, 2},
  8. {3, 4, 5},
  9. {6, 7, 8}
  10. }

1.7. 数组是值类型

Go中的数组是值类型,而不是引用类型。当对数组进行传递时,传递的是原始数组的副本。比如:将数组a传递给变量b,通过变量b对数组进行更改,原数组并不受影响。

  1. func main() {
  2. a := [3]int{1, 2, 3}
  3. b := a
  4. b[0] = 4
  5. b[1] = 5
  6. b[2] = 6
  7. fmt.Println(a == b)
  8. fmt.Println(a)
  9. fmt.Println(b)
  10. }
  11. //false
  12. //[1 2 3]
  13. //[4 5 6]

同样地,在把数组作为参数传递给函数时,依然是值传递,而原始数组不受影响。

2. 切片 (slice)

Go 数组的长度固定不可改变,在特定场景中这样的集合就不太适用,Go 中提供了一种灵活,功能强悍的内置类型切片(动态数组),与数组相比切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大。切片是数组的一个引用,因此切片是引用类型,在进行传递时,遵守引用的传递机制。
切片数据结构包含 Go 语言需要操作底层数组的元数据,分别是指向底层数组的指针、切片访问的元素的个数len(即长度)和切片允许增长到的元素个数cap(即容量)。
Go语言容器 (Container) - 图1

2.1. 切片的声明

切片的声明有两种方式:

  1. 通过声明一个未指定大小的数组来定义切片:var name []type,只需要声明切片的类型而不需要声明长度。
  2. 使用make()函数创建切片:var name []type = make([]type, len),可以简写为name := make([]type, len),也可以为切片指定容量name := make([]type, len, capacity),len为切片的当前长度,capacity是可选参数,在不声明capacity的情况下,默认capacity = len。

    使用 make() 函数生成的切片一定发生了内存分配操作,但给定开始与结束位置(包括切片复位)的切片只是将新的切片结构指向已经分配好的内存区域,设定开始与结束位置,不会发生内存分配操作。

2.2. 初始化切片

切片在未初始化时,默认为nil,长度为0,一般有如下两种形式对切片进行初始化:

  1. 声明且初始化切片

s := []int{1, 2, 3}[]表示是切片类型,初始值为1, 2, 3,其中capacity = len = 3

  1. 使用数组初始化切片

切片默认指向一段连续内存区域,可以是数组,也可以是切片本身。从连续内存区域生成切片是常见的操作,格式如下:slice := arr[startIndex : endIndex],不包含结束位置的元素。

  1. func main() {
  2. // 数组a
  3. a := [5]int{1, 2, 3, 4, 5}
  4. // 切片截取
  5. b := a[1:2]
  6. c := a[:5]
  7. d := a[1:]
  8. e := a[:]
  9. f := a[0:0]
  10. fmt.Println("a[1:2] = ", b)
  11. fmt.Println("a[ :5] = ", c)
  12. fmt.Println("a[1: ] = ", d)
  13. fmt.Println("a[ : ] = ", e)
  14. fmt.Println("a[0:0] = ", f)
  15. }
  16. //a[1:2] = [2]
  17. //a[ :5] = [1 2 3 4 5]
  18. //a[1: ] = [2 3 4 5]
  19. //a[ : ] = [1 2 3 4 5]
  20. //a[0:0] = []

从数组或切片生成新的切片具有如下特点:

  • 取出的元素数量为:结束位置 - 开始位置,取出元素不包含结束位置对应的索引,切片最后一个元素使用 slice[len(slice)]获取;
  • 当缺省开始位置时,表示从开头到结束位置;
  • 当缺省结束位置时,表示从开始位置到末尾;
  • 两者同时缺省时,与数组本身等效;
  • 两者同时为 0 时,等效于空切片,一般用于切片复位。

    2.3. append()copy()

    切片的长度是切片中元素的数量,切片的容量是切片最大能容纳的元素数量,可以通过len()函数获取切片的长度,通过cap()函数获取切片的容量。除此之外,还可以使用append()向切片追加一个或者多个元素,然后返回一个新的切片。copy()函数将原切片的元素复制到目标切片,并且返回复制的元素的个数。
    append()函数会改变切片所引用的数组,从而影响到引用同一数组的其它切片。 当切片中没有剩余空间,即cap-len == 0时,此时将动态分配新的数组空间进行扩容,切片在扩容时,容量的扩展是按原切片容量的 2 倍数进行扩充,返回的切片指针将指向这个空间,而原数组的内容将保持不变;其它引用此数组的切片则不受影响。 ```go package main

import “fmt”

func printSlice(x []int) { fmt.Printf(“len = %d cap = %d slice = %v\n”, len(x), cap(x), x) } func main() { var s []int printSlice(s) // 允许追加空切片 s = append(s, 0) printSlice(s)

  1. // 向切片添加一个元素
  2. s = append(s, 1)
  3. printSlice(s)
  4. // 同时添加多个元素
  5. s = append(s, 2, 3, 4)
  6. printSlice(s)
  7. // 创建切片s1,是s容量的两倍
  8. s1 := make([]int, len(s), (cap(s))*2)
  9. // 拷贝s的内容到s1
  10. copy(s1, s)
  11. printSlice(s1)

}

  1. ![image.png](https://cdn.nlark.com/yuque/0/2022/png/22161775/1648456983776-99dc3998-4e05-4a1a-a2f6-c108c375c2a9.png#clientId=ue32c8217-3d6c-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=111&id=GRnLi&margin=%5Bobject%20Object%5D&name=image.png&originHeight=111&originWidth=701&originalType=binary&ratio=1&rotation=0&showTitle=false&size=12852&status=done&style=none&taskId=u7405f20d-37a2-47f4-a2ab-46dc1724963&title=&width=701)
  2. > 注:`copy()`方法是进行值复制,切片s1与切片s两者不存在联系,切片s发生变化时,s1不会随着变化。
  3. 在上述的示例代码中,向切片添加元素都是添加到尾部,其实可以使用`append()`函数加上切片索引的形式实现在切片的任意位置插入元素。
  4. ```go
  5. package main
  6. import "fmt"
  7. func main() {
  8. var s = []string{"a", "c"}
  9. // 在index处添加一个元素
  10. s = append(s[:1], append([]string{"b"}, s[1:]...)...)
  11. fmt.Println(s)
  12. // 在头部插入元素
  13. s = append([]string{"0"}, s...)
  14. fmt.Println(s)
  15. // 在index处插入切片
  16. var newSlice = []string{" ", " ", " "}
  17. s = append(s[:1], append(newSlice, s[1:]...)...)
  18. fmt.Println(s)
  19. }
  20. //[a b c]
  21. //[0 a b c]
  22. //[0 a b c]

在上述代码中,s[:1]返回的是切片的第一个元素,s[1:]返回的是从二个元开始的整个切片,切片截取搭配上append()函数可以实现非常灵活的操作。

注:在向可变参数的函数中传递切片时,需要在切片后面加上…

2.4. 删除切片元素

切片是一个引用类型(类似于一个指针),本身不保存数据,对切片做的任何修改都将反映到它所指向的底层数组。切片是引用类型,只能与 nil判定相等,不能互相判定相等。

切片和C语言指针类似,指针可以做运算偏移,但可能造成内存操作越界,切片在指针的基础上增加了大小,约束了切片对应的内存区域,切片使用中无法对切片内部的地址和大小进行手动调整,因此切片比指针更安全、强大。

  1. func main() {
  2. a := [5]int{1, 2, 3, 4, 5}// 数组a
  3. var b []int // 声明一个切片b
  4. if (b == nil) {
  5. fmt.Println("切片为空...")
  6. }
  7. b = a[:]
  8. for i := range b {
  9. b[i]++
  10. }
  11. fmt.Println("a[1:2] = ", a)
  12. }
  13. //切片为空...
  14. //a = [2 3 4 5 6]

Go语言并没有对删除切片元素提供专用的语法或者接口,需要使用切片本身的特性来删除元素,根据要删除元素的位置有三种情况,分别是在开头位置删除、在中间位置删除和在尾部删除,其中删除切片尾部的元素速度最快。
在开头位置删除

  1. // 删除开头的元素可以直接移动数据指针
  2. a = []int{1, 2, 3}
  3. a = a[1:] // 删除开头1个元素
  4. a = a[N:] // 删除开头N个元素
  5. // 也可以不移动数据指针,但是将后面的数据向开头移动,可以用append原地完成
  6. a = []int{1, 2, 3}
  7. a = append(a[:0], a[1:]...) // 删除开头1个元素
  8. a = append(a[:0], a[N:]...) // 删除开头N个元素
  9. // 还可以用copy()函数来删除开头的元素
  10. a = []int{1, 2, 3}
  11. a = a[:copy(a, a[1:])] // 删除开头1个元素
  12. a = a[:copy(a, a[N:])] // 删除开头N个元素

在中间位置删除

  1. // 删除中间的元素,需要对剩余的元素进行一次整体挪动,同样可以用append或copy原地完成
  2. a = []int{1, 2, 3, ...}
  3. a = append(a[:i], a[i+1:]...) // 删除中间1个元素
  4. a = append(a[:i], a[i+N:]...) // 删除中间N个元素
  5. a = a[:i+copy(a[i:], a[i+1:])] // 删除中间1个元素
  6. a = a[:i+copy(a[i:], a[i+N:])] // 删除中间N个元素

在尾部位置删除

  1. a = []int{1, 2, 3}
  2. a = a[:len(a)-1] // 删除尾部1个元素
  3. a = a[:len(a)-N] // 删除尾部N个元素

示例:删除切片指定位置的元素。

  1. package main
  2. import "fmt"
  3. func main() {
  4. seq := []string{"a", "b", "c", "d", "e"}
  5. // 指定删除位置
  6. index := 2
  7. // 查看删除位置之前的元素和之后的元素
  8. fmt.Println(seq[:index], seq[index+1:])
  9. // 将删除点前后的元素连接起来
  10. seq = append(seq[:index], seq[index+1:]...)
  11. fmt.Println(seq)
  12. }
  13. //[a b] [d e]
  14. //[a b d e]

切片删除元素的操作过程:
Go语言容器 (Container) - 图2
Go语言中删除切片元素的本质是,以被删除元素为分界点,将前后两个部分的内存重新连接起来。

2.5. 切片扩容

在使用append()函数为切片动态添加元素时,如果空间不足以容纳足够多的元素,切片就会进行“扩容”,此时新切片的长度会发生改变,会在内存中创建一个新的底层数组,切片指向这个新的的底层数组,而不再指向原数组,所以如果发生扩容,对切片的更改并不会影响原数组。切片在扩容时,容量的扩展规律是按原容量的 2 倍数进行扩充。

  1. package main
  2. import "fmt"
  3. func main() {
  4. var arr = [5]int{1, 2, 3, 4, 5}
  5. s1 := arr[:] // 指向arr数组
  6. s2 := arr[:] // 指向arr数组
  7. // 改变s1切片的元素,底层数组改变,s2切片也改变
  8. s1[1] = 0
  9. fmt.Println("arr = ", arr)
  10. fmt.Println("s2 = ", s2)
  11. // 默认len == cap
  12. fmt.Printf("长度:%d, 容量: %d\n", len(s1), cap(s1))
  13. // 向s1切片添加新元素,发生扩容
  14. s1 = append(s1, 6, 7, 8)
  15. fmt.Printf("长度:%d, 容量: %d\n", len(s1), cap(s1))
  16. fmt.Println("arr = ", arr)
  17. fmt.Println(s1)
  18. }

image.png

注:当容量不足时,添加元素会进行扩容,扩容容量为原切片的两倍;扩容会创建一个新的数组,切片指向新数组,原数组并不会发生变化。

往一个切片中不断添加元素扩容的过程,类似于公司搬家,公司发展初期,资金紧张,人员很少,所以只需要很小的房间即可容纳所有的员工,随着业务的拓展和收入的增加就需要扩充工位,但是办公地的大小是固定的,无法改变,因此公司只能选择搬家,每次搬家就需要将所有的人员转移到新的办公点。

  • 员工和工位就是切片中的元素。
  • 办公地就是分配好的内存。
  • 搬家就是重新分配内存。
  • 无论搬多少次家,公司名称始终不会变,代表外部使用切片的变量名不会修改。
  • 由于搬家后地址发生变化,因此内存“地址”也会有修改。

    3. 字典 (map)

    Map 是一种无序的键值对的集合,可以通过 key 来快速对应的 value 数据,key 可以是所有任何可以使用 == 进行比较的数据类型(基本类型),比如数字型、布尔型、字符串类型等,value 可以是任意的类型。可以使用for...range进行迭代,不过,Map 是无序的,我们无法决定它的返回顺序。Map同样是引用类型,长度不固定,可以通过len()函数返回键值对的数量。

    3.1. map声明

    Map的声明有两种方式,可以使用内建函数make()map关键字进行声明:
    1. // 使用map关键字
    2. var name map[keyType]valueType
    3. // 使用make函数
    4. name := make(map[keyType]valueType)

    注:[keyType]valueType 之间允许有空格,未初始化的map的值为nil,不能直接使用和赋值。

var 声明变量使用的关键字
name map 变量的变量名
map 声明 map 变量的关键字
keyType map 的键的类型
valueType map 的值的类型

3.2. 初始化map

  1. 声明同时初始化 ```go package main

import “fmt”

func main() { // 声明+初始化 cityMap := map[int]string{ 1: “广州”, 2: “上海”, 3: “北京”, 4: “杭州”, } // 遍历map for key, value := range cityMap { fmt.Println(key, value) } }

  1. ![image.png](https://cdn.nlark.com/yuque/0/2022/png/22161775/1648519040502-a49cb489-e635-470c-a6e5-1e921b2ef3c7.png#clientId=u7be463cc-c095-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=87&id=ub7a675b5&margin=%5Bobject%20Object%5D&name=image.png&originHeight=87&originWidth=618&originalType=binary&ratio=1&rotation=0&showTitle=false&size=1987&status=done&style=none&taskId=ub3703272-e8f6-42b9-a410-80f11b27959&title=&width=618)
  2. > 注:每次遍历map的返回的结果顺序都是不固定的。
  3. 2. 先声明后初始化
  4. ```go
  5. package main
  6. import "fmt"
  7. func main() {
  8. // 声明一个map
  9. var cityMap map[string]string
  10. // 初始化map
  11. cityMap["广州"] = "小蛮腰"
  12. cityMap["上海"] = "东方明珠"
  13. cityMap["北京"] = "故宫"
  14. cityMap["杭州"] = "西湖"
  15. // 遍历map
  16. for key, value := range cityMap {
  17. fmt.Println(key, value)
  18. }
  19. }
  20. //运行时错误:panic: assignment to entry in nil map

错误原因:map不同于array和基础类型,在声明时会初始化一个默认值,map是引用类型,如果未在声明时进行初始化,默认值是nil,不指向任何内存地址,所以nil map不能赋值,对nil map赋值会导致运行时错误。解决办法:可以在map声明后,通过make()函数为其分配内存地址后再进行赋值。

  1. package main
  2. import "fmt"
  3. func main() {
  4. // 声明一个map,默认值为nil,不能直接赋值
  5. var cityMap map[string]string
  6. // 使用make函数为nil map分配内存
  7. cityMap = make(map[string]string)
  8. // 初始化map
  9. cityMap["广州"] = "小蛮腰"
  10. cityMap["上海"] = "东方明珠"
  11. cityMap["北京"] = "故宫"
  12. cityMap["杭州"] = "西湖"
  13. // 遍历map
  14. for key, value := range cityMap {
  15. fmt.Println(key, value)
  16. }
  17. }

image.png

拓展:同为引用类型的slice,在使用append()nil slice添加元素并不会发生错误,原因在于append()函数底层的扩容机制(详情可参考2.5 切片扩容),append()函数将元素追加到切片的尾部时,如果数组太小无法进行追加,则会分配一个更大容量的数组,slice指向这个新数组。同理,将向nil slice追加元素时,会为nil slice重新分配新的数组,让nil slice指向这个数组的内存地址。 nil map问题官方文档解释如下: This variable m is a map of string keys to int values: var m map[string]int Map types are reference types, like pointers or slices, and so the value of m above is nil; it doesn’t point to an initialized map. A nil map behaves like an empty map when reading, but attempts to write to a nil map will cause a runtime panic; don’t do that. To initialize a map, use the built in make function: m = make(map[string]int) nil slice问题官方文档解释如下: nil map doesn’t point to an initialized map. Assigning value won’t reallocate point address. The append function appends the elements x to the end of the slice s, If the backing array of s is too small to fit all the given values a bigger array will be allocated. The returned slice will point to the newly allocated array.

  1. 使用make()初始化 ```go package main

import “fmt”

func main() { // 使用make声明并分配内存 cityMap := make(map[int]string) // 初始化map cityMap[1] = “北京” cityMap[2] = “上海” cityMap[3] = “广州” cityMap[4] = “深圳” // 遍历map for key, value := range cityMap { fmt.Println(key, value) } }

  1. ![image.png](https://cdn.nlark.com/yuque/0/2022/png/22161775/1648520847399-90481bef-5a10-4a9e-bcb4-719a90a39ed7.png#clientId=u7be463cc-c095-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=86&id=u9036c0f1&margin=%5Bobject%20Object%5D&name=image.png&originHeight=86&originWidth=596&originalType=binary&ratio=1&rotation=0&showTitle=false&size=1929&status=done&style=none&taskId=u62cf8d26-20bf-41d1-be3c-447395faf6a&title=&width=596)
  2. > 注:不能使用`make()`初始化map并同时进行赋值,错误示例如下:
  3. ```go
  4. // 使用make同时声明和为map赋值(错误)
  5. cityMap := make(map[string]string){
  6. "广州": "小蛮腰",
  7. "上海": "东方明珠",
  8. "北京": "故宫",
  9. "杭州": "西湖",
  10. }

总结: map的赋值一共有三种方式:

  1. 在使用map关键字声明时同时进行赋值初始化
  2. 使用map关键字声明后,使用make()函数为其分配内存地址进行初始化,再进行赋值
  3. 使用make()函数同时声明和初始化,再进行赋值

3.3. 特殊类型作为value值

  1. 切片作为value值

正常情况下,keyvalue一一对应,但有些情况下,可能需要一个key对应多个value值,此时可以通过将value定义为切片类型来实现。

  1. package main
  2. import "fmt"
  3. func main() {
  4. hobbyMap := make(map[string][]string)
  5. hobbyMap["张三"] = []string{"唱歌", "跳舞"}
  6. hobbyMap["李四"] = []string{"跑步", "游泳"}
  7. for k, v := range hobbyMap {
  8. fmt.Printf("%s的爱好是:%v\n", k, v)
  9. }
  10. }
  11. //张三的爱好是:[唱歌 跳舞]
  12. //李四的爱好是:[跑步 游泳]
  1. map作为value值

示例:

  • 使用map[string]map[string]sting的map类 型
  • key:表示用户名,是唯一的,不可重复
  • 如果某个用户名存在,就将其密码修改”888888”,如果不存在就增加这个用户信息(包括昵称nickname和密码 pwd)
  • 编写一个函数modifyUser(users map[string]map[string]sting, name string)完成上述功能 ```go package main

import “fmt”

func modifyUser(users map[string]map[string]string, name string) {

  1. if users[name] != nil {
  2. // 用户存在密码改为888888
  3. users[name]["pwd"] = "888888"
  4. } else {
  5. // 用户不存在,添加用户信息
  6. users[name] = make(map[string]string, 2)
  7. users[name]["pwd"] = "888888"
  8. users[name]["nickname"] = "小小" + name //示意
  9. }

}

func main() {

  1. users := make(map[string]map[string]string, 10)
  2. users["张三"] = make(map[string]string, 2)
  3. users["张三"]["nickname"] = "zs"
  4. users["张三"]["pwd"] = "1111111"
  5. modifyUser(users, "李四")
  6. modifyUser(users, "王五")
  7. for k, v := range users {
  8. fmt.Println(k, v)
  9. }

} //李四 map[nickname:小小李四 pwd:888888] //张三 map[nickname:zs pwd:1111111]
//王五 map[nickname:小小王五 pwd:888888]

  1. 3. struct作为value
  2. <a name="cyGjb"></a>
  3. ## 3.4. 增删改查
  4. 1. 增加和更新
  5. `map[key] = value`,如果key不存在于map中,则会对map增加一个键值对;如果key已经存在,则会对mapkey对应的value值进行更新。
  6. 2. 删除
  7. Go语言提供了一个内置函数`delete()`,用于删除容器内的元素,`delete()`函数的语法格式:<br />`delete(mapName, key)`,函数无返回值,当删除不存在的key时不会报错。<br />示例:
  8. ```go
  9. package main
  10. import "fmt"
  11. func main() {
  12. cityMap := make(map[int]string)
  13. cityMap[1] = "北京"
  14. cityMap[2] = "上海"
  15. cityMap[3] = "广州"
  16. cityMap[4] = "深圳"
  17. // 删除元素
  18. delete(cityMap, 1)
  19. for key, value := range cityMap {
  20. fmt.Println(key, value)
  21. }
  22. }

image.png

注:使用delete()每次只能删除一对键值对,Golang并没有提供清空所有元素的方法,如果需要对map进行清空,可以对key进行遍历逐个删除或者map = make(...),则原来的map指向的底层数据结构会被Golang的垃圾回收机制进行回收。

  1. 查找

可以通过key获取map中对应的value值,语法格式为:map[key]。如果key存在,则返回对应的value值;如果key不存在,则返回value类型的默认值,如value的类型为int,当key不存在时,返回的为0,在Golang中操作map,无论key是否存在都不会出现panic或者返回error
但是在很多情况下,需要判断key:value是否存在,为此,Golang通过在使用map[key]时返回第二参数来标识key是否存在,语法格式为:value, ok := map[key],当key存在时,返回的第二参数为true;当key不存在时,返回的第二参数为false。

  1. func main() {
  2. dict := map[string]int{"key1": 1, "key2": 2}
  3. value, ok := dict["key1"]
  4. if ok {
  5. fmt.Printf(value)
  6. } else {
  7. fmt.Println("key1不存在...")
  8. }
  9. }

3.5. map遍历

可以通过for...range对map进行遍历,遍历时,同时返回key和value。
在某些情况下,如果只需要获得value,可以使用下划线_来替代key,示例如下:

  1. package main
  2. import "fmt"
  3. func main() {
  4. dict := map[int]string{
  5. 1: "张三",
  6. 2: "李四",
  7. 3: "王五",
  8. }
  9. // 只遍历value值
  10. for _, value := range dict {
  11. fmt.Println(value)
  12. }
  13. }
  14. //李四
  15. //王五
  16. //张三

在只需要遍历key时,可以使用如下形式:for key := range dict,无需使用匿名变量:

  1. package main
  2. import "fmt"
  3. func main() {
  4. dict := map[int]string{
  5. 1: "张三",
  6. 2: "李四",
  7. 3: "王五",
  8. }
  9. // 只遍历key值
  10. for key := range dict {
  11. fmt.Println(key)
  12. }
  13. }
  14. //1
  15. //2
  16. //3

注:Golang中的map默认是无序的,遍历的结果顺序与填充的顺序无关,可能每次遍历返回的结果都不同,如果需要返回特定顺序的结果,需要进行排序。步骤如下:

  1. 先将map中的key存入切片中
  2. 对切片进行排序
  3. 遍历切片,按照key输出map的value
  1. package main
  2. import (
  3. "fmt"
  4. "sort"
  5. )
  6. func main() {
  7. dict := make(map[int]int)
  8. dict[2] = 2
  9. dict[5] = 8
  10. dict[1] = 10
  11. dict[6] = 1
  12. dict[4] = 9
  13. // 声明一个切片保存map的key
  14. var slice []int
  15. // 将map数据遍历保存到slice中
  16. for key := range dict {
  17. slice = append(slice, key)
  18. }
  19. // 对切片进行排序
  20. sort.Ints(slice)
  21. // 打印切片
  22. fmt.Println(slice)
  23. // 根据排序的key输出value
  24. for _, key := range slice {
  25. fmt.Printf("map[%v] = %v\n", key, dict[key])
  26. }
  27. }

image.png

3.6. map切片

当切片的数据类型为map时,称之为 slice of map,此时map的个数就能动态变化。
示例:使用map切片,map中保存学生的个人信息,包含name和age。

  1. package main
  2. import "fmt"
  3. func main() {
  4. // map切片,放入2个map
  5. slice := make([]map[string]string, 2)
  6. // 增加第一个学生的信息
  7. if slice[0] == nil {
  8. slice[0] = make(map[string]string, 2)
  9. slice[0]["name"] = "张三"
  10. slice[0]["age"] = "18"
  11. }
  12. // 增加第二个学生的信息
  13. if slice[1] == nil {
  14. slice[1] = make(map[string]string, 2)
  15. slice[1]["name"] = "李四"
  16. slice[1]["age"] = "20"
  17. }
  18. fmt.Println(slice)
  19. }
  20. //[map[age:18 name:张三] map[age:20 name:李四]]

3.7. 其他

  1. map是引用类型,遵守引用类型传递机制,一个函数在接收map后,对其进行修改,会直接修改原来的map,或者将一个map变量赋值给另一个变量时,它们都指向同一个底层map,相互之间会产生影响。
  2. map的容量是不固定的,可以动态变化,当向map中增加一个元素时,会自动进行扩容。
  3. map经常使用struct结构体作为value值,可以实现更为复杂的数据存储和管理。

    3.8. sync.Map

    Go语言中map如果在并发读的情况下是线程安全的,如果是在并发写的情况下,则是线程不安全的。Golang 为我们提供了一个 sync.Map 是并发写安全的。
    Golang 中的 map 的 key 和 value 的类型必须是一致的,但 sync.Map 的 key 和 value 不一定是要相同的类型,不同的类型也是支持的。
    此部分将在Go语言并发章节中详细介绍。

    4. 列表 (list)

    列表是一种非连续的存储容器,由多个节点组成,节点通过一些变量记录彼此之间的关系,列表有多种实现方法,如单链表、双链表等。
    在Golang中,列表使用 container/list包来实现,内部的实现原理是双链表,列表能够高效地进行任意位置的元素插入和删除操作。
    Golang 中的列表可以存储任意数据类型的值,列表的初始化有两种方式,分别为:使用 list.New()函数初始化和使用 var 关键字初始化。

    注:列表的元素可以是任意类型,这既带来了便利,也引来一些问题,例如给列表中放入了一个 interface{} 类型的值,取出值后,如果要将 interface{} 转换为其他类型将会发生宕机。

4.1. 列表初始化

  1. 使用list.New()初始化列表

listName := list.New(),通过list.New() 初始化了一个名为 listName 的列表。

  1. 使用 var关键字初始化列表

var listName = list.List,通过 list.List 初始化了一个名为 listName 的列表。
在上面的两种初始化方式中,列表没有限制其内保存成员的类型,即任意类型的成员可以同时存在列表中。列表是非引用类型,和切片、字典不同,列表在声明后可以直接使用,而切片以及字典在声明后不能直接使用,需要经过初始化。

4.2. 增删改查

  1. 获取元素

元素的获取可以使用内置的 Front() 函数获取头结点,使用 Back() 函数获取尾结点,使用 Prev() 获取前一个结点,使用 Next() 获取下一个结点。列表的底层数据结构为双链表,不支持随机访问,只能顺序访问:
列表的遍历
列表的遍历需要配合Front()函数和Next()函数对列表进行遍历访问:

  1. package main
  2. import (
  3. "container/list"
  4. "fmt"
  5. )
  6. func main() {
  7. list := list.New()
  8. // 在列表尾部插入三个元素
  9. list.PushBack(1)
  10. list.PushBack(2)
  11. list.PushBack(3)
  12. // 遍历列表
  13. for i := list.Front(); i != nil; i = i.Next() {
  14. fmt.Println(i.Value)
  15. }
  16. }
  17. //1
  18. //2
  19. //3
  1. 插入元素或链表

Golang的列表元素的插入有四种情景,分别为:在指定元素前插入、在指定元素后插入、在列表头部插入和在列表尾部插入,除了插入元素外,还能将整个列表插入到另一个列表中,分别为:在头部插入列表和在尾部插入列表。以上几种情况对应下面6种方法:

PushBack(v any) *Element 添加元素v到尾部,返回插入的元素
PushBefore(v any) *Element 添加元素v到头部,返回插入的元素
PushBackList(other *List) 在尾部链接一个其他链表
PushFrontList(other *List) 在头部链接一个其他链表
InsertAfter(v any, mark *Element) *Element 在节点mark之后插入元素v,返回插入的元素
InsertBefore(v any, mark *Element) *Element 在节点mark之前插入元素v,返回插入的元素

在头部、尾部插入元素
list双链表支持从队列前方或后方插入元素,前方插入使用PushFront()方法,后方插入使用PushBack()方法,两个方法均返回一个*list.Element的结构。

  1. package main
  2. import (
  3. "container/list"
  4. "fmt"
  5. )
  6. func main() {
  7. list := list.New()
  8. // 列表头部插入元素
  9. list.PushFront(1)
  10. list.PushFront('A') // 对应的数值为65
  11. list.PushFront("3")
  12. // 列表尾部插入元素
  13. list.PushBack([]int{4, 5, 6})
  14. // 遍历列表
  15. for i := list.Front(); i != nil; i = i.Next() {
  16. fmt.Println(i.Value)
  17. }
  18. }
  19. //3
  20. //65
  21. //1
  22. //[4 5 6]

注:当向头部插入元素时,后插入的在列表前面(头插法)。

在指定元素前后进行插入元素
在头部、尾部插入列表

  1. package main
  2. import (
  3. "container/list"
  4. "fmt"
  5. )
  6. func main() {
  7. // 列表1
  8. list1 := list.New()
  9. list1.PushBack(1)
  10. list1.PushBack(2)
  11. list1.PushBack(3)
  12. // 列表2
  13. list2 := list.New()
  14. list2.PushBack(0)
  15. list2.PushBack(0)
  16. list2.PushBack(0)
  17. fmt.Println("在列表1头部插入列表2...")
  18. list1.PushFrontList(list2)
  19. for i := list1.Front(); i != nil; i = i.Next() {
  20. fmt.Print(i.Value)
  21. }
  22. fmt.Println()
  23. fmt.Println("在列表1尾部插入列表2...")
  24. list1.PushBackList(list2)
  25. for i := list1.Front(); i != nil; i = i.Next() {
  26. fmt.Print(i.Value)
  27. }
  28. }
  29. //在列表1头部插入列表2...
  30. //000123
  31. //在列表1尾部插入列表2...
  32. //000123000
  1. 删除元素

Golang 的列表的删除元素使用 remove()函数,删除的元素不能为空,如果为空,会报异常。
Remove(e *Element) any ,该方法表示在列表中删除列表元素e,返回被删除元素的值。

  1. 更新元素

    4.3. 元素移动

    4.4.

    5. 其他

    5.1. Go语言nil

    5.2. makenew关键字的区别