[インデックス 18920] ファイルの概要
このコミットは、Go言語の標準ライブラリ text/scanner
パッケージにおけるエラーハンドリングの改善に関するものです。具体的には、Scan
メソッドが io.Reader
からの読み込み時に io.EOF
以外のエラーを受け取った場合でも、読み込んだバイト数が0であればそのエラーを無視してしまう問題を修正しています。
コミット
commit b89a9fff5eee0eab8cb98d3c6532a8613dfdf580
Author: Rui Ueyama <ruiu@google.com>
Date: Fri Mar 21 17:05:57 2014 -0700
text/scanner: handle non-io.EOF errors
Currently Scan ignores an error returned from source if the number
of bytes source has read is 0.
Fixes #7594.
LGTM=gri
R=golang-codereviews, bradfitz, gri
CC=golang-codereviews
https://golang.org/cl/78120043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/b89a9fff5eee0eab8cb98d3c6532a8613dfdf580
元コミット内容
text/scanner
パッケージの Scan
メソッドが、ソース(io.Reader
)からの読み込み時に、読み込んだバイト数が0であった場合に io.EOF
以外のエラーを無視してしまう問題を修正します。
変更の背景
text/scanner
パッケージは、Go言語のソースコードやその他のテキストを字句解析(スキャン)するための機能を提供します。このパッケージは内部的に io.Reader
インターフェースを介して入力ソースからデータを読み込みます。
Go言語の io.Reader
インターフェースの Read
メソッドの規約では、読み込みが成功したバイト数 n
とエラー err
を返します。重要な点として、Read
メソッドは n > 0
であっても非nilのエラーを返すことが許されています(例: 部分的な読み込みの後にネットワークエラーが発生した場合など)。また、n = 0
で err != nil
の場合、これは通常、読み込みが全く進まなかったことを示し、エラーが発生したことを意味します。特に、io.EOF
は入力の終端に達したことを示す特別なエラーです。
このコミット以前の text/scanner
の実装では、next()
メソッド(内部で io.Reader
から読み込みを行う)において、Read
メソッドが n=0
かつ err != io.EOF
のエラーを返した場合に、そのエラーが適切に処理されず、無視されてしまうというバグがありました。これにより、入力ソースが io.EOF
ではない致命的なエラーを返しても、text/scanner
はそれを検知せず、あたかも入力が終了したかのように振る舞う可能性がありました。これは、不正な入力や、基盤となるI/Oシステムの問題を適切に報告できないという点で、堅牢性に欠ける挙動でした。
コミットメッセージに記載されている Fixes #7594
は、この特定のバグを追跡していたGoのIssue番号ですが、現在の公開されているIssueトラッカーでは直接参照できない可能性があります(古いIssueであるか、内部的なトラッカーの番号であるため)。しかし、コミットメッセージ自体が問題の核心を明確に説明しています。
前提知識の解説
text/scanner
パッケージ
text/scanner
パッケージは、Go言語の標準ライブラリ go/scanner
パッケージに似ていますが、より汎用的なテキストの字句解析を提供します。これは、プログラミング言語のパーサーや、設定ファイルの解析など、様々なテキスト処理タスクの基盤として利用できます。主な機能は以下の通りです。
- トークン化: 入力ストリームを意味のある最小単位(トークン)に分割します。
- 位置情報: 各トークンのソースコード上の正確な位置(行、列、オフセット)を追跡します。
- エラーハンドリング: 字句解析中に発生したエラーを報告するためのメカニズムを提供します。
Scanner
型は Init
メソッドで io.Reader
を受け取り、そこから文字を読み込みます。Scan
メソッドを呼び出すことで次のトークンを取得し、Next
メソッドで次の文字を読み込みます。
io.Reader
インターフェースとエラーハンドリング
Go言語の io.Reader
インターフェースは、データを読み込むための基本的なインターフェースです。
type Reader interface {
Read(p []byte) (n int, err error)
}
Read
メソッドは、p
に最大 len(p)
バイトを読み込み、読み込んだバイト数 n
とエラー err
を返します。
n > 0, err == nil
: 正常にn
バイト読み込んだ。n > 0, err != nil
:n
バイト読み込んだが、その後にエラーが発生した(例: 部分的な読み込み)。この場合、n
バイトは有効なデータとして扱われるべきです。n = 0, err == nil
: これは通常、有効なデータがないことを意味し、ブロックする可能性があるか、またはRead
が何も読み込まなかったことを示します。これは稀なケースであり、通常はRead
がブロックするか、n > 0
を返すか、err != nil
を返すかのいずれかです。n = 0, err != nil
: 読み込みが全く進まず、エラーが発生したことを意味します。err == io.EOF
: 入力の終端に達したことを示します。これはエラーではなく、正常な終了条件です。err != io.EOF
:io.EOF
以外のエラー(例: ネットワークエラー、ファイルシステムエラーなど)が発生し、読み込みが続行できないことを示します。これは通常、致命的なエラーとして扱われるべきです。
このコミットの修正は、特に n = 0
かつ err != io.EOF
のケースに焦点を当てています。
技術的詳細
text/scanner
パッケージの Scanner
型の next()
メソッドは、内部的に入力ソースから文字を読み込む役割を担っています。このメソッドは、s.src
(内部の io.Reader
) からデータを s.srcBuf
に読み込みます。
修正前のコードでは、s.src.Read(s.srcBuf[s.srcEnd:])
の呼び出し後、エラー err
が発生した場合の処理に問題がありました。
// 修正前 (簡略化)
if err != nil {
// ...
if s.srcEnd == 0 { // 読み込んだバイト数が0の場合
// ...
if err != io.EOF { // io.EOF ではないエラーの場合
s.error(err.Error()) // エラーハンドラを呼び出す
}
// ...
}
// ...
}
このロジックでは、s.srcEnd == 0
(つまり、Read
が0バイトを返した) かつ err != io.EOF
の場合にのみ s.error()
が呼び出されていました。しかし、s.srcEnd
は Read
が返した n
の値ではなく、バッファの現在の終端位置を示します。より正確には、n
は Read
が実際に読み込んだバイト数であり、s.srcEnd
は s.srcBuf
内の有効なデータの終端を指します。
問題は、Read
が n=0
と err != io.EOF
を返した場合に、そのエラーが s.srcEnd == 0
の条件の外側で適切に処理されていなかった点にあります。つまり、s.srcEnd
が0でなくても(以前の読み込みでバッファにデータが残っていたとしても)、現在の Read
呼び出しで n=0
かつ err != io.EOF
であれば、それはエラーとして報告されるべきでした。
修正は、Read
がエラーを返した直後に、n
の値(s.srcEnd
の増加量)に関わらず、io.EOF
ではないエラーであればすぐに s.error()
を呼び出すように変更しました。これにより、Read
が0バイトを返したかどうかに関わらず、非 io.EOF
エラーが即座に処理されるようになります。
新しいテストケース TestIOError
は、この修正の意図を明確に示しています。このテストでは、Read
メソッドが常に (0, io.ErrNoProgress)
を返すカスタムの errReader
を作成し、Scanner
がこの非 io.EOF
エラーを適切に捕捉し、エラーハンドラを呼び出すことを検証しています。
コアとなるコードの変更箇所
src/pkg/text/scanner/scanner.go
--- a/src/pkg/text/scanner/scanner.go
+++ b/src/pkg/text/scanner/scanner.go
@@ -240,12 +240,15 @@ func (s *Scanner) next() rune {
s.srcEnd = i + n
s.srcBuf[s.srcEnd] = utf8.RuneSelf // sentinel
if err != nil {
+ if err != io.EOF {
+ s.error(err.Error())
+ }
if s.srcEnd == 0 {
if s.lastCharLen > 0 {
// previous character was not EOF
s.error("I/O error")
}
s.lastCharLen = 0
return EOF
}
- if err != io.EOF {
- s.error(err.Error())
- }
// If err == EOF, we won't be getting more
// bytes; break to avoid infinite loop. If
// err is something else, we don't know if
src/pkg/text/scanner/scanner_test.go
--- a/src/pkg/text/scanner/scanner_test.go
+++ b/src/pkg/text/scanner/scanner_test.go
@@ -462,6 +462,33 @@ func TestError(t *testing.T) {
testError(t, `/*/`, "1:4", "comment not terminated", EOF)
}
+// An errReader returns (0, err) where err is not io.EOF.
+type errReader struct{}
+
+func (errReader) Read(b []byte) (int, error) {
+ return 0, io.ErrNoProgress // some error that is not io.EOF
+}
+
+func TestIOError(t *testing.T) {
+ s := new(Scanner).Init(errReader{})
+ errorCalled := false
+ s.Error = func(s *Scanner, msg string) {
+ if !errorCalled {
+ if want := io.ErrNoProgress.Error(); msg != want {
+ t.Errorf("msg = %q, want %q", msg, want)
+ }
+ errorCalled = true
+ }
+ }
+ tok := s.Scan()
+ if tok != EOF {
+ t.Errorf("tok = %s, want EOF", TokenString(tok))
+ }
+ if !errorCalled {
+ t.Errorf("error handler not called")
+ }
+}
+
func checkPos(t *testing.T, got, want Position) {
if got.Offset != want.Offset || got.Line != want.Line || got.Column != want.Column {
t.Errorf("got offset, line, column = %d, %d, %d; want %d, %d, %d",
コアとなるコードの解説
src/pkg/text/scanner/scanner.go
の変更点
Scanner.next()
メソッド内のエラーハンドリングロジックが変更されました。
変更前:
if err != nil
ブロック内で、s.srcEnd == 0
(読み込んだバイト数が0) の場合にのみ、err != io.EOF
であれば s.error(err.Error())
が呼び出されていました。
変更後:
if err != nil
ブロックに入った直後に、if err != io.EOF
のチェックが追加されました。これにより、Read
メソッドが io.EOF
ではないエラーを返した場合、読み込んだバイト数 n
が0であろうとなかろうと(ただし、この特定のケースでは n
は s.srcEnd
に加算されるため、s.srcEnd
が0でない場合でも n
が0である可能性はあります)、すぐに s.error()
メソッドが呼び出されるようになりました。
元の if s.srcEnd == 0
ブロック内の if err != io.EOF
のチェックは削除されました。これは、新しいロジックがより包括的であるため、冗長になったためです。
この変更により、text/scanner
は io.Reader
からの非 io.EOF
エラーをより確実に捕捉し、ユーザー定義のエラーハンドラ (s.Error
) を介して報告できるようになりました。
src/pkg/text/scanner/scanner_test.go
の変更点
新しいテストケース TestIOError
が追加されました。
-
errReader
型の定義:io.Reader
インターフェースを実装するカスタム型errReader
が定義されています。このRead
メソッドは常に(0, io.ErrNoProgress)
を返します。io.ErrNoProgress
はio.EOF
ではないエラーの一例です。 -
TestIOError
関数:new(Scanner).Init(errReader{})
を使用して、errReader
を入力ソースとする新しいScanner
インスタンスを作成します。s.Error
フィールドにカスタムのエラーハンドラ関数を設定します。このハンドラは、エラーが呼び出されたことを記録し、報告されたエラーメッセージが期待されるio.ErrNoProgress.Error()
と一致するかどうかを検証します。s.Scan()
を呼び出して、スキャン処理を開始します。errReader
の性質上、すぐにエラーが発生し、EOF
トークンが返されることが期待されます。tok != EOF
のチェックで、Scan
がEOF
を返したことを確認します。!errorCalled
のチェックで、カスタムエラーハンドラが実際に呼び出されたことを確認します。
このテストは、text/scanner
が io.EOF
ではない io.Reader
からのエラーを正しく検出し、処理できることを保証します。
関連リンク
- Go言語
text/scanner
パッケージのドキュメント: https://pkg.go.dev/text/scanner - Go言語
io
パッケージのドキュメント: https://pkg.go.dev/io
参考にした情報源リンク
- Go言語のコミット履歴 (GitHub): https://github.com/golang/go/commits/master
- Go言語の
io.Reader
インターフェースの規約に関する一般的な情報 (Goのドキュメントやブログ記事など) - Go言語の
io.EOF
とその他のエラーの区別に関する情報
(注: Fixes #7594
の具体的なIssueページは、公開されているGoのIssueトラッカーでは見つけることができませんでした。これは、Issueが非常に古いか、内部的なトラッカーの番号である可能性があります。しかし、コミットメッセージとコード変更から問題の性質は明確に理解できます。)