Bootstrap

Go学习笔记——复合数据类型

一、关于map的定义和要点

  • map 类型是一个无序的键值对的集合。它有两种类型元素,一类是键(key),另一类是值(value)。在一个 map 中,键是唯一的,在集合中不能有两个相同的键。Go 也是通过这两种元素类型来表示一个 map 类型,要记得这个通用的 map 类型表示:“map[key_type]value_type”

  • map的key类型,必须要支持==和!=操作符,常用的key类型是int,string,自定义的可比较的数据结构等,而函数类型、map 类型自身,以及切片类型是不能作为 map 的 key 类型的!

//声明map类型时,可以是这样(注意短变量类型的声明要在局部范围内)
m1 := map[int][]string{1: []string{"val1_1", "val1_2"}, 3: []string{"val3_1", "val3_2", "val3_3"}, 7: []string{"val7_1"}}
//也可以是这样
m := make(map[int]string)
//或者是这样
mm := map[string]int{
		"key1": 1,
		"key2": 2,
	}
  • map和切片不同的时,切片时”零值可用“的,而map不是,也就是map在定义时,需要设定初始值。通常有两种做法,

a、一是使用复合字面值初始化 map 类型变量(说起来好象有点绕口,其实就是在定义的时候给个值而已),这里需要注意,go为这种定义map变了的方式提供了语法糖,也就是我们在使用字面值初始化map变量时,写法不用那么臃肿~~

//复合字面值定义map变量的书面写法

m1 := map[int][]string{
    1: []string{"val1_1", "val1_2"},
    3: []string{"val3_1", "val3_2", "val3_3"},
    7: []string{"val7_1"},
}

type Position struct { 
    x float64 
    y float64
}

m2 := map[Position]string{
    Position{29.935523, 52.568915}: "school",
    Position{25.352594, 113.304361}: "shopping-mall",
    Position{73.224455, 111.804306}: "hospital",
}

//采用go提供的语法糖后,和变量的定义是一样的,go编译器很聪明,很多时候你完全不用告诉它所有事情
m1 := map[int][]string{
    1: {"val1_1", "val1_2"},
    3: {"val3_1", "val3_2", "val3_3"},
    7: {"val7_1"},
}

type Position struct { 
    x float64 
    y float64
}

m2 := map[Position]string{
    {29.935523, 52.568915}: "school",
    {25.352594, 113.304361}: "shopping-mall",
    {73.224455, 111.804306}: "hospital",
}

b、二是使用内置函数make

m1 := make(map[int]string) // 未指定初始容量
m2 := make(map[int]string, 8) // 指定初始容量为8

不过,map 类型的容量不会受限于它的初始容量值,当其中的键值对数量超过初始容量后,Go 运行时会自动增加 map 类型的容量,保证后续键值对的正常插入。

二、map基本操作

1. 插入、更新键值

这里的注意点和其他语言基本类似,定义好map类型后,只需要把value赋给key就完成了插入操作,需要注意的时,如果map中存在某个key,在此进行赋值,则新的值会替代掉原有的值。所以,在map中插入数据时,不用自己判定是否插入成功,因为结果总是成功的。

m := make(map[int]string)
m[1] = "value1"
m[2] = "value2"
m[3] = "value3"
fmt.Println(m[1]) //value1
m[1] = "value1111"
fmt.Println(m[1]) //value111

2. 获取键值对数量

通过内置函数len来获取,这里需要注意,不能对map类型调用cap函数,这是map和slice的一处不同

mm := map[string]int{
		"key1": 1,
		"key2": 2,
}
mm["key1"] = 11
mm["key3"] = 3
fmt.Println(len(mm)) //3

3. 查找和读取数据

map的数据结构,让其更加广泛的用在查找和读取场合。

查找就是判定某个键值是否存在map中,而读取一般就是读取该键值在map中对应的值

