重学Go语言 | Go Map详解

1,405 阅读5分钟

我正在参加「掘金·启航计划」

mapGo语言中另外一种引用数据类型,也是比较常用的数据类型,今天在这篇文章中,我们来讨论一下。

什么是map

map,中文称为映射或者哈希表,其结构如下图所示:

Go语言中,用map[KeyType]ValueType表示一个map,其中,KeyType表示key的数据类型,ValueType表示value的数据类型:

var students map[string]int

map是一个无序的键值对(key-value)集合,其底层数据结构是一个哈希表,通过哈希函数,将key转换为对哈希表中的索引,将value存储到索引对应的位置,在map中查找、删除、查找value的时间复杂度O(1)

map的底层结构示意图:

创建map

由于map是引用数据类型,其底层引用一个哈希表,因此未经初始化(即未分配到内存空间)的map无法直接使用:

var m map[string]int

m["a"] = 1 
//未初始化,报错

初始化map有两种方式:

  • 使用make函数
  • 字面量初始化

make函数

内置函数make可以为map类型的变量分配内存:

m := make(map[string]string)
m["name"] = "test"
m["age"] = "12"

字面量初始化

通过字面量初始化map时,可以给map中的keyvalue赋初始值:

m := map[string]string{
	"name":"test",
	"age":"12",
}

如果不想给map初始化数据,也可以声明一个空的map类型:

m := map[string]string{}

注意,空的map已经初始化好了,只是没有存入值而已,而直接声明一个map变量时,该map变量为nil,这两者不一样

key数据类型限制

map的value可以是Go支持的任意数据类型,而key则所有限制:

key的数据类型必须是可以使用=!=进行比较

所以,key不能是函数、切片、map,因此这些数据类型不能进行比较,另外,而数组和结构体则可以作为map的key,不过,如果数组的元素包含函数、切片、map,则数组不能作为map的key,结构体的字段如果有以上三者,也同样不能作为map的key。

type Test struct {
	ID   string
	Name string
}

//正确
m := map[Test]int{}

type Test struct {
	ID     string
	Name   string
 	scores []int
}

//报错
m := map[Test]int{}

key的值必须是唯一,同一个map中不能相同的两个key

m := map[string]string{
	"name":"小明",
	"age":"18",
	"name":"小张"//报错,不能有相同的key
}

map的特征

  • map是无序的
  • map的key是唯一的
  • map是引用数据类型,因此在使用前必须初始化
  • 函数,切片,map等数据类型不能作为map的key。

map相关操作

初始化好map之后,我们可以对map进行相关操作:

  • 访问或给元素值赋值
  • 遍历map
  • 检测key是否存在
  • 获取map长度
  • 删除map的value。

访问map值

通过key,可以访问map变量中的value:

rank : = map[string]int{
  "PHP":90,
  "Go":99
  "Java":95
}

fmt.Println(rank["PHP"])

如果对应的key不存在,则会返回对应value数据类型的空值,比如value为string,则返回空字符串:

//因为value为int
fmt.Println(rank["Python"]) 
// 输出:0

m := map[string]string{"name":"小张"}

//由于value为string类型
fmt.Println(m["age"]) 
//输出空字符串

判断key是否存在

如果我们想在通过key访问map之前就确定对应的key是否存在,有另外一种写法:

v, ok := m[k]

上面的表达式中,有第二个返回值ok,该值为boolean类型,当key存在时,ok的值为true,v为对应的value;否则为okfalse,v为空值。

v,ok := rank[k]
if ok {
	fmt.Println(v)
}

if v,ok := rank[k];ok{
  fmt.Println(v)
}

不能对map的value进行取址操作

Go的数组和切片允许对元素进行取址操作,但不允许对map的元素进行取址操作:

//对数组元素取址
a := [3]int{1, 2, 3}
fmt.Println(&a[1])

//对切片元素取址
s := []int{1, 2, 3}
fmt.Println(&s[1])

m := map[string]string{
  "test":"test",
}
//对map元素取址,错误
fmt.Println(&m["test"])

为什么Go要限制对map的元素取址呢?

因为Go可以在添加新的键值对时更改键值对的内存位置。Go将在后台执行此操作,以将检索键值对的复杂性保持在恒定水平。因此,地址可能会变得无效,Go宁愿禁止访问一个可能无效的地址。

遍历map

使用for-range语句可以遍历map,获得mapkeyvalue

m := map[string]string{
    "name":"xiaoming",
    "age":"18岁"
}
for k,v := range m{
  fmt.Println(k,v)
}

map的排序

map是无序的,所以每次遍历map输出的顺序都不一样相同

var user = map[string]string{
	"id":   "0001",
	"name": "小张",
	"age":  "18岁",
}
for k, v := range user {
	fmt.Println(k, ":", v)
}
for k, v := range user {
	fmt.Println(k, ":",v)
}

要有序地遍历一个map类型的变量,可以这么做:

order := []string{}
for k, _ := range user {
	order = append(order, k)
}

for _, v := range order {
	fmt.Println(user[v])
}
for _, v := range order {
	fmt.Println(user[v])
}

获取长度

要获取map的长度,同样是用内置的len函数:

var user = map[string]string{
	"id":   "0001",
	"name": "小张",
	"age":  "18岁",
}
fmt.Println(len(user)) 
//输出:3

删除map的value

要删除map的value,可以使用Go内置的delete函数,该函数格式如下:

func delete(m map[KeyType]ValueType, key Type)

该函数的第一个参数是我们要操作的map类型的变量,第二个参数表示要删除哪个key:

m := map[string]int{
  "a":1,
  "b":2
}

fmt.Println(m)
delete(m,"a")
fmt.Println(m)

map的比较

map类型变量之间不能进行比较,map只能与nil进行比较:

var m map[int]string
//判断是否等于nil
if m == nil{
	fmt.Println("m hasn't been initialized")
}

m1 := map[string]string{"name": "小明"}
m2 := map[string]string{"name": "小明"}

//报错
if m1 == m2 {
	fmt.Println("相等")
}

map嵌套

由于mapvalue并没有数据类型的限制,所以value也可以是另一个map类型:

理论上map嵌套map可以一直嵌套下去,但一般不会这么做的。

mm := map[string]map[int]string{
	"a": {1: "test1"},
	"b": {2: "test2"},
	"c": {2: "test3"},
}
fmt.Println(mm)

小结

关于Go的map数据类型已经讲解完了,总结起来主要了以下几点:

  • 什么是map,如何创建map。
  • map中key的限制。
  • map的相关操作:访问,赋值,遍历,判断key是否存在,获取长度等。
  • map的嵌套。