深入理解golang中bufio.SplitFunc
前言
golang的bufio
包裡面定以的SplitFunc
是一個比較重要也比較難以理解的東西,本文希望通過結合簡單的例項介紹SplitFunc
的工作原理以及如何實現一個自己的SplitFunc
。
一個例子
在bufio
包裡面定義了一些常用的工具比如Scanner
,你可能需要讀取使用者在標準輸入裡面輸入的一些東西,比如我們做一個復讀機
,讀取使用者的每一行輸入,然後打印出來:
package main import ( "bufio" "fmt" "os" ) func main() { scanner := bufio.NewScanner(os.Stdin) scanner.Split(bufio.ScanLines) for scanner.Scan(){ fmt.Println(scanner.Text()) } }
這個程式很簡單,os.Stdin
實現了io.Reader
介面,我們從這個reader建立了一個scanner
,設定分割函式為bufio.ScanLines
,然後for
迴圈,每次讀到一行資料就將文字內容打印出來。麻雀雖小五臟俱全,這個小程式雖然簡單,卻引出了我們今天要介紹的物件:bufio.SplitFunc
,它的定義是這個樣子的:
package "buffio" type SplitFunc func(data []byte, atEOF bool) (advance int, token []byte, err error)
golang官方文件的描述是這個樣子的:
SplitFunc is the signature of the split function used to tokenize the input. The arguments are an initial substring of the remaining unprocessed data and a flag, atEOF, that reports whether the Reader has no more data to give. The return values are the number of bytes to advance the input and the next token to return to the user, if any, plus an error, if any. Scanning stops if the function returns an error, in which case some of the input may be discarded. Otherwise, the Scanner advances the input. If the token is not nil, the Scanner returns it to the user. If the token is nil, the Scanner reads more data and continues scanning; if there is no more data--if atEOF was true--the Scanner returns. If the data does not yet hold a complete token, for instance if it has no newline while scanning lines, a SplitFunc can return (0, nil, nil) to signal the Scanner to read more data into the slice and try again with a longer slice starting at the same point in the input. The function is never called with an empty data slice unless atEOF is true. If atEOF is true, however, data may be non-empty and, as always, holds unprocessed text.
英文!引數這麼多!返回值這麼多!好煩!不知道各位讀者遇到這種文件會不會有這種感覺...正式由於這種情況,我才決定寫一篇文章介紹一下SplitFunc
的具體工作原理,用一種通俗的方式結合具體例項加以說明,希望對讀者有所幫助。
好了,廢話少說,開始正題吧!
Scanner和SplitFunc的工作機制
package "buffio" type SplitFunc func(data []byte, atEOF bool) (advance int, token []byte, err error)
Scanner
是有快取的,意思是Scanner
底層維護了一個Slice
用來儲存已經從Reader
中讀取的資料,Scanner
會呼叫我們設定SplitFunc
,將緩衝區內容(data)和是否已經輸入完了(atEOF)以引數的形式傳遞給SplitFunc
,而SplitFunc
的職責就是根據上述的兩個引數返回下一次Scan
需要前進幾個位元組(advance),分割出來的資料(token),以及錯誤(err)。
這是一個通訊雙向的過程,Scanner
告訴我們的SplitFunc
已經掃描到的資料和是否到結尾了,我們的SplitFunc
則根據這些資訊將分割的結果返回和下次掃描需要前進的位置返回給Scanner
。用一個例子來說明:
package main import ( "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()) } }
輸出
false 2 abfalse 4 abcdfalse 8 abcdefghfalse 12 abcdefghijkltrue 12 abcdefghijkl
這裡我們把緩衝區的初始大小設定為了2,不夠的時候會擴充套件為原來的2倍,最大為bufio.MaxScanTokenSize
,這樣一開始掃描2個位元組,我們的緩衝區就滿了,reader的內容還沒有讀取到EOF,然後split函式執行,輸出:
false 2 ab
緊接著函式返回0, nil, nil
這個返回值告訴Scanner資料不夠,下次讀取的位置前進0位,需要繼續從reader裡面讀取,此時因為緩衝區滿了,所以容量擴充套件為2 * 2 = 4
,reader的內容還沒有讀取到EOF,輸出
false 4 abcd
重複上述步驟,一直到最後全部內容讀取完了,EOF此時變成了true
true 12 abcdefghijkl
看了上面的過程是不是對SplitFunc的工作原來有了一點理解了呢?再回頭看一下golang的官方文件有沒有覺得稍微理解了一點?下面是bufio.ScanLines
的實現,讀者可以自己研究一下該函式是如何工作的
標準庫裡的ScanLines
func ScanLines(data []byte, atEOF bool) (advance int, token []byte, err error) { // 表示我們已經掃描到結尾了 if atEOF && len(data) == 0 { return 0, nil, nil } // 找到\n的位置 if i := bytes.IndexByte(data, '\n'); i >= 0 { // 把下次開始讀取的位置向前移動i + 1位 return i + 1, dropCR(data[0:i]), nil } // 這裡處理的reader內容全部讀取完了,但是內容不為空,所以需要把剩餘的資料返回 if atEOF { return len(data), dropCR(data), nil } // 表示現在不能分割,向Reader請求更多的資料 return 0, nil, nil }
參考
In-depth introduction to bufio.Scanner in Golang