迭代 Go 地图容器的详细指南
又见面了!在之前的讨论中,我们谈到了在Go中使用本地map
容器。在本教程中,我们将通过迭代地图来探索更多关于地图的知识。我们还将简单介绍一下这种情况在多线程环境下的变化,所以请继续阅读,了解更多内容
现在,让我们先了解一下我们通常如何在地图上迭代。通常情况下,如果你想根据你定义的任何条件来更新某个特定地图上的所有条目,可能需要进行这种操作。
对地图容器的迭代
要对Go的map
容器进行迭代,我们可以直接使用for
循环来传递地图中的所有可用键。
为了更好地理解,让我们举一个简单的例子,我们在地图上插入一堆条目并扫描所有的条目:
package main
import (
"fmt"
)
func main() {
// Allocate memory for the map
var myMap = make(map[int]string)
myMap[0] = "test"
myMap[2] = "sample"
myMap[1] = "GoLang is Fun!"
// Iterate over all keys
for key, val := range myMap {
fmt.Printf("Key: %d, Value: %s\n", key, val)
}
}
输出示例
Key: 0, Value: test
Key: 2, Value: sample
Key: 1, Value: GoLang is Fun!
在这里,迭代的顺序并不 依赖于任何排序的标准,所以如果你在这里得到不同的输出,不要感到惊讶!这是因为Golang的。这是因为 Golang 的map
内部使用了 C 语言中的 hashmap,它使用哈希函数来索引一个键。
由于我们的键在这里是整数,所以看起来很好,但是如果你希望在一个map[string]string
,顺序会是随机的。
因此,假设我们需要通过一些排序的标准来迭代这个map
,我们如何做到这一点呢?
迭代排序后的键值
为了以有序的方式遍历键,我们可以事先简单地对键进行排序。但要做到这一点,我们需要额外的空间来存储地图中的键的列表。
我们可以通过以下方式实现这一目的:
package main
import (
"fmt"
"sort"
)
func main() {
// Allocate memory for the map
var myMap = make(map[int]string)
myMap[0] = "test"
myMap[2] = "sample"
myMap[1] = "GoLang is Fun!"
// Let's get a slice of all keys
keySlice := make([]int, 0)
for key, _ := range myMap {
keySlice = append(keySlice , key)
}
// Now sort the slice
sort.Ints(keySlice)
// Iterate over all keys in a sorted order
for _, key := range keySlice {
fmt.Printf("Key: %d, Value: %s\n", key, myMap[key])
}
}
我们首先创建一个所有键的片断,然后使用sort.Ints()
,因为我们的地图是map[int]string
,所以键是整数。
之后,我们可以通过排序后的片断进行迭代,并使用myMap[key]
来获取值。
输出示例
Key: 0, Value: test
Key: 1, Value: GoLang is Fun!
Key: 2, Value: sample
这可以保证以排序的方式给你一个迭代。要玩这个片段,请访问这个链接。
在地图读/写操作的并发情况下会发生什么?
到目前为止,我们一直假设只有一个函数可以访问和修改地图,所以迭代很简单。然而,如果这种迭代需要在多个函数中进行,或者更确切地说,在多个goroutine中进行,那该怎么办?
让我们举个例子来研究一下发生了什么。
这里有一个程序,它使用多个goroutines来访问和修改一个全局map
。所有的goroutines都会更新所有的键,然后最后对它们进行迭代。
我使用了GoRoutines、Defer 和WaitGroups等概念。如果这些术语看起来不熟悉,我建议你在回来之前好好看看。
在这种情况下会发生什么?
package main
import (
"fmt"
"strconv"
"sync"
)
// Let's have a global map variable so that all goroutines can easily access it
var myMap map[int]string
func myGoRoutine(id int, numKeys int, wg *sync.WaitGroup) {
defer wg.Done()
for key, _ := range myMap {
myMap[key] = strconv.Itoa(id)
}
for key, value := range myMap {
fmt.Printf("Goroutine #%d -> Key: %d, Value: %s\n", id, key, value)
}
}
func main() {
// Initially set some values
myMap = make(map[int]string)
myMap[0] = "test"
myMap[2] = "sample"
myMap[1] = "GoLang is Fun!"
// Get the number of keys
numKeys := len(myMap)
var wg sync.WaitGroup
for i := 0; i < 3; i++ {
wg.Add(1)
go myGoRoutine(i, numKeys, &wg)
}
// Blocking wait
wg.Wait()
// Iterate over all keys
for key, value := range myMap {
fmt.Printf("Key: %d, Value: %s\n", key, value)
}
}
这是我得到的结果,尽管这对你来说可能有所不同,因为goroutines的调度是随机的。
Goroutine #2 -> Key: 0, Value: 2
Goroutine #2 -> Key: 2, Value: 0
Goroutine #2 -> Key: 1, Value: 0
Goroutine #1 -> Key: 0, Value: 1
Goroutine #1 -> Key: 2, Value: 1
Goroutine #1 -> Key: 1, Value: 1
Goroutine #0 -> Key: 0, Value: 0
Goroutine #0 -> Key: 2, Value: 1
Goroutine #0 -> Key: 1, Value: 1
Key: 0, Value: 1
Key: 2, Value: 1
Key: 1, Value: 1
如果你观察一下,由于多个goroutines在同一时间进行潜在的读写,这将得到意想不到的输出。例如,Goroutine 0和2的行为是不同的,因为Goroutine 0在Goroutine 2能够从地图上读出之前就已经写到地图上了
这种类型的情况--即多个线程(goroutine)可以访问同一个内存位置进行读写,被称为数据竞赛条件,即使你的开发测试运行正常,在生产中也很难发现。
在这里,我们不能简单地使用一个正常的map
。为了处理这个问题,一种方法是通过互斥(例如mutex
锁)来执行并发约束。另一种公认的做法是使用go的sync.Map
容器。这提供了对我们的地图容器的同步访问,因此它可以在多线程的情况下安全使用。
希望这篇文章能让你思考什么时候 需要迭代地图,什么时候不需要。直到下一次!