DRYな備忘録

Don't Repeat Yourself.

【Go言語】可変長のioをReadしたい【bufio.Scanner】【io.Rader】

io.Readerを使った読み込み

ファイルの読み込みやTCPコネクションのメッセージ読み込みに、io.Readerインターフェースを実装したstructのReadメソッドを使う

以下その例

package main

import "fmt"
import "os"

func main() {

        file, _ := os.Open("sample.txt")

        bufferSize := 4
        buf := make([]byte, bufferSize)

        n, e := file.Read(buf)
        fmt.Println("LENGTH READ:", n, "ERROR:", e)
        fmt.Println("---RESULT---\n", string(buf))
}

問題

上記の例では、読み込んだトークンを流し込むバッファー([]byte型)を自分で定義して、ReaderのReadに渡している。しかしTCPコネクションのメッセージなど、どのような大きさになる分からない場合に、いたずらにバッファーの大きさ(上記におけるbufferSize)を大きく取るわけにもいかない。もちろん、小さく取りすぎると正しく十分取得できない。

上記の実行例

% go run main.go
LENGTH READ: 4 ERROR: <nil>
---RESULT---
abcd

4文字で切れる

bufio.Scannerを使った読み込み

そこでGo言語では、bufio.Scannerを提供している。このScannerはざっくり言うと「読み込もうとしたioのトークンが途切れるまで読み込む」という機能を持っている。

package main

import "os"
import "fmt"
import "bufio"

func main() {
        file, _ := os.Open("sample.txt")
        scnr := bufio.NewScanner(file)
        scnr.Scan()
        fmt.Println(scnr.Text())
}

結果

% go run main2.go
abcdefghijklmnopgrstuvwxyz

だいぶラク。うまくいっているように見えるが

「トークンが途切れる」とは?

実はsample.txtの内容は以下の通り

% cat sample.txt
abcdefghijklmnopgrstuvwxyz
0123456789

デフォルトではnewlineで区切られるため、二行目が読み込まれていない

ちゃんと説明を見よう(ハイ

Scanner provides a convenient interface for reading data such as a file of newline-delimited lines of text. Successive calls to the Scan method will step through the 'tokens' of a file, skipping the bytes between the tokens. The specification of a token is defined by a split function of type SplitFunc; the default split function breaks the input into lines with line termination stripped. Split functions are defined in this package for scanning a file into lines, bytes, UTF-8-encoded runes, and space-delimited words. The client may instead provide a custom split function.

Scannerはファイルなど改行区切りのテキストを読み込むために手軽なインターフェースを提供します。連続的にScanメソッドを呼ぶと、現在まで読み込んだトークンを破棄し、次のトークンまで読み込みが実行されます。「トークン」の単位はSplitFuncによって定義されます。デフォルトではこの関数は「行の終端」を検知してトークンを区切ります。Split関数はこのパッケージ内ではファイルを行・byte列・UTF8文字列・スペース区切り文字列を扱うために定義されています。これらの代わりに、パッケージの使用者はこのルールを独自に実装したSplitFuncを渡すことができます。

Scanning stops unrecoverably at EOF, the first I/O error, or a token too large to fit in the buffer. When a scan stops, the reader may have advanced arbitrarily far past the last token. Programs that need more control over error handling or large tokens, or must run sequential scans on a reader, should use bufio.Reader instead.

Scanの実行はファイルの終端、I/Oエラー、過大なトークンによって回復せずに終了します。Scanの実行が終了すると、与えられたReaderは不定のトークン位置まで移動します。したがって、繊細なエラーハンドリングや、厳格に連続的なScanが必要なアプリケーションは、bufio.Scannerではなくbufio.Readerを使用すべきです。

とのこと

ハイ。とりあえず今回得たいもの得るだけだったら適当に

package main

import "os"
import "fmt"
import "bufio"

func main() {

        var pool string

        file, _ := os.Open("sample.txt")
        scnr := bufio.NewScanner(file)

        scnr.Scan()
        pool += scnr.Text()

        scnr.Scan()
        pool += "\n" + scnr.Text()

        fmt.Println(pool)
}

雑感

  • Go言語の標準パッケージの中身を読むべきだと思っていたが、
  • まずはどういうパッケージがあるか俯瞰する必要があるっぽい
  • アホみたいにbufio.Readerだけで頑張って実装してたパッケージがforkされてScannerに置き換えられて使われてて、はずかしい思いをした

f:id:otiai10:20140824140410p:plain

DRY