這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
之前的一篇筆記曾分析過,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.gopackage mainimport ( "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 isk=cool, v=echo coolk=hello, v=echo hellok=world, v=echo worldk=go, v=echo go
第2次運行輸出:
$ ./map_range_rand k=go, v=echo gok=is, v=echo isk=cool, v=echo coolk=hello, v=echo hellok=world, v=echo world
第3次運行輸出:
$ ./map_range_rand k=hello, v=echo hellok=world, v=echo worldk=go, v=echo gok=is, v=echo isk=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 mainimport ("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_randk=cool, v=echo coolk=go, v=echo gok=hello, v=echo hellok=is, v=echo isk=world, v=echo world
【參考資料】
Go Blog - Go maps in action
=========================== EOF ==========================