Go 1.4 相比 Go 1.3 有哪些值得注意的改動(dòng)?
Go 1.4 值得關(guān)注的改動(dòng):
- for-range 循環(huán)語法更加靈活。在 Go 1.4 之前,即使你只關(guān)心循環(huán)迭代本身,而不使用循環(huán)變量(index/value),也必須顯式地寫一個(gè)變量(通常是空白標(biāo)識(shí)符 _),如 for _ = range x {}。Go 1.4 允許省略循環(huán)變量,可以直接寫成 for range x {}。雖然這種場景不常見,但在需要時(shí)能讓代碼更簡潔。
- 修復(fù)了編譯器允許對指向指針的指針(pointer-to-pointer)類型直接調(diào)用方法的問題。Go 語言規(guī)范允許對指針類型的值進(jìn)行方法調(diào)用時(shí)自動(dòng)插入一次解引用(dereference),但只允許一次。例如,若類型 T 有方法 M(),t 是 *T 類型,則 t.M() 合法。然而,Go 1.4 之前的編譯器錯(cuò)誤地接受了對 **T 類型的變量 x 直接調(diào)用 x.M(),這相當(dāng)于進(jìn)行了兩次解引用,違反了規(guī)范。Go 1.4 禁止了這種調(diào)用,這是一個(gè)破壞性變更(breaking change),但預(yù)計(jì)實(shí)際受影響的代碼非常少。
- 擴(kuò)展了對新操作系統(tǒng)和架構(gòu)的支持。Go 1.4 引入了對在 ARM 處理器上運(yùn)行 Android 操作系統(tǒng)的實(shí)驗(yàn)性支持,可以構(gòu)建 Go 應(yīng)用或供 Android 應(yīng)用調(diào)用的 .so 庫。此外,還增加了對 ARM 上的 Native Client (NaCl) 以及 AMD64 架構(gòu)上的 Plan 9 操作系統(tǒng)的支持。
- Go 運(yùn)行時(shí)(runtime)的大部分實(shí)現(xiàn)從 C 語言遷移到了 Go 語言。這次重構(gòu)使得垃圾回收器(garbage collector)能夠精確地掃描運(yùn)行時(shí)自身的棧,實(shí)現(xiàn)了完全精確的垃圾回收,從而減少了內(nèi)存占用。同時(shí),棧(stack)的實(shí)現(xiàn)改為連續(xù)棧(contiguous stacks),解決了棧熱分裂(hot split)問題,并為 Go 1.5 計(jì)劃中的并發(fā)垃圾回收(concurrent garbage collector)引入了寫屏障(write barrier)機(jī)制。
- 引入了 internal 包機(jī)制和規(guī)范導(dǎo)入路徑(canonical import path)檢查。internal 包提供了一種方式來定義只能被特定代碼樹內(nèi)部導(dǎo)入的包,增強(qiáng)了大型項(xiàng)目代碼的封裝性。規(guī)范導(dǎo)入路徑通過在 package 聲明行添加特定注釋來指定唯一的導(dǎo)入路徑,防止同一個(gè)包被通過不同路徑導(dǎo)入,提高了代碼的可維護(hù)性。
- 修復(fù)了 bufio.Scanner 在處理文件結(jié)束符(EOF)時(shí)的行為。此修復(fù)確保了即使在輸入數(shù)據(jù)耗盡時(shí),自定義的分割函數(shù)(split function)也會(huì)在文件結(jié)束符(EOF)處被最后調(diào)用一次。這使得分割函數(shù)有機(jī)會(huì)按預(yù)期生成一個(gè)最終的空令牌(token),但也可能影響依賴舊有錯(cuò)誤行為的自定義分割函數(shù)。
下面是一些值得展開的討論:
Runtime 重構(gòu)與核心變化
Go 1.4 的一個(gè)里程碑式的改動(dòng)是將運(yùn)行時(shí)的絕大部分代碼從 C 語言和少量匯編遷移到了 Go 語言實(shí)現(xiàn)。這次重構(gòu)雖然龐大,但其設(shè)計(jì)目標(biāo)是對用戶程序在語義上透明,同時(shí)帶來了幾個(gè)關(guān)鍵的技術(shù)進(jìn)步和性能優(yōu)化。
首先,這次遷移使得 Go 1.4 的垃圾回收器(GC)能夠?qū)崿F(xiàn) 完全精確(fully precise) 的內(nèi)存管理。精確 GC 意味著回收器能夠準(zhǔn)確地識(shí)別內(nèi)存中哪些是活躍的指針,哪些不是。在此之前,GC 可能存在保守掃描(conservative scanning)的情況,即把一些非指針的數(shù)據(jù)(比如整數(shù))誤判為指針,導(dǎo)致這些數(shù)據(jù)引用的內(nèi)存無法被回收(稱為“假陽性”)。精確 GC 消除了這種假陽性,能夠更有效地回收不再使用的內(nèi)存,根據(jù)官方文檔,這使得程序的堆(heap)內(nèi)存占用相比之前版本減少了 10%-30%。
其次,Goroutine 的 棧(stack)實(shí)現(xiàn)從分段棧(segmented stacks)改為了連續(xù)棧(contiguous stacks)。這一點(diǎn)在 Go 1.3 中也提及了:每個(gè) Goroutine 的棧由多個(gè)小的、不連續(xù)的內(nèi)存塊(段)組成。當(dāng)一個(gè)函數(shù)調(diào)用需要的棧空間超過當(dāng)前段的剩余空間時(shí),會(huì)觸發(fā)“棧分裂”,分配一個(gè)新的棧段。這種機(jī)制的主要缺點(diǎn)是 “棧熱分裂(hot split)” 問題:如果一個(gè)函數(shù)調(diào)用頻繁地發(fā)生在棧段即將耗盡的邊界處,就會(huì)導(dǎo)致在循環(huán)中頻繁地分配和釋放新的棧段,帶來顯著的性能開銷,且性能表現(xiàn)難以預(yù)測。
Go 1.4 采用的連續(xù)棧則為每個(gè) Goroutine 分配一塊連續(xù)的內(nèi)存作為其棧。當(dāng)棧空間不足時(shí),運(yùn)行時(shí)會(huì)分配一塊更大的新連續(xù)內(nèi)存,將舊棧的全部內(nèi)容(所有活躍的棧幀)復(fù)制到新棧,并更新棧內(nèi)部指向自身的指針。這個(gè)過程依賴于 Go 的逃逸分析(escape analysis)保證,即指向棧上數(shù)據(jù)的指針通常只存在于棧自身內(nèi)部(向下傳遞),使得復(fù)制和指針更新成為可能。雖然復(fù)制棧有成本,但它是一次性的(直到下一次增長),避免了熱分裂問題,使得性能更加穩(wěn)定和可預(yù)測。正如 Go 1.3 的設(shè)計(jì)文檔(Contiguous Stacks design document)中所討論的,這種方式解決了分段棧的核心痛點(diǎn)。
由于連續(xù)棧消除了熱分裂帶來的性能懲罰,Goroutine 的 初始棧大小得以顯著減小。Go 1.4 將 Goroutine 的默認(rèn)初始棧大小從 8192 字節(jié)(8KB)降低到了 2048 字節(jié)(2KB),這有助于在創(chuàng)建大量 Goroutine 時(shí)節(jié)省內(nèi)存。
再次,為了給 Go 1.5 計(jì)劃引入的 并發(fā)垃圾回收(concurrent garbage collector) 做準(zhǔn)備,Go 1.4 引入了 寫屏障(write barrier)。寫屏障是一種機(jī)制,它將程序中對堆(heap)上指針值的寫入操作從直接的內(nèi)存寫入,改為通過一個(gè)運(yùn)行時(shí)函數(shù)調(diào)用來完成。在 Go 1.4 中,這個(gè)屏障本身可能還沒有太多實(shí)際的 GC 協(xié)調(diào)工作,主要是為了測試其對編譯器和程序性能的影響。在 Go 1.5 中,當(dāng) GC 與用戶 Goroutine 并發(fā)運(yùn)行時(shí),寫屏障將允許 GC 介入和記錄這些指針寫入操作,以確保 GC 的正確性(例如,防止 GC 錯(cuò)誤地回收被用戶代碼新近引用的對象)。
此外,接口值(interface value)的內(nèi)部實(shí)現(xiàn)也發(fā)生了改變。在早期版本中,接口值內(nèi)部根據(jù)存儲(chǔ)的具體類型(concrete type)是持有指向數(shù)據(jù)的指針,還是直接存儲(chǔ)單字大小的標(biāo)量值(如小整數(shù))。這種雙重表示給 GC 處理帶來了復(fù)雜性。從 Go 1.4 開始,接口值 始終 存儲(chǔ)一個(gè)指向?qū)嶋H數(shù)據(jù)的指針。對于大多數(shù)情況(接口通常存儲(chǔ)指針類型或較大的結(jié)構(gòu)體),這個(gè)改變影響很小。但對于將小整數(shù)等非指針類型的值存入接口的場景,現(xiàn)在會(huì)觸發(fā)一次額外的堆內(nèi)存分配,以存儲(chǔ)這個(gè)值并讓接口持有指向它的指針。
最后,關(guān)于 無效指針檢查。Go 1.3 引入了一個(gè)運(yùn)行時(shí)檢查,如果發(fā)現(xiàn)內(nèi)存中本應(yīng)是指針的位置包含明顯無效的值(如 3),程序會(huì)崩潰。這旨在幫助發(fā)現(xiàn)將整數(shù)錯(cuò)誤地當(dāng)作指針使用的 bug。然而,一些(不規(guī)范的)代碼確實(shí)可能這樣做。為了提供一個(gè)過渡方案,Go 1.4 增加了 GODEBUG 環(huán)境變量 invalidptr=0。設(shè)置該變量可以禁用這種崩潰。但官方強(qiáng)調(diào)這只是一個(gè)臨時(shí)解決方法,不能保證未來版本會(huì)繼續(xù)支持,正確的做法是修改代碼,避免將整數(shù)和指針混用(類型別名)。
Internal 包:增強(qiáng)封裝性
Go 語言通過導(dǎo)出(exported, 首字母大寫)和未導(dǎo)出(unexported, 首字母小寫)標(biāo)識(shí)符提供了基本的代碼封裝能力。對于一個(gè)獨(dú)立的包來說,這通常足夠了。但是,當(dāng)一個(gè)大型項(xiàng)目(比如一個(gè)復(fù)雜的庫或應(yīng)用程序)本身需要被拆分成多個(gè)內(nèi)部協(xié)作的包時(shí),問題就出現(xiàn)了。如果這些內(nèi)部包之間需要共享一些公共函數(shù)或類型,按照 Go 的可見性規(guī)則,這些共享的標(biāo)識(shí)符必須是導(dǎo)出的(首字母大寫)。但這會(huì)導(dǎo)致一個(gè)不希望的副作用:這些本應(yīng)只在項(xiàng)目內(nèi)部使用的 API,也意外地暴露給了項(xiàng)目的最終用戶。外部用戶可能會(huì)開始依賴這些內(nèi)部實(shí)現(xiàn)細(xì)節(jié),使得項(xiàng)目維護(hù)者未來重構(gòu)或修改內(nèi)部結(jié)構(gòu)變得困難,因?yàn)樾枰紤]對這些“非官方”用戶的兼容性。
為了解決這種“要么全公開,要么全包內(nèi)私有”的二元限制,Go 1.4 引入了一個(gè)由 go 工具鏈強(qiáng)制執(zhí)行的約定: internal 包 。
核心規(guī)則:
如果一個(gè)目錄名為 internal,那么位于這個(gè) internal 目錄(及其子目錄)下的所有包,只能被 直接包含 該 internal 目錄的 父目錄 及其 子樹 中的代碼所導(dǎo)入。任何處于這個(gè)父目錄樹之外的代碼都無法導(dǎo)入該 internal 包。
文件樹示例:
假設(shè)我們有如下的項(xiàng)目結(jié)構(gòu):
/home/user/
└── myproject/
├── go.mod
├── cmd/
│ └── myapp/
│ └── main.go <- 可以導(dǎo)入 internal/util, *不能* 導(dǎo)入 pkg/internal/core
├── pkg/
│ ├── api/
│ │ └── handler.go <- 可以導(dǎo)入 internal/util 和 pkg/internal/core
│ └── internal/ <- 這是 pkg 目錄下的 internal
│ └── core/
│ └── core.go <- 定義內(nèi)部核心功能
├── internal/ <- 這是項(xiàng)目根目錄下的 internal
│ └── util/
│ └── util.go <- 定義項(xiàng)目范圍的內(nèi)部工具
└── vendor/ <- (無關(guān))
└── anotherpkg/ <- 一個(gè)與 pkg 平級(jí)的目錄
└── service.go <- *不能* 導(dǎo)入 internal/util 或 pkg/internal/core
/home/user/
└── otherproject/
└── main.go <- *不能* 導(dǎo)入 myproject/internal/util 或 myproject/pkg/internal/core
根據(jù)上述規(guī)則和示例:
- myproject/internal/util 包:
- 它的父目錄是 myproject/。
- 因此,只有 myproject/ 目錄及其所有子目錄中的代碼(如 myproject/cmd/myapp/main.go, myproject/pkg/api/handler.go)可以導(dǎo)入 myproject/internal/util。
- myproject/anotherpkg/service.go 因?yàn)椴辉?nbsp;myproject/ 的子樹中(雖然在同一個(gè)項(xiàng)目下,但 internal 的直接父級(jí)是 myproject,anotherpkg 與 internal 平級(jí)),所以不能導(dǎo)入它。
- 外部項(xiàng)目 otherproject/main.go 顯然也不能導(dǎo)入。
- myproject/pkg/internal/core 包:
- 它的父目錄是 myproject/pkg/。
- 因此,只有 myproject/pkg/ 目錄及其所有子目錄中的代碼(如 myproject/pkg/api/handler.go)可以導(dǎo)入 myproject/pkg/internal/core。
- 位于 myproject/cmd/myapp/main.go 的代碼,雖然也在 myproject 項(xiàng)目內(nèi),但它不屬于 myproject/pkg/ 的子樹,所以 不能 導(dǎo)入 myproject/pkg/internal/core。
- 外部項(xiàng)目和 myproject/anotherpkg 同理,也不能導(dǎo)入。
總結(jié): internal 目錄就像一道屏障,它允許其“直系親屬”(父目錄及其后代)訪問內(nèi)部成員,但阻止了所有“外人”(包括同一項(xiàng)目中的非后代包以及其他項(xiàng)目)的訪問。
這個(gè)檢查是由 go build, go test 等 go 命令在編譯時(shí)強(qiáng)制執(zhí)行的。在 Go 1.4 中,此規(guī)則首先應(yīng)用于 Go 標(biāo)準(zhǔn)庫($GOROOT)自身的組織,從 Go 1.5 開始,該規(guī)則被推廣到所有用戶的 GOPATH 和后來的 Go Modules 項(xiàng)目中。
規(guī)范導(dǎo)入路徑:確保唯一性與可維護(hù)性
在 Go 中,開發(fā)者可以使用 go get 工具方便地獲取和安裝托管在公共服務(wù)(如 github.com)上的代碼。包的導(dǎo)入路徑通常就反映了其托管位置,例如 github.com/user/repo。然而,Go 也提供了一種機(jī)制,允許開發(fā)者設(shè)置 自定義導(dǎo)入路徑(custom/vanity import paths),比如使用自己的域名 mycompany.com/mylib,并通過在 mycompany.com/mylib 這個(gè) URL 提供特定的 HTML <meta> 標(biāo)簽,將 go get 工具重定向到實(shí)際的代碼倉庫(例如 github.com/user/repo)。
這種自定義路徑很有用,它可以:
- 為包提供一個(gè)穩(wěn)定的、與托管服務(wù)無關(guān)的名稱。即使未來將代碼庫從 GitHub 遷移到 GitLab,只要更新 mycompany.com/mylib 的重定向,使用者的導(dǎo)入路徑無需更改。
- 支持使用 go 工具不直接識(shí)別的版本控制系統(tǒng)或服務(wù)器。
但這也帶來了一個(gè)問題:同一個(gè)包現(xiàn)在可能有兩個(gè)有效的導(dǎo)入路徑:自定義路徑 (mycompany.com/mylib) 和實(shí)際托管路徑 (github.com/user/repo)。這會(huì)導(dǎo)致:
- 意外的重復(fù)導(dǎo)入:如果一個(gè)程序的不同部分不小心通過不同的路徑導(dǎo)入了同一個(gè)包,編譯器會(huì)認(rèn)為它們是兩個(gè)不同的包,導(dǎo)致代碼冗余,甚至可能因?yàn)闋顟B(tài)不共享而引發(fā) bug。
- 更新問題:用戶可能一直使用非官方的托管路徑導(dǎo)入,如果包作者只維護(hù)自定義路徑的重定向,用戶可能無法及時(shí)獲知更新。
- 破壞兼容性:如果包作者遷移了倉庫并更新了自定義路徑的重定向,那些仍然使用舊托管路徑的用戶代碼會(huì)直接編譯失敗。
為了解決這些問題,Go 1.4 引入了 規(guī)范導(dǎo)入路徑(canonical import path) 檢查機(jī)制。
工作方式: 包的作者可以在其源代碼文件的 package 聲明行的末尾添加一個(gè)特定格式的注釋,來聲明該包的 唯一 官方導(dǎo)入路徑。
語法:
package pdf // import "rsc.io/pdf"
或者使用塊注釋:
package pdf /* import "rsc.io/pdf" */
效果: 當(dāng) go 命令(如 go build, go install)編譯一個(gè)導(dǎo)入了帶有此種注釋的包時(shí),它會(huì)檢查導(dǎo)入時(shí)使用的路徑是否與注釋中聲明的規(guī)范路徑完全一致。如果不一致,go 命令將 拒絕編譯 導(dǎo)入方代碼。
示例: 如果 rsc.io/pdf 包中包含了 package pdf // import "rsc.io/pdf" 的注釋,那么任何試圖 import "github.com/rsc/pdf" 的代碼在編譯時(shí)都會(huì)失敗。這強(qiáng)制所有使用者都必須使用 rsc.io/pdf 這個(gè)規(guī)范路徑。
重要提示: 這個(gè)檢查是在 構(gòu)建時(shí)(build time) 進(jìn)行的,而不是在 go get 下載時(shí)。這意味著,如果 go get github.com/rsc/pdf 成功下載了代碼,但在后續(xù)編譯時(shí)因?yàn)橐?guī)范路徑檢查失敗,你需要手動(dòng)刪除本地 GOPATH 或 Go Modules 緩存中通過錯(cuò)誤路徑下載的包副本。
相關(guān)改進(jìn): 為了配合這個(gè)特性,go get -u(更新包)命令也增加了一項(xiàng)檢查:它會(huì)驗(yàn)證本地已下載包的遠(yuǎn)程倉庫地址是否與其自定義導(dǎo)入路徑解析出的地址一致。如果包的實(shí)際托管位置自上次下載后發(fā)生了改變(可能意味著倉庫遷移),go get -u 會(huì)失敗,防止意外更新。可以使用新的 -f 標(biāo)志來強(qiáng)制覆蓋此檢查。
子倉庫路徑遷移: Go 官方也借此機(jī)會(huì)宣布,其下的子倉庫(如 code.google.com/p/go.tools 等)將統(tǒng)一使用 golang.org/x/ 前綴的自定義導(dǎo)入路徑(如 golang.org/x/tools),并計(jì)劃在未來(約 2015 年 6 月 1 日)為這些包添加規(guī)范導(dǎo)入路徑注釋。屆時(shí),使用 Go 1.4 及更高版本的用戶如果還在使用舊的 code.google.com 路徑,編譯將會(huì)失敗。官方強(qiáng)烈建議所有開發(fā)者更新其代碼,改用新的 golang.org/x/ 路徑導(dǎo)入這些子倉庫包。好消息是,舊版本的 Go (Go 1.0+) 也能識(shí)別和使用新的 golang.org/x/ 路徑,所以更新導(dǎo)入路徑不會(huì)破壞對舊 Go 版本的兼容性。
bufio.Scanner EOF 行為變更
bufio.Scanner 是 Go 標(biāo)準(zhǔn)庫中用于方便地讀取輸入流(如文件、網(wǎng)絡(luò)連接或字符串)并將其分割成一個(gè)個(gè)“令牌(token)”的工具。默認(rèn)情況下,它可以按行或按 UTF-8 單詞分割,但它也允許用戶提供自定義的分割邏輯,即 分割函數(shù)(SplitFunc)。
SplitFunc 的類型簽名是:
type SplitFunc func(data []byte, atEOF bool) (advance int, token []byte, err error)
- data: 當(dāng)前 Scanner 緩沖區(qū)中剩余未處理的數(shù)據(jù)。
- atEOF: 一個(gè)布爾值,指示是否已經(jīng)到達(dá)輸入流的末尾(End Of File)。**true 表示底層 reader 不會(huì)再提供更多數(shù)據(jù)了**。
- advance: SplitFunc 應(yīng)該告訴 Scanner 消耗掉 data 中的多少字節(jié)。
- token: 這次調(diào)用找到的令牌。如果還沒找到完整的令牌,可以返回 nil。
- err: 如果遇到錯(cuò)誤,返回非 nil 的 error。
Go 1.4 之前的行為與問題:
在 Go 1.4 之前,Scanner 在處理 EOF 時(shí)存在一個(gè)微妙的問題。當(dāng)輸入流恰好在最后一個(gè)有效令牌的分隔符之后結(jié)束時(shí),或者當(dāng)輸入流為空時(shí),SplitFunc 可能無法可靠地生成一個(gè)預(yù)期的、位于流末尾的 空令牌。文檔承諾了可以做到這一點(diǎn),但實(shí)際行為有時(shí)不一致。
Go 1.4 的修復(fù)與新行為:
Go 1.4 修復(fù)了這個(gè)問題。現(xiàn)在的行為更加明確和可靠:**當(dāng)輸入流耗盡后,SplitFunc 保證會(huì)被最后調(diào)用一次,并且這次調(diào)用時(shí) atEOF 參數(shù)為 true**。這次調(diào)用給予了 SplitFunc 處理輸入結(jié)束狀態(tài)的最后機(jī)會(huì),使其能夠根據(jù)需要生成最后一個(gè)令牌,即使這個(gè)令牌是空的。
代碼示例:
假設(shè)我們要實(shí)現(xiàn)一個(gè)按逗號(hào)分割的 SplitFunc,并且希望正確處理末尾的空字段(例如 "a,b," 應(yīng)該產(chǎn)生三個(gè)令牌:"a", "b", "")。下面是一個(gè)能體現(xiàn) Go 1.4 行為的實(shí)現(xiàn):
package main
import (
"bufio"
"bytes"
"fmt"
"strings"
)
// customSplit: 按逗號(hào)分割,能處理末尾空字段
func customSplit(data []byte, atEOF bool) (advance int, token []byte, err error) {
// 查找第一個(gè)逗號(hào)
if i := bytes.IndexByte(data, ','); i >= 0 {
// 找到逗號(hào),返回逗號(hào)之前的部分
return i + 1, data[:i], nil
}
// 沒有找到逗號(hào)
if atEOF {
// 如果是 EOF,無論 data 是否為空,都認(rèn)為掃描結(jié)束。
// data 中剩余的部分(如果非空)是最后一個(gè) token。
iflen(data) == 0 {
// 沒有剩余數(shù)據(jù)且已達(dá) EOF,停止掃描。
return0, nil, nil
}
// 如果有剩余數(shù)據(jù),返回它作為最后一個(gè) token。
returnlen(data), data, nil
}
// 沒有逗號(hào),也沒到 EOF,請求 Scanner 讀取更多數(shù)據(jù)
return0, nil, nil
}
func main() {
inputs := []string{
"a,b,c", // 標(biāo)準(zhǔn)情況
"a,b,", // 末尾有逗號(hào),應(yīng)有空字段
"", // 空輸入
"a", // 單個(gè)字段
",a,b", // 開頭有逗號(hào),應(yīng)有空字段
"a,,b", // 中間有逗號(hào),應(yīng)有空字段
}
for _, input := range inputs {
fmt.Printf("Scanning input: %q\n", input)
scanner := bufio.NewScanner(strings.NewReader(input))
scanner.Split(customSplit)
count := 0
for scanner.Scan() {
count++
fmt.Printf(" Token %d: %q\n", count, scanner.Text())
}
if err := scanner.Err(); err != nil {
fmt.Printf(" Error during scan: %v\n", err)
}
fmt.Println("---")
}
}
預(yù)期輸出 (Go 1.4 及以后):
Scanning input: "a,b,c"
Token 1: "a"
Token 2: "b"
Token 3: "c"
---
Scanning input: "a,b,"
Token 1: "a"
Token 2: "b"
Token 3: ""
---
Scanning input: ""
---
Scanning input: "a"
Token 1: "a"
---
Scanning input: ",a,b"
Token 1: ""
Token 2: "a"
Token 3: "b"
---
Scanning input: "a,,b"
Token 1: "a"
Token 2: ""
Token 3: "b"
---
主要的區(qū)別在于輸入 "a,b,"。在 Go 1.4 之前的版本中,由于 bufio.Scanner 的 bug,最后一個(gè)由結(jié)尾逗號(hào)產(chǎn)生的空令牌 "" 無法被正確掃描出來,導(dǎo)致輸出只有 "a" 和 "b"。而 Go 1.4 修復(fù)了這個(gè) bug,使得輸出能正確包含 "a", "b" 和 ""。其他不涉及嚴(yán)格在 EOF 產(chǎn)生空令牌的情況,輸出行為通常是一致的。
解釋:
在 Go 1.4 及以后版本,對于輸入 "a,b,"
:
- SplitFunc 找到第一個(gè)逗號(hào),返回 "a"。
- SplitFunc 找到第二個(gè)逗號(hào),返回 "b"。
- SplitFunc 找到第三個(gè)逗號(hào),返回 "" (空字符串)。
- 此時(shí) data 變?yōu)?nbsp;"",Scanner 讀取發(fā)現(xiàn)已到 EOF。
- Scanner 最后一次調(diào)用 SplitFunc,傳入 data 為 []byte("") 且 atEOF 為 true。
- customSplit 函數(shù)根據(jù)邏輯,因?yàn)?nbsp;len(data) 為 0,返回 (0, nil, nil)。
- Scanner 接收到 (0, nil, nil) 且 atEOF 為 true,知道掃描結(jié)束。關(guān)鍵在于,第三步已經(jīng)成功返回了末尾的空令牌 ""。