需要注意的时,判定key是否存在map中的时候,需要使用go推荐的“comma ok”用法。因为map不是零值可用类型,也就是当我们从map中随便获取一个不存在的key,也会得到一个零值(比如0,""等),而我们无法判定这个零值是本身的数据,还是因为键值不存在而返回的一个零值。

而“comma ok”用法可以帮我们解决这个问题

//不存在的情况
mm := map[string]int{
		"key1": 1,
		"key2": 2,
}
v, ok := mm["key11"]
if !ok {
  fmt.Println("key11不存在")//输出
}
fmt.Println(v);//0
//存在的情况
mm := map[string]int{
		"key1": 1,
		"key2": 2,
}

v, ok := mm["key1"]
if !ok {
  fmt.Println("key1不存在")//不输出
}
fmt.Println(v);//1

4. 删除数据

通过内置的delete函数来从map中删除数据,这是删除键唯一的方法。

m := map[string]int {
  "key1" : 1,
  "key2" : 2,
}

fmt.Println(m) // map[key1:1 key2:2]
delete(m, "key2") // 删除"key2"
fmt.Println(m) // map[key1:1]

5. 遍历 map 中的键值数据

遍历 map 的键值对只有一种方法,那就是像对待切片那样通过 for range 语句对 map 数据进行遍历


package main
  
import "fmt"

func main() {
    m := map[int]int{
        1: 11,
        2: 12,
        3: 13,
    }

    fmt.Printf("{ ")
    for k, v := range m {
        fmt.Printf("[%d, %d] ", k, v)
    }
    fmt.Printf("}\n")
}

如果不需要键值,则可以使用空“_”标识符来代替


for k, _ := range m { 
  fmt.Printf("[%d] ", k)// 使用k
}

或者更直接一点

for k := range m { 
  fmt.Printf("[%d] ", k)// 使用k
}

同样,如果只关心value则可以这样


for _, v := range mm {
		fmt.Printf("[%d] ", v)
}

注意,只要value的话就不能再直接了哦~

另外,对同一 map 做多次遍历的时候,每次遍历元素的次序都不相同,也就是程序逻辑千万不要依赖遍历 map 所得到的的元素次序。


package main
  
import "fmt"

func doIteration(m map[int]int) {
    fmt.Printf("{ ")
    for k, v := range m {
        fmt.Printf("[%d, %d] ", k, v)
    }
    fmt.Printf("}\n")
}

func main() {
    m := map[int]int{
        1: 11,
        2: 12,
        3: 13,
    }

    for i := 0; i < 3; i++ {
        doIteration(m)
    }
}
//最终三次的输出结果是
//{ [3, 13] [1, 11] [2, 12] }
//{ [1, 11] [2, 12] [3, 13] }
//{ [3, 13] [1, 11] [2, 12] }

三、map 变量的传递开销

map的传递是一个“描述符”,而不是整个数据的拷贝,所以开销固定,也很小

当map 变量被传递到函数或方法内部后,我们在函数内部对 map 类型参数的修改在函数外部也是可见的

package main
  
import "fmt"

func foo(m map[string]int) {
    m["key1"] = 11
    m["key2"] = 12
}

func main() {
    m := map[string]int{
        "key1": 1,
        "key2": 2,
    }
    fmt.Println(m) // map[key1:1 key2:2]  
    foo(m)
    fmt.Println(m) // map[key1:11 key2:12] 
}

四、内部实现和扩容

我先跳过,后续再回头来研究(也在学习这门课同学药根据实际情况丫,我是最近项目多,深入原理会增加心智负担和时间成本)

关于扩容,只记录一个要点,map 中数据元素的 value 位置可能在扩容过程中发生变化,所以 Go 不允许获取 map 中 value 的地址,这个约束是在编译期间就生效的。

五、map与并发

我这里暂时只记录一个结论,那就是map不是线程安全的,对并发不友好

好了,这篇看起来很多,但都是很实用的东西,除了原理部分,整体都比较简单,本文的文字内容和代码案例,一部分仍然是摘自TonyBai老师的,也有一部分是个人总结提炼后的内容和代码。