はじめに (対象読者・この記事でわかること)

この記事は、Goでファイルやバイト列を部分的に読み取る際にio.SectionReaderを使っているけれど、「まだ読めるバイトが残っているかどうか」を確実に判断したい方を対象としています。io.SectionReaderio.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バイトを超えて読み進めることはありません。

しかし、多くのコード例では「Readio.EOFを返すまで無限ループ」というパターンを持ち込みがちで、セクション境界でio.EOFが返った瞬間にループを抜ければよいのですが、誤って次のReadを呼んでしまうと、二度目のio.EOFが返り、これを「異常終了」と捉えてエラーを返してしまうケースが見受けられます。

この問題を回避するには、「残バイト数」を事前に把握し、0バイトになる前にループを終了させることで、無駄なReadを発生させなくなります。

残バイトを正確に計算する実装パターン

以下、実装上「残バイト数」を正確に把握するための手順を示します。

Step1: Size()でセクション長を取得する

SectionReaderは自身のセクション長をSize() int64で公開しています。これはコンストラクタで指定したlengthと等しい値です。

Go
sec := io.NewSectionReader(reader, offset, length) total := sec.Size() // 残バイトの初期値

Size()は内部で保持している値を返すだけなので、システムコールも発生せず高速です。

Step2: 現在のオフセットをPos()で取得する

残念ながらSectionReaderにはPos()メソッドは存在しませんが、Seek(0, io.SeekCurrent)で現在位置を取得できます。

Go
cur, _ := sec.Seek(0, io.SeekCurrent) remain := total - cur

remainが正確な残バイト数になります。

Step3: ループを回す

remainが0を超えている間のみReadを呼び出します。

Go
buf := 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 }

使い方は以下の通り。

Go
for RemainSectionReader(sec) > 0 { n, _ := sec.Read(buf) // 処理 }

まとめ

本記事では、io.SectionReaderで残バイトを正確に把握する方法を解説しました。

  • Size()でセクション長を取得
  • Seek(0, io.SeekCurrent)で現在位置を取得
  • 両者の差分で残バイト数を算出し、ループ条件に組み込む

この記事を通して、無駄なReadを防ぎ、高速かつ予測可能なバイナリパース処理を書けるようになりました。

次回は、同様の発想でio.LimitedReaderの「残量」を正確に把握する方法や、bufio.Readerと組み合わせた際の注意点について掘り下げていきます。

参考資料