欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

迭代 Go 地图容器的详细指南

最编程 2024-04-18 15:48:06
...

又见面了!在之前的讨论中,我们谈到了在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都会更新所有的键,然后最后对它们进行迭代。

我使用了GoRoutinesDeferWaitGroups等概念。如果这些术语看起来不熟悉,我建议你在回来之前好好看看。

在这种情况下会发生什么?

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 容器。这提供了对我们的地图容器的同步访问,因此它可以在多线程的情况下安全使用。

希望这篇文章能让你思考什么时候 需要迭代地图,什么时候不需要。直到下一次!