用 Go map 要注意這 1 個細節,避免依賴他!
最近又有同學問我這個日經話題,想轉他文章時,結果發現我的公眾號竟然沒有發過,因此今天我再嘮叨兩句,好讓大家避開這個 “坑”。
有的小夥伴沒留意過 Go map 輸出、遍歷順序,以為它是穩定的有序的,會在業務程式中直接依賴這個結果集順序,結果栽了個大跟頭,吃了線上 BUG。
有的小夥伴知道是無序的,但卻不知道為什麼,有的卻理解錯誤?
今天透過本文,我們將揭開 for range map
輸出的 “神秘” 面紗,看看它內部實現到底是怎麼樣的,順序到底是怎麼樣?
開始吸魚之路。
前言
例子如下:
func main() {
m := make(map[int32]string)
m[0] = "EDDYCJY1"
m[1] = "EDDYCJY2"
m[2] = "EDDYCJY3"
m[3] = "EDDYCJY4"
m[4] = "EDDYCJY5"
for k, v := range m {
log.Printf("k: %v, v: %v", k, v)
}
}
假設執行這段程式碼,輸出的結果是怎麼樣?是有序,還是無序輸出呢?
k: 3, v: EDDYCJY4
k: 4, v: EDDYCJY5
k: 0, v: EDDYCJY1
k: 1, v: EDDYCJY2
k: 2, v: EDDYCJY3
從輸出結果上來講,是非固定順序輸出的,也就是每次都不一樣。但這是為什麼呢?
首先建議你先自己想想原因。其次我在面試時聽過一些說法。有人說因為是雜湊的所以就是無(亂)序等等說法。當時我是有點 ???
這也是這篇文章出現的原因,希望大家可以一起研討一下,理清這個問題 :)
看一下彙編
...
0x009b 00155 (main.go:11) LEAQ type.map[int32]string(SB), AX
0x00a2 00162 (main.go:11) PCDATA $2, $0
0x00a2 00162 (main.go:11) MOVQ AX, (SP)
0x00a6 00166 (main.go:11) PCDATA $2, $2
0x00a6 00166 (main.go:11) LEAQ ""..autotmp_3+24(SP), AX
0x00ab 00171 (main.go:11) PCDATA $2, $0
0x00ab 00171 (main.go:11) MOVQ AX, 8(SP)
0x00b0 00176 (main.go:11) PCDATA $2, $2
0x00b0 00176 (main.go:11) LEAQ ""..autotmp_2+72(SP), AX
0x00b5 00181 (main.go:11) PCDATA $2, $0
0x00b5 00181 (main.go:11) MOVQ AX, 16(SP)
0x00ba 00186 (main.go:11) CALL runtime.mapiterinit(SB)
0x00bf 00191 (main.go:11) JMP 207
0x00c1 00193 (main.go:11) PCDATA $2, $2
0x00c1 00193 (main.go:11) LEAQ ""..autotmp_2+72(SP), AX
0x00c6 00198 (main.go:11) PCDATA $2, $0
0x00c6 00198 (main.go:11) MOVQ AX, (SP)
0x00ca 00202 (main.go:11) CALL runtime.mapiternext(SB)
0x00cf 00207 (main.go:11) CMPQ ""..autotmp_2+72(SP), $0
0x00d5 00213 (main.go:11) JNE 193
...
我們大致看一下整體過程,重點處理 Go map 迴圈迭代的是兩個 runtime 方法,如下:
runtime.mapiterinit runtime.mapiternext
但你可能會想,明明用的是 for range
進行迴圈迭代,怎麼出現了這兩個函式,怎麼回事?
看一下轉換後
var hiter map_iteration_struct
for mapiterinit(type, range, &hiter); hiter.key != nil; mapiternext(&hiter) {
index_temp = *hiter.key
value_temp = *hiter.val
index = index_temp
value = value_temp
original body
}
實際上編譯器對於 slice 和 map 的迴圈迭代有不同的實現方式,並不是 for
一扔就完事了,還做了一些附加動作進行處理。而上述程式碼就是 for range map
在編譯器展開後的偽實現
看一下原始碼
runtime.mapiterinit
func mapiterinit(t *maptype, h *hmap, it *hiter) {
...
it.t = t
it.h = h
it.B = h.B
it.buckets = h.buckets
if t.bucket.kind&kindNoPointers != 0 {
h.createOverflow()
it.overflow = h.extra.overflow
it.oldoverflow = h.extra.oldoverflow
}
r := uintptr(fastrand())
if h.B > 31-bucketCntBits {
r += uintptr(fastrand()) << 31
}
it.startBucket = r & bucketMask(h.B)
it.offset = uint8(r >> h.B & (bucketCnt - 1))
it.bucket = it.startBucket
...
mapiternext(it)
}
透過對 mapiterinit
方法閱讀,可得知其主要用途是在 map 進行遍歷迭代時進行初始化動作。共有三個形參,用於讀取當前雜湊表的型別資訊、當前雜湊表的儲存資訊和當前遍歷迭代的資料
為什麼
我們們關注到原始碼中 fastrand
的部分,這個方法名,是不是迷之眼熟。沒錯,它是一個生成隨機數的方法。再看看上下文:
...
// decide where to start
r := uintptr(fastrand())
if h.B > 31-bucketCntBits {
r += uintptr(fastrand()) << 31
}
it.startBucket = r & bucketMask(h.B)
it.offset = uint8(r >> h.B & (bucketCnt - 1))
// iterator state
it.bucket = it.startBucket
在這段程式碼中,它生成了隨機數。用於決定從哪裡開始迴圈迭代。更具體的話就是根據隨機數,選擇一個桶位置作為起始點進行遍歷迭代
因此每次重新 for range map
,你見到的結果都是不一樣的。那是因為它的起始位置根本就不固定!
runtime.mapiternext
func mapiternext(it *hiter) {
...
for ; i < bucketCnt; i++ {
...
k := add(unsafe.Pointer(b), dataOffset+uintptr(offi)*uintptr(t.keysize))
v := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+uintptr(offi)*uintptr(t.valuesize))
...
if (b.tophash[offi] != evacuatedX && b.tophash[offi] != evacuatedY) ||
!(t.reflexivekey || alg.equal(k, k)) {
...
it.key = k
it.value = v
} else {
rk, rv := mapaccessK(t, h, k)
if rk == nil {
continue // key has been deleted
}
it.key = rk
it.value = rv
}
it.bucket = bucket
if it.bptr != b {
it.bptr = b
}
it.i = i + 1
it.checkBucket = checkBucket
return
}
b = b.overflow(t)
i = 0
goto next
}
在上小節中,我們們已經選定了起始桶的位置。接下來就是透過 mapiternext
進行具體的迴圈遍歷動作。該方法主要涉及如下:
從已選定的桶中開始進行遍歷,尋找桶中的下一個元素進行處理 如果桶已經遍歷完,則對溢位桶 overflow buckets
進行遍歷處理
透過對本方法的閱讀,可得知其對 buckets 的遍歷規則以及對於擴容的一些處理(這不是本文重點。因此沒有具體展開)
總結
在本文開始,我們們先提出核心討論點:“為什麼 Go map 遍歷輸出是不固定順序?”。
經過這一番分析,原因也很簡單明瞭。就是 for range map
在開始處理迴圈邏輯的時候,就做了隨機播種...
你想問為什麼要這麼做?
當然是官方有意為之,因為 Go 在早期(1.0)的時候,雖是穩定迭代的,但從結果來講,其實是無法保證每個 Go 版本迭代遍歷規則都是一樣的。而這將會導致可移植性問題。
因此,改之。也請不要依賴...
參考
Go maps in action
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70024420/viewspace-2927537/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 跨團隊溝通:避免依賴 - pd
- 為什麼 Go 用起來會難受?這 6 個細節你知道嗎Go
- go 中的迴圈依賴Go
- Go 官方依賴注入工具wireGo依賴注入
- 1.go環境安裝,IDE配置以及依賴管理GoIDE
- 電商選品需要注意的8個細節
- 用例依賴管理
- 這些Java程式碼最佳化細節,你需要注意!Java
- 圖解SparkStreaming與Kafka的整合,這些細節大家要注意!圖解SparkKafka
- Go 開發時要了解的 1 個記憶體模型細節Go記憶體模型
- go語言依賴注入實現Go依賴注入
- Java面試要注意哪些細節Java面試
- 節前超級乾貨福利放送!這可能是最實用的 Conan 管理依賴貼NaN
- 利用 uber-go/dig 庫管理依賴Go
- 程式設計師面試 IT 公司,這些細節一定要注意!程式設計師面試
- SQL如何實現查詢節點依賴SQL
- 「轉」Laravel 依賴注入原理(詳細註釋)Laravel依賴注入
- spring 詳細講解(ioc,依賴注入,aop)Spring依賴注入
- Go中使用Google Wire實現依賴注入Go依賴注入
- 依賴管理和依賴範圍
- GO 變數使用細節Go變數
- 簡單分析Go語言中陣列的這些細節Go陣列
- 依賴
- Spring【依賴注入】就是這麼簡單Spring依賴注入
- go語言go get 匯入官方依賴的解決方法Go
- 半導體“依賴症”:世界依賴中國、中國依賴世界
- 這幾個關於Spring 依賴注入的問題你清楚嗎?Spring依賴注入
- 這個Maven依賴的問題,你敢說你沒遇到過Maven
- 有了Git這個功能,再也不需要依賴IDE了!GitIDE
- MYSQL索引建立需要注意以下幾點細節MySql索引
- 關於使用vector時需要注意的細節
- Maven依賴管理:控制依賴的傳遞Maven
- Go etcd 的依賴問題終於解決了。。。Go
- 搞定Go單元測試(四)—— 依賴注入框架(wire)Go依賴注入框架
- 13-angular 思考和分析 依賴注入 -1Angular依賴注入
- 【webpack進階】使用babel避免webpack編譯執行時模組依賴WebBabel編譯
- Angular 應用級別的依賴 FakeAngular
- 原生應用新增 Flutter 模組依賴Flutter