我们前面了解过数组,知道数组的元素是通过位置号(索引号)有序排列的,获取元素也是通过索引获取的。而 map 类型与数组相似,也是由元素集合而成。但是 map 的元素不是有序排列的,也没有位置号(索引号),取而代之的是 键名(索引名),也就是要给每一个元素取一个唯一的名字,然后对应一个元素值。这样一个完整元素就是一个键值对形式,这与 PHP 的键值对数组、Python 的字典结构一样。
map 是以键值对形式为元素结构形成的无序集合,且是引用类型。元素的定位通过键名进行的,键名在集合内不可重复,必须唯一。如同日常生活中的对照表或者字典,用一个名字对应一个值,所以称为映射也很形象。map 的元素是可以动态增加的,未初始化的 map 的整体值为 nil。
map 声明的语法主体格式:map [ 键名类型 ] 值类型
● 键名类型:元素名的数据类型。键名可以是 string、int 等任何可以比较的类型(数组、切片、结构体等是不能作为键名的),通常使用 string 类型居多。
● 值类型:元素值的数据类型,可以是任意类型。
// test01 项目的 main 包,文件名 main.go
package main
import (
"fmt"
)
// 主函数,程序入口
func main() {
var m1 map[string]int // 完整格式声明键名是 string 类型、值是 int 类型的 map
var m2 = map[string]int{"one": 11, "two": 22} // 完整格式声明并初始化两个键值对元素 "one": 11, "two": 22
m3 := map[string]int{"one": 11, "two": 22} // 简式声明并初始化两个键值对元素 "one": 11, "two": 22
m4 := make(map[string]int) // 使用 make 函数简式声明键名是 string 类型、值是 int 类型的 map
m5 := make(map[string]int, 8) // 使用 make 函数简式声明并预设容量为 8 个元素
fmt.Println("映射 m1:", m1)
fmt.Println("映射 m2:", m2)
fmt.Println("映射 m3:", m3)
fmt.Println("映射 m4:", m4)
fmt.Println("映射 m5:", m5)
fmt.Println(m1 == nil, m4 == nil)
}
上述代码编译运行结果如下:
映射 m1: map[]
映射 m2: map[one:11 two:22]
映射 m3: map[one:11 two:22]
映射 m4: map[]
映射 m5: map[]
true false
使用 make 函数声明 map 与不使用 make 函数直接声明的区别是,make 函数会直接创建并初始化内存区块,并可以预留键值对元素数量的空间,避免动态增加时频繁触发内存分配而影响性能。在上述代码最后一个打印输出语句就可以看出明显的区别了。
map 是引用类型,会动态增加键值对元素数,也有长度和容量的概念,cap 函数不能用于获取 map 的容量,但 len 函数是可以获取 map 的键值对元素个数的。通过示例理解一下:
// test01 项目的 main 包,文件名 main.go
package main
import (
"fmt"
)
// 主函数,程序入口
func main() {
m1 := map[string]int{"one": 11, "two": 22}
m2 := make(map[string]int, 8)
fmt.Println("映射 m1长度:", len(m1))
fmt.Println("映射 m2长度:", len(m2))
}
上述代码编译运行结果如下:
映射 m1长度: 2
映射 m2长度: 0
虽然不能直接获取 map 的容量看到直观的效果,但根据 切片 章节关于长度和容量的分析,我们可以很快理解 make 函数第2个参数的意义。
其实 map 元素的获取与数组和切片一样,都是方括号里加上索引,只不过 map 的索引不是位置号了,是自定义的键名。而遍历也是如此,数组和切片的遍历方法同样适用于 map,只是注意索引是键名的转变。
// test01 项目的 main 包,文件名 main.go
package main
import (
"fmt"
)
// 主函数,程序入口
func main() {
m := map[string]int{"one": 11, "two": 22, "three": 33}
fmt.Println("修改前输出映射 m 中 one 的值:", m["one"])
m["one"] = 20
fmt.Println("修改后输出映射 m 中 one 的值:", m["one"])
for k, v := range m {
fmt.Println("遍历输出", k, "->", v)
}
}
上述代码编译运行结果如下:
修改前输出映射 m 中 one 的值: 11
修改后输出映射 m 中 one 的值: 20
遍历输出 two -> 22
遍历输出 three -> 33
遍历输出 one -> 20
从输出结果看,可以充分证明 map 是无序的,多次编译运行,发现输出顺序会变化不同。其它方面与切片和数组一样,就不过多描述了。有不熟悉的可以看前面的章节。
直接看示例代码:
// test01 项目的 main 包,文件名 main.go
package main
import (
"fmt"
)
// 主函数,程序入口
func main() {
m := map[string]int{"one": 11, "two": 22}
fmt.Println("声明与初始化后的 m:", m)
// 增加或修改,如果 m 中存在 three 键名,则直接修改,否则增加键 three,值为 56
m["three"] = 56
fmt.Println("增加 three 后的 m:", m)
// 删除键名为 one 的键值对元素
delete(m, "one")
fmt.Println("删除 one 后的 m:", m)
// 查找 m 中键名为 two 的元素,获取其值
a := m["two"]
fmt.Println("变量 a 的值:", a)
}
上述代码编译运行结果如下:
声明与初始化后的 m: map[one:11 two:22]
增加 three 后的 m: map[one:11 three:56 two:22]
删除 one 后的 m: map[three:56 two:22]
变量 a 的值: 22
删除有专用函数 delete(要操作的map, 要删除元素的键名),如果想删除所有元素(清空),那就不要删除,重新 make 一个 map 更高效,垃圾回收会帮你收拾残局。而增加与修改的赋值语句是一模一样的,是增加还是修改,取决于给定的键名在指定 map 中是否存在,如果存在就修改它,不存在就增加它。
特别注意:
● 如果 map 的某个值是引用类型,那不要通过 map 直接修改该引用类型中的数据,应该先修改引用的数据,然后再将整体数据赋值给 map 的这个键。后面会有章节在示例中描述。
● map 不是并发安全的,就是并发使用 map 类型数据时,在并发环境下,只读可以,同时读写会有问题,这种情况下需要使用 sync.Map 来替代,后面会又章节讲述它的应用。
.
.
上一节:Go/Golang语言学习实践[回顾]教程19–详解Go语言复合数据类型之切片 []
下一节:Go/Golang语言学习实践[回顾]教程21–详解Go语言的空值、零值、nil
.