はじめに (対象読者・この記事でわかること)
この記事は、Goでファイルやバイト列を部分的に読み取る際にio.SectionReaderを使っているけれど、「まだ読めるバイトが残っているかどうか」を確実に判断したい方を対象としています。io.SectionReaderはio.Readerインターフェースを満たすため、通常のReadと同様にio.EOFを返してくれますが、セクションの境界で誤って余分なReadをしてしまうと、パフォーマンス低下や予期しないEOFエラーの原因になります。
この記事を読むことで、SectionReader.Size()とSectionReader.Seek()を組み合わせて「残バイト数」を正確に計算する方法、そしてそれをもとに安全にループを回す実装パターンを習得できます。結果として、無駄なシステムコールを減らし、高速かつ堅牢なバイナリパース処理を書けるようになります。
前提知識
この記事を読み進める上で、以下の知識があるとスムーズです。
- Goのio.Readerインターフェースの基本的な使い方
- io.EOFが返るタイミングの理解
- io.SectionReaderのコンストラクタio.NewSectionReaderの使い方
SectionReaderのEOFを正確に扱う理由
io.SectionReaderは、元となるio.ReaderAtの指定範囲(offset, length)のみを「仮想的なファイル」として見せるリーダーです。例えば、長さ1000バイトのファイルのうち、先頭から100バイト目から200バイトまでを切り出して扱いたいときに、コード上はoffset=100, length=200としてSectionReaderを作れば、以後Readを呼び出しても200バイトを超えて読み進めることはありません。
しかし、多くのコード例では「Readがio.EOFを返すまで無限ループ」というパターンを持ち込みがちで、セクション境界でio.EOFが返った瞬間にループを抜ければよいのですが、誤って次のReadを呼んでしまうと、二度目のio.EOFが返り、これを「異常終了」と捉えてエラーを返してしまうケースが見受けられます。
この問題を回避するには、「残バイト数」を事前に把握し、0バイトになる前にループを終了させることで、無駄なReadを発生させなくなります。
残バイトを正確に計算する実装パターン
以下、実装上「残バイト数」を正確に把握するための手順を示します。
Step1: Size()でセクション長を取得する
SectionReaderは自身のセクション長をSize() int64で公開しています。これはコンストラクタで指定したlengthと等しい値です。
Gosec := io.NewSectionReader(reader, offset, length) total := sec.Size() // 残バイトの初期値
Size()は内部で保持している値を返すだけなので、システムコールも発生せず高速です。
Step2: 現在のオフセットをPos()で取得する
残念ながらSectionReaderにはPos()メソッドは存在しませんが、Seek(0, io.SeekCurrent)で現在位置を取得できます。
Gocur, _ := sec.Seek(0, io.SeekCurrent) remain := total - cur
remainが正確な残バイト数になります。
Step3: ループを回す
remainが0を超えている間のみReadを呼び出します。
Gobuf := make([]byte, 1024) for remain > 0 { if remain < int64(len(buf)) { buf = buf[:remain] // 最後のブロックだけ小さく } n, err := sec.Read(buf) if err != nil && err != io.EOF { return fmt.Errorf("unexpected read error: %w", err) } // nバイト処理 remain -= int64(n) if err == io.EOF { break // 念のため } }
これで、セクション境界で無駄なReadを発生させることなく、確実に全バイトを処理できます。
ハマった点:Size()が「残量」ではないこと
最初はSize()を「残バイト数」と勘違いして、以下のように書いてしまいました。
Go// 誤り例 for sec.Size() > 0 { sec.Read(buf) }
Size()はセクション全体の長さであり、読み進めても減りません。正しくは「`Size() - 現在位置」で残量を計算する必要があります。
解決策:ヘルパー関数を書く
以下のようにヘルパーを用意しておくと、以後のコードがスッキリします。
Go// RemainSectionReader SectionReaderの残バイト数を返す func RemainSectionReader(sec *io.SectionReader) int64 { cur, _ := sec.Seek(0, io.SeekCurrent) return sec.Size() - cur }
使い方は以下の通り。
Gofor RemainSectionReader(sec) > 0 { n, _ := sec.Read(buf) // 処理 }
まとめ
本記事では、io.SectionReaderで残バイトを正確に把握する方法を解説しました。
Size()でセクション長を取得Seek(0, io.SeekCurrent)で現在位置を取得- 両者の差分で残バイト数を算出し、ループ条件に組み込む
この記事を通して、無駄なReadを防ぎ、高速かつ予測可能なバイナリパース処理を書けるようになりました。
次回は、同様の発想でio.LimitedReaderの「残量」を正確に把握する方法や、bufio.Readerと組み合わせた際の注意点について掘り下げていきます。
参考資料
