【注意】最后更新于 July 12, 2023,文中内容可能已过时,请谨慎使用。
1.定义
map是一种集合,可以像遍历数组或切片那样去遍历它。因为map是由Hash表实现的,所以对map的读取顺序不固定。
1.1 注意事项
- map是无序的,每次打印出来的map都会不一样,它不能通过index获取,而必须通过key获取。返回顺序未知,因此每次打印的顺序可能不一样。
map
的长度是不固定的,和切片一样可以扩展。内置的len()函数同样适用于map,返回map拥有的键值对的数量。
- 同一个
map
中key
必须保证唯一。
key
的数据类型必须是可参与比较运算的类型,也就是支持==或!=操作的类型。
- 引用类型则不能作为key的数据类型。
- map的value可以是任何数据类型。
- map和切片一样,也是一种引用类型。
- map是非并发安全的。
2. 声明语法
可以使用var
关键字来定义map,也可以使用内建函数make()
。
使用var关键字声明map,未初始化的map的默认值是nil,不能存放键值对。如果要使用map存储键值对,必须在声明时初始化,或者使用make()函数分配到内存空间。
2.1 声明初始化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
package main
import "fmt"
func main() {
// 声明的同时初始化
var ageMap = map[string]int {
"张三": 20,
"李四": 23,
"王五": 33,
}
fmt.Printf("变量ageMap--> 值: %v 类型: %T \n",ageMap,ageMap)
// 短变量声明初始化
ageMap2 := map[string]int{"张三": 20, "李四": 23, "王五": 33,}
fmt.Printf("变量ageMap2--> 值: %v 类型: %T",ageMap2,ageMap2)
}
/**输出
变量ageMap--> 值: map[张三:20 李四:23 王五:33] 类型: map[string]int
变量ageMap2--> 值: map[张三:20 李四:23 王五:33] 类型: map[string]int
*/
|
2.2 使用make
1
2
3
4
5
6
7
8
9
10
11
|
package main
import "fmt"
func main() {
// 先创建后赋值
ageMap := make(map[string]int)
ageMap["张三"] = 20
ageMap["李四"] = 23
ageMap["王五"] = 23
fmt.Printf("变量ageMap-- 值: %v 类型: %T",ageMap,ageMap)
}
// 输出: 变量ageMap--> 值: map[张三:20 李四:23 王五:23] 类型: map[string]int
|
3.遍历Map
3.1 遍历基础map
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
package main
import "fmt"
func main() {
// 短变量声明初始化
ageMap := map[string]int{"张三": 20, "李四": 23, "王五": 33,}
// 遍历
for k, v := range ageMap {
fmt.Printf("k-> 值: %v v--> %v \n",k,v)
}
}
/**输出
姓名: 张三 年龄: 20
姓名: 李四 年龄: 23
姓名: 王五 年龄: 33
*/
|
3.2 遍历嵌套map
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
package main
import "fmt"
func main() {
// 声明嵌套map
productMap := map[string]map[string]float32{
"水果": {"香蕉": 3.22, "苹果": 1.88, "葡萄": 2.49},
"家具": {"桌子": 66.00, "凳子": 12.00, "沙发": 999.00},
"手机": {"小米10": 3813.00, "华为P40": 5781.00, "iphone12": 7823.00},
}
// 遍历
for class, product := range productMap {
for name, price := range product {
fmt.Printf("分类: %s 产品:%s 价格: %0.2f \n",class,name,price)
}
}
}
/**输出
分类: 水果 产品:苹果 价格: 1.88
分类: 水果 产品:葡萄 价格: 2.49
分类: 水果 产品:香蕉 价格: 3.22
分类: 家具 产品:桌子 价格: 66.00
分类: 家具 产品:凳子 价格: 12.00
分类: 家具 产品:沙发 价格: 999.00
分类: 手机 产品:小米10 价格: 3813.00
分类: 手机 产品:华为P40 价格: 5781.00
分类: 手机 产品:iphone12 价格: 7823.00
*/
|
4.操作map
4.1 判断key是否存在
通过value, ok := map[key]获知key/value是否存在
。ok是bool型,如果ok是true,则该键值对存在,否则不存在。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
package main
import "fmt"
func main() {
// 声明map
fruitMap := map[string]float32{"香蕉": 3.22, "苹果": 1.88, "葡萄": 2.49}
// 存在: ok=true 存在: ok=false
price,ok := fruitMap["香蕉"]
if ok {
fmt.Printf("香蕉存在!价格: %.2f \n",price)
}
price2,ok2 := fruitMap["樱桃"]
if ok2 {
fmt.Printf("樱桃存在!价格: %.2f \n",price2)
} else {
fmt.Printf("樱桃不存在! \n")
}
// 简写
if price,ok := fruitMap["苹果"];ok {
fmt.Printf("苹果存在!价格: %.2f \n",price)
}
}
/**输出
香蕉存在!价格: 3.22
樱桃不存在!
苹果存在!价格: 1.88
*/
|
4.2 删除
delete(map, key)
函数用于删除集合的某个元素,参数为map
和其对应的key
。删除函数不返回任何值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
package main
import "fmt"
func main() {
// 声明map
fruitMap := map[string]float32{"香蕉": 3.22, "苹果": 1.88, "葡萄": 2.49,"梨":4.13}
fmt.Printf("删除前-->fruitMap = %v \n",fruitMap)
// 删除苹果
delete(fruitMap,"苹果")
fmt.Printf("删除后-->fruitMap = %v \n",fruitMap)
// 清空map
fruitMap = map[string]float32{}
fmt.Printf("清空后-->fruitMap = %v \n",fruitMap)
}
/**输出
删除前-->fruitMap = map[梨:4.13 苹果:1.88 葡萄:2.49 香蕉:3.22]
删除后-->fruitMap = map[梨:4.13 葡萄:2.49 香蕉:3.22]
清空后-->fruitMap = map[]
*/
|
5.map是引用类型
map与切片相似,都是引用类型。将一个map赋值给一个新的变量时,它们指向同一块内存(底层数据结构)。因此,修改两个变量的内容都能够引起它们所指向的数据发生变化。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
package main
import "fmt"
func main() {
// 声明map
fruitMap := map[string]float32{"香蕉": 3.22, "苹果": 1.88, "葡萄": 2.49,"梨":4.13}
fmt.Printf("调用函数前-->fruitMap = %v \n",fruitMap)
// 调用函数
testMap(fruitMap)
fmt.Printf("调用函数后-->fruitMap = %v \n",fruitMap)
}
// map是引用类型,函数改变他的值外层变量也会变
func testMap(fruitMap map[string]float32){
fruitMap["香蕉"] = 5.99
}
/**输出
调用函数前-->fruitMap = map[梨:4.13 苹果:1.88 葡萄:2.49 香蕉:3.22]
调用函数后-->fruitMap = map[梨:4.13 苹果:1.88 葡萄:2.49 香蕉:5.99]
*/
|
6.map是非线程安全
和其他语言不同的是,map
并不支持并发的读写,map
并发读写是初级开发者经常会犯的错误。下面的示例由于协程并发读写会报错:fatal error:concurrent map read and map write
6.1 错误示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
func ErrorConcurrency1() {
mp := map[string]int{
"a": 10,
"b": 5,
}
// 开启读协程
go func() {
for true {
fmt.Println("a = ", mp["a"])
}
}()
// 开启写协程
go func() {
for true {
mp["b"] = 10
}
}()
time.Sleep(time.Second * 3)
fmt.Println("ok")
}
|
6.2 不支持并发读写原因
Go
语言为什么不支持并发的读写,是一个频繁被提前的问题。我们可以在Go
官方文档中找到答案。
官方文档的解释是:"map不需要从多个Goroutine安全访问,在实际情况下,map可能是一些已经同步的较大数据结构或者计算的一部分。因此,要求所有map操作都互斥将减慢大多数程序的速度,而只会增加少数程序的安全性"。即Go
语言不支持并发读写的原因是保证大多数场景下的查询效率。
6.3 实现线程安全
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
func Concurrency1() {
// 声明读写锁变量
var lock sync.RWMutex
s := make(map[int]int)
// 开启写协程
for i := 0; i < 10; i++ {
go func(i int) {
// 获取写锁
lock.Lock()
s[i] = i
fmt.Printf("设置map第%d个元素是%d \n", i, s[i])
// 释放写锁
lock.Unlock()
}(i)
}
// 开启读协程
for i := 0; i < 10; i++ {
go func(i int) {
// 获取读锁
lock.RLock()
fmt.Printf("map第%d个元素是%d \n", i, s[i])
// 释放读锁
lock.RUnlock()
}(i)
}
time.Sleep(time.Second * 1)
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
func Concurrency2() {
// 声明读写锁变量
var s sync.Map
// 开启写协程
for i := 0; i < 10; i++ {
go func(i int) {
s.Store(i, i)
}(i)
}
// 开启读协程
for i := 0; i < 10; i++ {
go func(i int) {
load, _ := s.Load(i)
fmt.Printf("map第%d个元素是%v \n", i, load)
}(i)
}
time.Sleep(time.Second * 1)
}
|