深入理解 Go 標準庫之 bufio.Scanner

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。眾所周知,[帶緩衝的 IO 標準庫](https://golang.org/pkg/bufio/) 一直是 Go 中最佳化讀寫操作的利器。對於寫操作來說,在被發送到 `socket` 或硬碟之前,`IO 緩衝區` 提供了一個臨時儲存區來存放資料,緩衝區儲存的資料達到一定容量後才會被"釋放"出來進行下一步儲存,這種方式大大減少了寫操作或是最終的系統調用被觸發的次數,這無疑會在頻繁使用系統資源的時候節省下巨大的系統開銷。而對於讀操作來說,`緩衝 IO` 意味著每次操作能夠讀取更多的資料,既減少了系統調用的次數,又通過以塊為單位讀取硬碟資料來更高效地使用底層硬體。本文會更加側重於講解 [bufio](https://golang.org/pkg/bufio/) 包中的 [Scanner](https://golang.org/pkg/bufio/#Scanner) 掃描器模組,它的主要作用是把資料流分割成一個個標記併除去它們之間的空格。```"foo bar baz"```如果我們只想得到上面字串中的單詞,那麼掃描器能幫我們按順序檢索出 "foo","bar" 和 "baz" 這三個單詞( [查看源碼](https://play.golang.org/p/_GKmSMZmWZ) )```gopackage mainimport ( "bufio" "fmt" "strings")func main() { input := "foo bar baz" scanner := bufio.NewScanner(strings.NewReader(input)) scanner.Split(bufio.ScanWords) for scanner.Scan() { fmt.Println(scanner.Text()) }}```輸出結果:```foobarbaz````Scanner` 掃描器讀取資料流的時候會使用帶緩衝區的 IO,並接受 `io.Reader` 作為參數。如果你需要在記憶體中處理字串或者是 bytes 切片,可以首先考慮使用 [bytes.Split](https://golang.org/pkg/bytes/#Split) 或是 [strings.Split](https://golang.org/pkg/strings/#Split) 這樣的工具集,當處理這些流資料時,`bytes` 或是 `strings` 標準庫中的方法可能是最簡單可靠的。在底層,掃描器使用緩衝不斷儲存資料,當緩衝區非空或者是讀到檔案的末尾時 (EOF) `split` 函數會被調用,目前我們介紹了一個預定義好的 `split` 函數,但根據下面的函數簽名來看,它的用途可能更加廣泛。```gofunc(data []byte, atEOF bool) (advance int, token []byte, err error)```目前為止,我們知道 `Split` 函數會在讀資料的時候被調用,從傳回值來看,它的執行應該有 3 種不同情況。### 1. 需要補充更多的資料這表示傳入的資料還不足以產生一個字元流的標記,當返回的值分別是 `0, nil, nil` 的時候,掃描器會嘗試讀取更多的資料,如果緩衝區已滿,那麼緩衝區會在任何讀取操作前自動擴容為原來的兩倍,讓我們來仔細看一下這個過程 [查看源碼](https://play.golang.org/p/j7RDUVujNv)```gopackage mainimport ( "bufio" "fmt" "strings")func main() { input := "abcdefghijkl" scanner := bufio.NewScanner(strings.NewReader(input)) split := func(data []byte, atEOF bool) (advance int, token []byte, err error) { fmt.Printf("%t\t%d\t%s\n", atEOF, len(data), data) return 0, nil, nil } scanner.Split(split) buf := make([]byte, 2) scanner.Buffer(buf, bufio.MaxScanTokenSize) for scanner.Scan() { fmt.Printf("%s\n", scanner.Text()) }}```輸出結果:```false2abfalse4abcdfalse8abcdefghfalse12abcdefghijkltrue12abcdefghijkl```上例中的 `split` 函數可以說是簡單且極其貪婪的 -- 總是請求更多的資料, `Scanner` 嘗試讀取更多的資料的同時會保證緩衝區擁有足夠的空間來存放這些資料。在上面的例子中,我們將緩衝區的大小設定為 2。```gobuf := make([]byte, 2)scanner.Buffer(buf, bufio.MaxScanTokenSize)```在 `split` 函數第一次被調用後,`scanner` 會倍增緩衝區的容量,讀取更多的資料,然後再次調用 `split` 函數。在第二次調用之後增長倍數仍然保持不變,通過觀察輸出結果可以發現第一次調用 `split` 得到大小為 2 的切片,然後是 4、8,最後到 12,因為沒有更多的資料了。*緩衝區的預設大小是 [4096](https://github.com/golang/go/blob/13cfb15cb18a8c0c31212c302175a4cb4c050155/src/bufio/scan.go#L76) 個位元組。*在這值得我們來討論一下 `atEOF` 這個參數,通過這個參數我們能夠在 `split` 函數中判斷是否還有資料可供使用,它能夠在達到資料末尾 (EOF) 或者是讀取出錯的時候觸發為真,一旦任何上述情況發生, `scanner` 將拒絕讀取任何東西,像這樣的 `flag` 標誌可被用來拋出異常(因其不完整的字元標記),最終會導致 `scanner.Split()` 在調用的時候返回 `false` 並終止整個進程。異常可以通過 `Err` 方法來取得。```gopackage mainimport ( "bufio" "errors" "fmt" "strings")func main() { input := "abcdefghijkl" scanner := bufio.NewScanner(strings.NewReader(input)) split := func(data []byte, atEOF bool) (advance int, token []byte, err error) { fmt.Printf("%t\t%d\t%s\n", atEOF, len(data), data) if atEOF { return 0, nil, errors.New("bad luck") } return 0, nil, nil } scanner.Split(split) buf := make([]byte, 12) scanner.Buffer(buf, bufio.MaxScanTokenSize) for scanner.Scan() { fmt.Printf("%s\n", scanner.Text()) } if scanner.Err() != nil { fmt.Printf("error: %s\n", scanner.Err()) }}```輸出結果:```false12abcdefghijkltrue12abcdefghijklerror: bad luck````atEOF` 參數同時也能夠用於處理那些遺留在緩衝區中的資料,其中一個預定義的 `split` 函數漸進式掃描輸入反映了 [這種行為](https://github.com/golang/go/blob/be943df58860e7dec008ebb8d68428d54e311b94/src/bufio/scan.go#L403) ,例如我們這樣輸入下面這些單詞時```foobarbaz```因為在行末並沒有 `\n` 字元,因此當 [ScanLines](https://golang.org/pkg/bufio/#ScanLines) 無法找到新一行的字元時,它就會返回剩餘的字元來作為最後的字元標記 ([查看源碼](https://golang.org/pkg/bufio/#ScanLines))```gopackage mainimport ( "bufio" "fmt" "strings")func main() { input := "foo\nbar\nbaz" scanner := bufio.NewScanner(strings.NewReader(input)) // 事實上這裡並不需要傳入 ScanLines 因為這原本就是標準庫預設的 split 函數 scanner.Split(bufio.ScanLines) for scanner.Scan() { fmt.Println(scanner.Text()) }}```輸出結果:```foobarbaz```### 2. 已找到字元標記(token)當 `split` 函數能夠檢測到 _標記_ 時,就會發生這種情況。它返回在緩衝區中向前移動的字元數和 _標記_ 本身。返回兩個值的原因在於 _標記_ 向前移動的距離不總是等於位元組個數。假設輸入為 "foo foo foo" ,當我們的目標只是找到其中的單詞 ( [掃描單詞](https://golang.org/pkg/bufio/#ScanWords) ) 時,`split` 函數會跳過它們之間的空格。```(4, "foo")(4, "foo")(3, "foo")```讓我們通過一個具體的例子看一下,下面的這個函數將只會尋找連續的 `foo` 串, [查看源碼](https://play.golang.org/p/X_adw-KnUM)```gopackage mainimport ( "bufio" "bytes" "fmt" "io" "strings")func main() { input := "foofoofoo" scanner := bufio.NewScanner(strings.NewReader(input)) split := func(data []byte, atEOF bool) (advance int, token []byte, err error) { if bytes.Equal(data[:3], []byte{'f', 'o', 'o'}) { return 3, []byte{'F'}, nil } if atEOF { return 0, nil, io.EOF } return 0, nil, nil } scanner.Split(split) for scanner.Scan() { fmt.Printf("%s\n", scanner.Text()) }}```輸出結果:```FFF```### 3. 報錯如果 `split` 函數返回了錯誤那麼掃描器就會停止工作,[查看源碼](https://play.golang.org/p/KpiyhMFUyT)```gopackage mainimport ( "bufio" "errors" "fmt" "strings")func main() { input := "abcdefghijkl" scanner := bufio.NewScanner(strings.NewReader(input)) split := func(data []byte, atEOF bool) (advance int, token []byte, err error) { return 0, nil, errors.New("bad luck") } scanner.Split(split) for scanner.Scan() { fmt.Printf("%s\n", scanner.Text()) } if scanner.Err() != nil { fmt.Printf("error: %s\n", scanner.Err()) }}```輸出結果:```error: bad luck```然而,其中有一種特殊的錯誤並不會使掃描器立即停止工作。### ErrFinalToken掃描器給訊號(signal) 提供了一個叫做 [最終標記](https://golang.org/pkg/bufio/#pkg-variables) 的選項,這是一個不會打破迴圈(掃描過程依然返回真)的特殊標記,但隨後的一系列調用會使掃描動作立刻終止。```gofunc (s *Scanner) Scan() bool { if s.done { return false } ...```在 Go 語言官方 [issue #11836](https://github.com/golang/go/issues/11836) 中提供了一種方法使得當發現特殊標記時也能夠立即停止掃描。[查看源碼](https://play.golang.org/p/ArL-k-i2OV)```gopackage mainimport ( "bufio" "bytes" "fmt" "strings")func split(data []byte, atEOF bool) (advance int, token []byte, err error) { advance, token, err = bufio.ScanWords(data, atEOF) if err == nil && token != nil && bytes.Equal(token, []byte{'e', 'n', 'd'}) { return 0, []byte{'E', 'N', 'D'}, bufio.ErrFinalToken } return}func main() { input := "foo end bar" scanner := bufio.NewScanner(strings.NewReader(input)) scanner.Split(split) for scanner.Scan() { fmt.Println(scanner.Text()) } if scanner.Err() != nil { fmt.Printf("Error: %s\n", scanner.Err()) }}```輸出結果:```fooEND```> `io.EOF` 和 `ErrFinalToken` 類型的錯誤都不被認為是真的起作用的錯誤 -- `Err` 方法會在任何這兩個錯誤出現並停止掃描器時仍然返回 `nil`### 最大標記大小 / ErrTooLong預設情況下,緩衝區的最大長度應該小於 `64 * 1024` 個位元組,這意味著找到的標記不能大於這個限制。```gopackage mainimport ( "bufio" "fmt" "strings")func main() { input := strings.Repeat("x", bufio.MaxScanTokenSize) scanner := bufio.NewScanner(strings.NewReader(input)) for scanner.Scan() { fmt.Println(scanner.Text()) } if scanner.Err() != nil { fmt.Println(scanner.Err()) }}```上面的程式會列印出 `bufio.Scanner: token too long` ,我們可以通過 [Buffer](https://golang.org/pkg/bufio/#Scanner.Buffer) 方法來自訂緩衝區的長度,在上文第一小節中這個方法有出現過,但我們這次會舉一個更切題的例子,[查看源碼](https://play.golang.org/p/ZsgJzuIy4r)```gobuf := make([]byte, 10)input := strings.Repeat("x", 20)scanner := bufio.NewScanner(strings.NewReader(input))scanner.Buffer(buf, 20)for scanner.Scan() { fmt.Println(scanner.Text())}if scanner.Err() != nil { fmt.Println(scanner.Err())}```輸出結果:```bufio.Scanner: token too long```### 防止死迴圈幾年前 [issue #8672](https://github.com/golang/go/issues/8672) 被提出,解決方案是加多一段代碼,通過判斷 `atEOF` 為真且緩衝區為空白來確定 `split` 函數可以被調用,而現有的代碼可能會進入死迴圈。```gopackage mainimport ( "bufio" "bytes" "fmt" "strings")func main() { input := "foo|bar" scanner := bufio.NewScanner(strings.NewReader(input)) split := func(data []byte, atEOF bool) (advance int, token []byte, err error) { if i := bytes.IndexByte(data, '|'); i >= 0 { return i + 1, data[0:i], nil } if atEOF { return len(data), data[:len(data)], nil } return 0, nil, nil } scanner.Split(split) for scanner.Scan() { if scanner.Text() != "" { fmt.Println(scanner.Text()) } }}````split` 函數假設當 `atEOF` 為真就能夠安全地使用剩餘的緩衝作為標記,這引發了 [issue #8672](https://github.com/golang/go/issues/8672) 被修複之後的另一個問題: 因為緩衝區可以為空白,所以當返回 `(0, [], nil)` 時 `split` 函數並不能增加緩衝區的大小, [issue #9020](https://github.com/golang/go/issues/9020) 發現了此種情況下的 `panic` ,[查看源碼](https://play.golang.org/p/HUbd-ZInAQ)```foobarpanic: bufio.Scan: 100 empty tokens without progressing```當我第一次閱讀有關 **Scanner** 或是 [SplitFunc](https://golang.org/pkg/bufio/#SplitFunc) 的文檔時我並沒能弄明白在所有情況下它們是如何工作的,即便是閱讀原始碼也協助甚微,因為 [Scan](https://github.com/golang/go/blob/be943df58860e7dec008ebb8d68428d54e311b94/src/bufio/scan.go#L128) 看上去真的很複雜,希望這篇文章能夠協助其他人更好地理清這塊的細節。

via: https://medium.com/golangspec/in-depth-introduction-to-bufio-scanner-in-golang-55483bb689b4

作者:Michał Łowicki 譯者:yujiahaol68 校對:rxcai polaris1119

本文由 GCTT 原創編譯,Go語言中文網 榮譽推出

本文由 GCTT 原創翻譯,Go語言中文網 首發。也想加入譯者行列,為開源做一些自己的貢獻嗎?歡迎加入 GCTT!
翻譯工作和譯文發表僅用於學習和交流目的,翻譯工作遵照 CC-BY-NC-SA 協議規定,如果我們的工作有侵犯到您的權益,請及時聯絡我們。
歡迎遵照 CC-BY-NC-SA 協議規定 轉載,敬請在本文中標註並保留原文/譯文連結和作者/譯者等資訊。
文章僅代表作者的知識和看法,如有不同觀點,請樓下排隊吐槽

2338 次點擊  ∙  2 贊  
相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.