【GoLang 笔记】遍历 map 时的 key 随机化问题及解决方法

简介: 【GoLang 笔记】遍历 map 时的 key 随机化问题及解决方法

之前的一篇笔记曾分析过,Go 的 map 在底层是用 hashmap 实现的。由于高效的 hash 函数肯定不是对 key 做顺序散列的,所以,与其它语言实现的 hashmap 类似,在使用 Go 语言 map 过程中,key-value 的插入顺序与遍历 map 时 key 的访问顺序是不相同的。熟悉 hashmap 的同学对这个情况应该非常清楚。

所以,本文要提到的肯定不是这个,而是一个比较让人惊奇的情况,下面开始说明。

 

1. 通过 range 遍历 map 时,key 的顺序被随机化

在 golang 1.4 版本中,借助关键字 range 对 Go 语言的 map 做遍历访问时,前后两轮遍历访问到的 key 的顺序居然是被随机化的!

这个现象在其它语言中是很少见的,比如 C 语言实现 hashmap 时,通常会用数组(即一段连续的内存空间)来存 key,虽然 key 的分布顺序与插入顺序不一致,但 k-v 数据填充完毕后,整个 hashmap 的 key 的次序是固定的,所以,后续遍历这个 hashmap 时,每轮遍历访问到的 key 的顺序是一致的。

但 Go 语言通过 range 遍历 map 时,确实会对 map 的 key 顺序做随机化。下面是一段简单的验证程序。

// map_range_rand.go
package main
import (
    "fmt"
)
func main() {
    m := make(map[string]string)
    m["hello"] = "echo hello"
    m["world"] = "echo world"
    m["go"] = "echo go"
    m["is"] = "echo is"
    m["cool"] = "echo cool"
    for k, v := range m {
        fmt.Printf("k=%v, v=%v\n", k, v)
    }
}

 

在 go v1.4 环境中,执行 go build map_range_rand.go 完成编译后,运行产出的 2 进制文件,结果如下。 第 1 次运行输出:

$ ./map_range_rand

k=is, v=echo is

k=cool, v=echo cool

k=hello, v=echo hello

k=world, v=echo world

k=go, v=echo go

第 2 次运行输出:

$ ./map_range_rand

k=go, v=echo go

k=is, v=echo is

k=cool, v=echo cool

k=hello, v=echo hello

k=world, v=echo world

第 3 次运行输出:

$ ./map_range_rand

k=hello, v=echo hello

k=world, v=echo world

k=go, v=echo go

k=is, v=echo is

k=cool, v=echo cool

可以很清楚地看到,每次遍历时,key 的顺序都是不同的。

后来在 golang 官方 blog 的文章 Go maps in action 中,确认了这个现象确实存在,而且是 Go 语言的设计者们有意为之,在这篇文章关于 Iteration order 的说明中提到:

When iterating over a map with a range loop, the iteration order is not specified and is not guaranteed to be the same from one iteration to the next. Since Go 1 the runtime randomizes map iteration order, as programmers relied on the stable iteration order of the previous implementation.

看起来是因为大家在使用 Go 的 map 时,可能会在业务逻辑中依赖 map key 的稳定遍历顺序,而 Go 底层实现并不保证这一点。因此,Go 语言索性对 key 次序做随机化,以提醒大家不要依赖 range 遍历返回的 key 次序。

奇怪的是,我在 golang 1.2 环境中编译上面的示例代码后反复运行,输出结果中 key 的次序是非随机化的。

不过,不管如何,这个默认的次序肯定是不能依赖的。

2. 业务依赖 key 次序时,如何解决随机化问题其实 Go maps in action 一文已经给出了解决方法:

If you require a stable iteration order you must maintain a separate data structure that specifies that order.

可见,需要另外维护一个数据结构来保持有序的 key,然后根据有序 key 来遍历 map。

下面是本文对上个例子给出的稳定遍历次序的解决方法。

package main
import (
    "fmt"
    "sort"
)
func main() {
    m := make(map[string]string)
    m["hello"] = "echo hello"
    m["world"] = "echo world"
    m["go"] = "echo go"
    m["is"] = "echo is"
    m["cool"] = "echo cool"
    sorted_keys := make([]string, 0)
    for k, _ := range m {
        sorted_keys = append(sorted_keys, k)
    }
    // sort 'string' key in increasing order
    sort.Strings(sorted_keys)
    for _, k := range sorted_keys {
        fmt.Printf("k=%v, v=%v\n", k, m[k])
    }        
}

上面的示例中,通过引入 sort 对 key 做排序,然后根据有序的 keys 遍历 map 即可保证每次遍历 map 时的 key 顺序是固定的。

$ go build map_range_rand.go

可以验证,每次的执行结果中 key 的次序都是按字典序进行升序排列的:

$ ./map_range_rand

k=cool, v=echo cool

k=go, v=echo go

k=hello, v=echo hello

k=is, v=echo is

k=world, v=echo world

【参考资料】

Go Blog - Go maps in action

相关文章
|
19天前
|
存储 Go 容器
【golang】对键值有顺序要求时,不要使用 map
【golang】对键值有顺序要求时,不要使用 map
43 0
|
19天前
|
存储 Go
Golang底层原理剖析之map
Golang底层原理剖析之map
36 1
|
7月前
|
安全 Cloud Native Go
需要提醒你关于 golang 中 map 使用的几点注意事项
需要提醒你关于 golang 中 map 使用的几点注意事项
|
8月前
|
存储 安全 编译器
Golang 语言中 map 的键值类型选择,它是并发安全的吗?
Golang 语言中 map 的键值类型选择,它是并发安全的吗?
38 0
|
19天前
|
SQL 前端开发 Go
编程笔记 GOLANG基础 001 为什么要学习Go语言
编程笔记 GOLANG基础 001 为什么要学习Go语言
|
19天前
|
Go
golang力扣leetcode 105.从前序与中序遍历序列构造二叉树
golang力扣leetcode 105.从前序与中序遍历序列构造二叉树
37 0
|
19天前
|
存储 编译器 Go
Golang深入浅出之-掌握Go语言Map:初始化、增删查改与遍历
【4月更文挑战第21天】Go语言中的`map`提供快速的键值对操作,包括初始化、增删查改和遍历。初始化时,推荐使用`make()`函数,如`make(map[string]int)`。插入和查询键值对直接通过索引访问,更新则重新赋值。删除键值对需用`delete()`函数,确保键存在。遍历map常用`for range`,注意避免在遍历中修改map。了解这些并避免易错点,能提升代码效率和可读性。
24 1
Golang深入浅出之-掌握Go语言Map:初始化、增删查改与遍历
|
19天前
|
Go 数据安全/隐私保护
第九章 Golang中map
第九章 Golang中map
25 2
|
19天前
|
存储 IDE 编译器
编程笔记 GOLANG基础 005 第一个程序:hello world 使用vscode
编程笔记 GOLANG基础 005 第一个程序:hello world 使用vscode
|
19天前
|
Go 开发工具
编程笔记 GOLANG基础 004 GOLANG常用命令及VSCODE快捷键
编程笔记 GOLANG基础 004 GOLANG常用命令及VSCODE快捷键