书接上回《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,
}
复制代码
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 不是线程安全的,对并发不友好
好了,这篇看起来很多,但都是很实用的东西,除了原理部分,整体都比较简单。
评论