Go 1.12 相比 Go 1.11 有哪些值得注意的改動(dòng)?
https://go.dev/doc/go1.12
Go 1.12 值得關(guān)注的改動(dòng):
- 平臺(tái)支持與兼容性: Go 1.12 增加了對(duì) linux/arm64 平臺(tái)的 競(jìng)爭(zhēng)檢測(cè)器(race detector) 支持。同時(shí),該版本是最后一個(gè)支持 僅二進(jìn)制包(binary-only packages) 的發(fā)布版本。
- 構(gòu)建緩存: 構(gòu)建緩存(build cache) 在 Go 1.12 中變?yōu)閺?qiáng)制要求,這是邁向棄用 $GOPATH/pkg 的一步。如果設(shè)置環(huán)境變量 GOCACHE=off,那么需要寫(xiě)入緩存的 go 命令將會(huì)執(zhí)行失敗。回顧歷史,$GOPATH/pkg 曾用于存儲(chǔ)預(yù)編譯的包文件(.a 文件)以加速后續(xù)構(gòu)建,但在 Go Modules 模式下,其功能已被更精細(xì)化的構(gòu)建緩存機(jī)制取代,后者默認(rèn)位于用戶緩存目錄(例如 ~/.cache/go-build 或 %LocalAppData%\go-build),存儲(chǔ)的是更細(xì)粒度的編譯單元,與源代碼版本和構(gòu)建參數(shù)關(guān)聯(lián)。
- Go Modules: 當(dāng) GO111MODULE=on 時(shí),go 命令增強(qiáng)了在非模塊目錄下的操作支持。go.mod 文件中的 go 指令明確指定了模塊所使用的 Go 語(yǔ)言版本。模塊下載現(xiàn)在支持并發(fā)執(zhí)行。
- 編譯器工具鏈: 改進(jìn)了 活躍變量分析(live variable analysis) 和函數(shù)內(nèi)聯(lián)(inlining),需要注意這對(duì) finalizer 的執(zhí)行時(shí)機(jī)和 runtime.Callers 的使用方式產(chǎn)生了影響。引入了 -lang 標(biāo)志來(lái)指定語(yǔ)言版本,更新了 ABI 調(diào)用約定,并在 linux/arm64 上默認(rèn)啟用棧幀指針(stack frame pointers)。
- Runtime: 顯著提升了 GC sweep 階段的性能,并更積極地將內(nèi)存釋放回操作系統(tǒng)(Linux 上默認(rèn)使用 MADV_FREE)。定時(shí)器和網(wǎng)絡(luò) deadline 相關(guān)操作性能得到優(yōu)化。
- fmt 包: fmt 包打印 map 時(shí),現(xiàn)在會(huì)按照鍵的排序順序輸出,便于調(diào)試和測(cè)試。排序有明確規(guī)則(例如 nil 最小,數(shù)字/字符串按常規(guī),NaN 特殊處理等),并且修復(fù)了之前 NaN 鍵值顯示為 <nil> 的問(wèn)題。
- reflect 包: 新增了 reflect.MapIter 類型和 Value.MapRange 方法,提供了一種通過(guò)反射按 range 語(yǔ)句語(yǔ)義迭代 map 的方式。
下面是一些值得展開(kāi)的討論:
Go Modules 功能增強(qiáng)
Go 1.12 對(duì) Go Modules 進(jìn)行了一些重要的改進(jìn),主要體現(xiàn)在以下幾個(gè)方面:提升了在模塊外部使用 go 命令的體驗(yàn),go 指令版本控制更明確,并發(fā)下載提高效率,以及 replace 指令解析邏輯的調(diào)整。
模塊外部的模塊感知操作
在 Go 1.11 中,如果你設(shè)置了 GO111MODULE=on 但不在一個(gè)包含 go.mod 文件的目錄及其子目錄中,大部分 go 命令(如 go get, go list)會(huì)報(bào)錯(cuò)或回退到 GOPATH 模式。
Go 1.12 改進(jìn)了這一點(diǎn):即使當(dāng)前目錄沒(méi)有 go.mod 文件,只要設(shè)置了 GO111MODULE=on,像 go get, go list, go mod download 這樣的命令也能正常工作,前提是這些操作不需要根據(jù)當(dāng)前目錄解析相對(duì)導(dǎo)入路徑或修改 go.mod 文件。
這種情況下,go 命令的行為類似于在一個(gè)需求列表初始為空的臨時(shí)模塊中操作。你可以方便地使用 go get 下載一個(gè)二進(jìn)制工具,或者使用 go list -m all 查看某個(gè)模塊的信息,而無(wú)需先 cd 到一個(gè)模塊目錄或創(chuàng)建一個(gè)虛擬的 go.mod 文件。此時(shí),go env GOMOD 會(huì)報(bào)告系統(tǒng)的空設(shè)備路徑(如 Linux/macOS 上的 /dev/null 或 Windows 上的 NUL)。
例如,在一個(gè)全新的、沒(méi)有任何 Go 項(xiàng)目文件的目錄下:
# 確保 Go Modules 開(kāi)啟
export GO111MODULE=on # 或 set GO111MODULE=on on Windows
# 在 Go 1.12+ 中,可以直接運(yùn)行
go get golang.org/x/tools/cmd/goimports@latest
# 查看 GOMOD 變量
go env GOMOD
# 輸出: /dev/null (或 NUL)
這在 Go 1.11 中通常會(huì)失敗或表現(xiàn)不同。這個(gè)改動(dòng)主要帶來(lái)了便利性。
并發(fā)安全的模塊下載
現(xiàn)在,執(zhí)行下載和解壓模塊的 go 命令(如 go get, go mod download, 或構(gòu)建過(guò)程中的隱式下載)是并發(fā)安全的。這意味著多個(gè) go 進(jìn)程可以同時(shí)操作模塊緩存($GOPATH/pkg/mod)而不會(huì)導(dǎo)致數(shù)據(jù)損壞。這對(duì)于 CI/CD 環(huán)境或者本地并行構(gòu)建多個(gè)模塊的場(chǎng)景非常有用,可以提高效率。
需要注意的是,存放模塊緩存($GOPATH/pkg/mod)的文件系統(tǒng)必須支持文件鎖定(file locking)才能保證并發(fā)安全。
go 指令的含義變更
go.mod 文件中的 go 指令(例如 go 1.12)現(xiàn)在有了更明確的含義:它 指定了該模塊內(nèi)的 Go 源代碼文件所使用的 Go 語(yǔ)言版本特性 。
如果 go.mod 文件中沒(méi)有 go 指令,go 工具鏈(比如 go build, go mod tidy)會(huì)自動(dòng)添加一個(gè),版本號(hào)為當(dāng)前使用的 Go 工具鏈版本(例如,用 Go 1.12 執(zhí)行 go mod tidy 會(huì)添加 go 1.12)。
這個(gè)改變會(huì)影響工具鏈的行為:
- 如果一個(gè)模塊的 go.mod 聲明了 go 1.12,而你嘗試用 Go 1.11.0 到 1.11.3 的工具鏈來(lái)構(gòu)建它,并且構(gòu)建因?yàn)槭褂昧?Go 1.12 的新特性而失敗時(shí),go 命令會(huì)報(bào)告一個(gè)錯(cuò)誤,提示版本不匹配。
- 使用 Go 1.11.4 或更高版本,或者 Go 1.11 之前的版本,則不會(huì)因?yàn)檫@個(gè) go 指令本身報(bào)錯(cuò)(但如果代碼確實(shí)用了新版本特性,編譯仍會(huì)失敗)。
- 如果你需要使用 Go 1.12 的工具鏈,但希望生成的 go.mod 兼容舊版本(如 Go 1.11),可以使用 go mod edit -go=1.11 來(lái)手動(dòng)設(shè)置語(yǔ)言版本。
這個(gè)機(jī)制使得模塊可以明確聲明其所需的最低 Go 語(yǔ)言版本,有助于管理項(xiàng)目的兼容性。
replace 指令的查找時(shí)機(jī)
當(dāng) go 命令需要解析一個(gè)導(dǎo)入路徑,但在當(dāng)前活動(dòng)的模塊(主模塊及其依賴)中找不到時(shí),Go 1.12 的行為有所調(diào)整:它現(xiàn)在會(huì) 先嘗試使用主模塊 go.mod 文件中的 replace 指令 來(lái)查找替換,然后再查詢本地模塊緩存和遠(yuǎn)程源(如 proxy.golang.org)。
這意味著 replace 指令的優(yōu)先級(jí)更高了,特別是對(duì)于那些在依賴關(guān)系圖中找不到的模塊。
此外,如果 replace 指令指定了一個(gè)本地路徑但沒(méi)有版本號(hào)(例如 replace example.com/original => ../forked),go 命令會(huì)使用一個(gè)基于零值 time.Time 的偽版本號(hào)(pseudo-version),如 v0.0.0-00010101000000-000000000000。
編譯器改進(jìn)
Go 1.12 的編譯器工具鏈帶來(lái)了一些優(yōu)化和調(diào)整,開(kāi)發(fā)者需要注意其中的一些變化,尤其是與垃圾回收、棧信息和兼容性相關(guān)的部分。
更精確的活躍變量分析與 Finalizer 時(shí)機(jī)
編譯器的 活躍變量分析(live variable analysis) 得到了改進(jìn)。這個(gè)分析過(guò)程用于判斷在程序的某個(gè)點(diǎn),哪些變量將來(lái)可能還會(huì)被用到。分析越精確,編譯器就能越早地識(shí)別出哪些變量已經(jīng)不再“活躍”。
這對(duì) 設(shè)置了 Finalizer 的對(duì)象(使用 runtime.SetFinalizer)有潛在影響。Finalizer 是在對(duì)象變得不可達(dá)(unreachable)并被垃圾收集器回收之前調(diào)用的函數(shù)。由于 Go 1.12 的編譯器能更早地確定對(duì)象不再活躍,這可能導(dǎo)致其對(duì)應(yīng)的 Finalizer 比在舊版本中更早被執(zhí)行。
如果你的程序邏輯依賴于 Finalizer 在某個(gè)較晚的時(shí)間點(diǎn)執(zhí)行(這通常是不推薦的設(shè)計(jì)),你可能會(huì)遇到問(wèn)題。標(biāo)準(zhǔn)的解決方案是,在需要確保對(duì)象(及其關(guān)聯(lián)資源)保持“存活”的代碼點(diǎn)之后,顯式調(diào)用 runtime.KeepAlive(obj)。這會(huì)告訴編譯器:在這個(gè)調(diào)用點(diǎn)之前,obj 必須被認(rèn)為是活躍的,即使后續(xù)代碼沒(méi)有直接使用它。
更積極的函數(shù)內(nèi)聯(lián)與 runtime.Callers
編譯器現(xiàn)在默認(rèn)會(huì)對(duì)更多種類的函數(shù)進(jìn)行 內(nèi)聯(lián)(inlining),包括那些僅僅是調(diào)用另一個(gè)函數(shù)的簡(jiǎn)單包裝函數(shù)。內(nèi)聯(lián)是一種優(yōu)化手段,它將函數(shù)調(diào)用替換為函數(shù)體的實(shí)際代碼,以減少函數(shù)調(diào)用的開(kāi)銷(xiāo)。
雖然內(nèi)聯(lián)通常能提升性能,但它對(duì)依賴棧幀信息的代碼有影響,特別是使用 runtime.Callers 的代碼。runtime.Callers 用于獲取當(dāng)前 goroutine 的調(diào)用棧上的程序計(jì)數(shù)器(Program Counter, PC)。
在舊代碼中,開(kāi)發(fā)者可能直接遍歷 runtime.Callers 返回的 pc 數(shù)組,并使用 runtime.FuncForPC 來(lái)獲取函數(shù)信息。如下所示:
// 舊代碼,在 Go 1.12 中可能丟失內(nèi)聯(lián)函數(shù)的棧幀
var pcs [10]uintptr
n := runtime.Callers(1, pcs[:])
for i := 0; i < n; i++ {
pc := pcs[i]
f := runtime.FuncForPC(pc)
if f != nil {
fmt.Println(f.Name())
}
}
由于 Go 1.12 更積極地內(nèi)聯(lián),如果一個(gè)函數(shù) B 被內(nèi)聯(lián)到了調(diào)用者 A 中,那么 runtime.Callers 返回的 pc 序列里可能就不再包含代表 B 的那個(gè)棧幀的 pc 了。直接遍歷 pc 會(huì)丟失 B 的信息。
正確的做法是使用 runtime.CallersFrames。這個(gè)函數(shù)接收 pc 切片,并返回一個(gè) *runtime.Frames 迭代器。通過(guò)調(diào)用迭代器的 Next() 方法,可以獲取到更完整的棧幀信息(runtime.Frame), 包括那些被內(nèi)聯(lián)的函數(shù) 。
// 新代碼,可以正確處理內(nèi)聯(lián)函數(shù)
var pcs [10]uintptr
n := runtime.Callers(1, pcs[:]) // 獲取程序計(jì)數(shù)器
frames := runtime.CallersFrames(pcs[:n]) // 創(chuàng)建棧幀迭代器
for {
frame, more := frames.Next() // 獲取下一幀
// frame.Function 包含了函數(shù)名,即使是內(nèi)聯(lián)的
fmt.Println(frame.Function)
fmt.Printf(" File: %s, Line: %d\n", frame.File, frame.Line)
if !more { // 如果沒(méi)有更多幀了,退出循環(huán)
break
}
}
因此,如果你依賴 runtime.Callers 來(lái)獲取詳細(xì)的調(diào)用棧信息, 強(qiáng)烈建議遷移到使用 runtime.CallersFrames 。
方法表達(dá)式包裝器不再出現(xiàn)在棧跟蹤中
當(dāng)使用 方法表達(dá)式(method expression),例如 http.HandlerFunc.ServeHTTP,編譯器會(huì)生成一個(gè)包裝函數(shù)(wrapper)。在 Go 1.12 之前,這些由編譯器生成的包裝器會(huì)出現(xiàn)在 runtime.CallersFrames、runtime.Stack 的輸出以及 panic 時(shí)的棧跟蹤信息中。
Go 1.12 改變了這一行為:這些包裝器不再被報(bào)告。這使得棧跟蹤更簡(jiǎn)潔,也與 gccgo 編譯器的行為保持了一致。
如果你的代碼依賴于在棧跟蹤中觀察到這些特定的包裝器幀,你需要調(diào)整代碼。如果需要在 Go 1.11 和 1.12 之間保持兼容,可以將方法表達(dá)式 x.M 替換為等效的函數(shù)字面量 func(...) { x.M(...) },后者不會(huì)生成這種現(xiàn)在被隱藏的特定包裝器。
-lang 編譯器標(biāo)志
編譯器 gc 現(xiàn)在接受一個(gè)新的標(biāo)志 -lang=version,用于指定期望的 Go 語(yǔ)言版本。例如,使用 -lang=go1.8 編譯代碼時(shí),如果代碼中使用了類型別名(type alias,Go 1.9 引入的特性),編譯器會(huì)報(bào)錯(cuò)。
這個(gè)功能有助于確保代碼庫(kù)維持對(duì)特定舊版本 Go 的兼容性。不過(guò)需要注意,對(duì)于 Go 1.12 之前的語(yǔ)言特性,這個(gè)標(biāo)志的強(qiáng)制執(zhí)行可能不是完全一致的。
ABI 調(diào)用約定變更
編譯器工具鏈現(xiàn)在使用不同的 應(yīng)用二進(jìn)制接口(Application Binary Interface, ABI) 約定來(lái)調(diào)用 Go 函數(shù)和匯編函數(shù)。這主要是內(nèi)部實(shí)現(xiàn)細(xì)節(jié)的改變,對(duì)大多數(shù)用戶應(yīng)該是透明的。
一個(gè)可能需要注意的例外情況是:當(dāng)一個(gè)調(diào)用同時(shí)跨越 Go 代碼和匯編代碼,并且這個(gè)調(diào)用還跨越了包的邊界時(shí)。如果鏈接時(shí)遇到類似 “relocation target not defined for ABIInternal (but is defined for ABI0)” 的錯(cuò)誤,這通常表示遇到了 ABI 不匹配的問(wèn)題。可以參考 Go ABI 設(shè)計(jì)文檔的兼容性部分獲取更多信息。
其他改進(jìn)
- 編譯器生成的 DWARF 調(diào)試信息得到了諸多改進(jìn),包括參數(shù)打印和變量位置信息的準(zhǔn)確性。
- 在 linux/arm64 平臺(tái)上,Go 程序現(xiàn)在會(huì)維護(hù)棧幀指針(frame pointers),這有助于 perf 等性能剖析工具更好地工作。這個(gè)功能會(huì)帶來(lái)平均約 3% 的運(yùn)行時(shí)開(kāi)銷(xiāo)。可以通過(guò)設(shè)置 GOEXPERIMENT=noframepointer 來(lái)構(gòu)建不帶幀指針的工具鏈。
- 移除了過(guò)時(shí)的 “safe” 編譯器模式(通過(guò) -u gcflag 啟用)。
Runtime 性能與效率提升
Go 1.12 的 Runtime 在垃圾回收 (GC)、內(nèi)存管理和并發(fā)原語(yǔ)方面進(jìn)行了一些重要的性能優(yōu)化。
顯著改進(jìn)的 GC Sweep 性能
Go 的并發(fā)標(biāo)記清掃(Mark-Sweep)垃圾收集器包含標(biāo)記(Mark)和清掃(Sweep)兩個(gè)主要階段。標(biāo)記階段識(shí)別所有存活的對(duì)象,清掃階段回收未被標(biāo)記的內(nèi)存空間。
在 Go 1.12 之前,即使堆中絕大部分對(duì)象都是存活的(即只有少量垃圾需要回收),清掃階段的耗時(shí)有時(shí)也可能與整個(gè)堆的大小相關(guān)。
Go 1.12 顯著提高了當(dāng)大部分堆內(nèi)存保持存活時(shí)的清掃性能 。這意味著,在應(yīng)用程序內(nèi)存使用率很高的情況下,GC 清掃階段的效率更高了。(重點(diǎn))
其主要影響是: 減少了緊隨垃圾回收周期之后的內(nèi)存分配延遲 。當(dāng) GC 剛剛結(jié)束,應(yīng)用開(kāi)始請(qǐng)求新的內(nèi)存時(shí),如果清掃階段更快完成,那么分配器就能更快地獲得可用的內(nèi)存,從而降低分配操作的停頓時(shí)間。這對(duì)于需要低延遲響應(yīng)的應(yīng)用尤其有利。
更積極地將內(nèi)存釋放回操作系統(tǒng)
Go runtime 會(huì)管理一個(gè)內(nèi)存堆,并適時(shí)將不再使用的內(nèi)存歸還給底層操作系統(tǒng)。Go 1.12 在這方面變得 更加積極 。
特別是在響應(yīng)無(wú)法重用現(xiàn)有堆空間的大內(nèi)存分配請(qǐng)求時(shí),runtime 會(huì)更主動(dòng)地嘗試將之前持有但現(xiàn)在空閑的內(nèi)存塊釋放給 OS。
在 Linux 系統(tǒng)上,Go 1.12 runtime 現(xiàn)在默認(rèn)使用 MADV_FREE 系統(tǒng)調(diào)用來(lái)通知內(nèi)核某塊內(nèi)存不再需要。相比之前的 MADV_DONTNEED(Go 1.11 及更早版本的行為),MADV_FREE 通常對(duì) runtime 和內(nèi)核來(lái)說(shuō) 效率更高 。
然而,MADV_FREE 的一個(gè)副作用是:內(nèi)核并不會(huì)立即回收這部分內(nèi)存,而是將其標(biāo)記為“可回收”,等到系統(tǒng)內(nèi)存壓力增大時(shí)才會(huì)真正回收。這可能導(dǎo)致通過(guò) top 或 ps 等工具觀察到的進(jìn)程 常駐內(nèi)存大小(Resident Set Size, RSS) 比使用 MADV_DONTNEED 時(shí) 看起來(lái)更高 。 (重點(diǎn)) 盡管 RSS 數(shù)值可能較高,但這部分內(nèi)存實(shí)際上對(duì) Go runtime 來(lái)說(shuō)是空閑的,并且在需要時(shí)可被內(nèi)核回收給其他進(jìn)程使用。
如果你希望恢復(fù)到 Go 1.11 的行為(即使用 MADV_DONTNEED,讓內(nèi)核立即回收內(nèi)存,RSS 下降更快),可以通過(guò)設(shè)置環(huán)境變量 GODEBUG=madvdontneed=1 來(lái)實(shí)現(xiàn)。
定時(shí)器與 Deadline 性能提升
Go runtime 內(nèi)部用于處理定時(shí)器(time.Timer, time.Ticker)和截止時(shí)間(net.Conn 的 SetDeadline 等)的代碼 性能得到了提升 。
這意味著依賴大量定時(shí)器或頻繁設(shè)置網(wǎng)絡(luò)連接 deadline 的應(yīng)用,在 Go 1.12 下可能會(huì)觀察到更好的性能表現(xiàn)。
其他 Runtime 改進(jìn)
- 內(nèi)存分析(Memory Profiling)的準(zhǔn)確性得到提升,修復(fù)了之前版本中對(duì)大型堆內(nèi)存分配可能存在的重復(fù)計(jì)數(shù)問(wèn)題。
- 棧跟蹤(Tracebacks)、runtime.Caller 和 runtime.Callers 的輸出 不再包含編譯器生成的包初始化函數(shù) 。如果在全局變量的初始化階段發(fā)生 panic 或獲取棧跟蹤,現(xiàn)在會(huì)看到一個(gè)名為 PKG.init.ializers 的函數(shù),而不是具體的內(nèi)部初始化函數(shù)。
- 可以通過(guò)設(shè)置環(huán)境變量 GODEBUG=cpu.extension=off 來(lái)禁用標(biāo)準(zhǔn)庫(kù)和 runtime 中對(duì)可選 CPU 指令集擴(kuò)展(如 AVX 等)的使用(目前在 Windows 上尚不支持)。
reflect 包增強(qiáng):標(biāo)準(zhǔn)的 Map 迭代器
在 Go 1.12 之前,如果想通過(guò) reflect 包來(lái)遍歷一個(gè) map 類型的值,過(guò)程相對(duì)比較繁瑣。通常需要先用 Value.MapKeys() 獲取所有鍵的 reflect.Value 切片,然后遍歷這個(gè)切片,再用 Value.MapIndex(key) 來(lái)獲取每個(gè)鍵對(duì)應(yīng)的值。
Go 1.12 引入了一種更簡(jiǎn)潔、更符合 Go 語(yǔ)言習(xí)慣的方式來(lái)通過(guò)反射遍歷 map。
reflect.MapIter 類型與 Value.MapRange 方法
reflect 包新增了一個(gè) MapIter 類型,它扮演著 map 迭代器的角色。可以通過(guò) reflect.Value 的新方法 MapRange() 來(lái)獲取一個(gè) *MapIter 實(shí)例。
這個(gè) MapIter 的行為 遵循與 Go 語(yǔ)言中 for range 語(yǔ)句遍歷 map 完全相同的語(yǔ)義 :
- 迭代順序是隨機(jī)的。
- 使用 iter.Next() 方法來(lái)將迭代器推進(jìn)到下一個(gè)鍵值對(duì)。如果存在下一個(gè)鍵值對(duì),則返回 true;如果迭代完成,則返回 false。
- 在調(diào)用 iter.Next() 并返回 true 后,可以使用 iter.Key() 獲取當(dāng)前鍵的 reflect.Value,使用 iter.Value() 獲取當(dāng)前值的 reflect.Value。
使用示例
下面是一個(gè)使用 MapRange 遍歷 map 的例子,并與舊方法進(jìn)行了對(duì)比:
package main
import (
"fmt"
"reflect"
)
func main() {
data := map[string]int{"apple": 1, "banana": 2, "cherry": 3}
mapValue := reflect.ValueOf(data)
fmt.Println("使用 reflect.MapRange (Go 1.12+):")
// 獲取 map 迭代器
iter := mapValue.MapRange()
// 循環(huán)迭代
for iter.Next() {
k := iter.Key() // 獲取當(dāng)前鍵
v := iter.Value() // 獲取當(dāng)前值
fmt.Printf(" Key: %v (%s), Value: %v (%s)\n",
k.Interface(), k.Kind(), v.Interface(), v.Kind())
}
fmt.Println("\n使用 reflect.MapKeys (Go 1.11 及更早):")
// 獲取所有鍵
keys := mapValue.MapKeys()
// 遍歷鍵
for _, k := range keys {
v := mapValue.MapIndex(k) // 根據(jù)鍵獲取值
fmt.Printf(" Key: %v (%s), Value: %v (%s)\n",
k.Interface(), k.Kind(), v.Interface(), v.Kind())
}
}
好處
MapRange 和 MapIter 提供了一種更直接、更符合 Go range 習(xí)慣的方式來(lái)處理反射中的 map 迭代,使得代碼更易讀、更簡(jiǎn)潔。它避免了先收集所有鍵再逐個(gè)查找值的兩步過(guò)程。