Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 13711] ファイルの概要

このコミットは、Go言語の標準ライブラリであるgo/scannerパッケージ内のエラー出力に関するバグ修正です。具体的には、エラーが存在しない場合にPrintError関数が不要な「ゴミ」を出力してしまう問題を解決しています。

コミット

commit d6c69dc602431828c5e5818b24bbf0593652480c
Author: Robert Griesemer <gri@golang.org>
Date:   Thu Aug 30 16:10:33 2012 -0700

    go/scanner: don't print garbage if there's no error
    
    R=r
    CC=golang-dev
    https://golang.org/cl/6489059

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/d6c69dc602431828c5e5818b24bbf0593652480c

元コミット内容

このコミットは、go/scannerパッケージのerrors.goファイルにあるPrintError関数において、エラーオブジェクトがnilであるにもかかわらず、elseブロックが実行され、結果として不必要な出力("garbage")が行われる可能性があった問題を修正しています。

変更の背景

go/scannerパッケージは、Goソースコードの字句解析(スキャン)を行うための機能を提供します。このパッケージには、解析中に発生したエラーを収集し、整形して出力するためのPrintError関数が含まれています。

変更前のPrintError関数は、引数として渡されたerrscanner.ErrorList型であるかどうかをチェックし、もしそうであればそのリスト内の全てのエラーを出力していました。しかし、errErrorList型ではない、かつnilである場合(つまり、エラーが全く発生していない場合)、elseブロックが実行されてしまい、fmt.Fprintf(w, "%s\n", err)という行が実行されていました。

Go言語において、nilerrorインターフェース値を%sフォーマット指定子で出力しようとすると、通常は<nil>という文字列が出力されます。このコミットの背景にある問題は、エラーがないにもかかわらず、この<nil>という文字列や、それに類する「ゴミ」が標準出力や指定されたio.Writerに書き込まれてしまうことでした。これは、エラーがないことを示すために何も出力すべきではないという期待に反する動作であり、ログのノイズになったり、後続の処理に影響を与えたりする可能性がありました。

このコミットは、このような不必要な出力を防ぎ、PrintError関数が真にエラーが存在する場合にのみ出力を行うようにするための修正です。

前提知識の解説

  1. go/scannerパッケージ: Go言語の標準ライブラリの一部で、Goソースコードの字句解析(lexical analysis)を行うための機能を提供します。ソースコードをトークン(識別子、キーワード、演算子など)のストリームに変換する役割を担います。このパッケージは、コンパイラやリンター、コードフォーマッターなど、Goコードを扱う様々なツールで利用されます。

  2. errorインターフェース: Go言語におけるエラーハンドリングの基本的な仕組みです。errorは組み込みのインターフェースであり、Error() stringというメソッドを一つだけ持ちます。このメソッドは、エラーの文字列表現を返します。関数がエラーを返す場合、通常はerror型の値を返します。エラーがない場合はnilを返します。

  3. scanner.ErrorList: go/scannerパッケージ内で定義されている型で、複数のスキャンエラーをまとめるためのリストです。このErrorList型もerrorインターフェースを実装しており、Error()メソッドを呼び出すと、リスト内の全てのエラーメッセージを結合した文字列を返します。

  4. io.Writerインターフェース: Go言語におけるI/O操作の基本的なインターフェースの一つです。Write([]byte) (n int, err error)メソッドを持ち、バイトスライスを書き込む機能を提供します。os.Stdout(標準出力)、bytes.Buffer、ファイルなど、様々な出力先がこのインターフェースを実装しています。

  5. fmt.Fprintf関数: fmtパッケージの関数で、指定されたio.Writerにフォーマットされた文字列を書き込みます。fmt.Fprintf(w, format, a...)のように使用し、wio.Writerformatにフォーマット文字列、aに可変引数を取ります。%sは文字列をフォーマットするための動詞です。

  6. 型アサーション (value.(type)): Go言語の機能で、インターフェース型の変数が特定の具象型であるかどうかをチェックし、もしそうであればその具象型の値を取り出すことができます。value, ok := err.(ErrorList)のように使用し、okは型アサーションが成功したかどうかを示す真偽値です。

技術的詳細

src/pkg/go/scanner/errors.goファイル内のPrintError関数は、以下のような構造を持っていました(変更前):

func PrintError(w io.Writer, err error) {
	if list, ok := err.(ErrorList); ok {
		// errがErrorList型の場合、リスト内の各エラーを出力
		for _, e := range list {
			fmt.Fprintf(w, "%s\n", e)
		}
	} else {
		// errがErrorList型ではない場合、err自体を出力
		fmt.Fprintf(w, "%s\n", err)
	}
}

このロジックの問題点は、errnilの場合に発生します。

  1. errnilの場合、if list, ok := err.(ErrorList); okの型アサーションは失敗します。なぜなら、nilErrorList型ではないからです。
  2. 型アサーションが失敗すると、okfalseとなり、実行はelseブロックに移ります。
  3. elseブロック内でfmt.Fprintf(w, "%s\n", err)が実行されます。このとき、errnilであるため、fmtパッケージはnilerrorインターフェース値を文字列としてフォーマットしようとします。結果として、wには通常<nil>\nという文字列が書き込まれます。

この<nil>という出力は、エラーがないにもかかわらず出力されるため、「ゴミ」と表現されていました。

今回の修正は、このelseブロックの条件をより厳密にすることで、errnilの場合にこのブロックが実行されないようにします。

変更後のロジックは以下のようになります:

func PrintError(w io.Writer, err error) {
	if list, ok := err.(ErrorList); ok {
		// errがErrorList型の場合、リスト内の各エラーを出力
		for _, e := range list {
			fmt.Fprintf(w, "%s\n", e)
		}
	} else if err != nil { // ここが変更点
		// errがErrorList型ではない、かつerrがnilではない場合のみ、err自体を出力
		fmt.Fprintf(w, "%s\n", err)
	}
}

else if err != nilという条件が追加されたことで、errErrorList型ではない場合に加えて、errnilではないという条件も満たされた場合にのみ、fmt.Fprintf(w, "%s\n", err)が実行されるようになります。これにより、errnilの場合にはどのブロックも実行されなくなり、何も出力されないという期待通りの動作が実現されます。

この修正は、Go言語におけるエラーハンドリングの慣習、すなわち「エラーがない場合はnilを返し、その際には何も出力しない」という原則に合致させるためのものです。

コアとなるコードの変更箇所

変更はsrc/pkg/go/scanner/errors.goファイル内のPrintError関数の一箇所のみです。

--- a/src/pkg/go/scanner/errors.go
+++ b/src/pkg/go/scanner/errors.go
@@ -120,7 +120,7 @@ func PrintError(w io.Writer, err error) {
 		for _, e := range list {
 			fmt.Fprintf(w, "%s\n", e)
 		}
-	} else {
+	} else if err != nil {
 		fmt.Fprintf(w, "%s\n", err)
 	}\n
 }

具体的には、123行目の}の直後にあるelse {else if err != nil {に変更されています。

コアとなるコードの解説

この変更は、PrintError関数がエラーを整形して出力する際の条件をより厳密にすることで、不必要な出力を防ぐことを目的としています。

  • 変更前: else { ... } errscanner.ErrorList型でなかった場合、無条件にこのブロックが実行されていました。これにはerrnilであるケースも含まれていました。nilerror%sでフォーマットすると、<nil>という文字列が出力されてしまい、エラーがないにもかかわらず出力が行われるという問題がありました。

  • 変更後: else if err != nil { ... } elseif err != nilという条件が追加されました。これにより、errscanner.ErrorList型ではない、かつ errnilではない場合にのみ、このブロックが実行されるようになります。 もしerrnilであれば、最初のif条件(errErrorList型かどうかのチェック)も、このelse if条件(errnilではないかどうかのチェック)も満たさないため、PrintError関数は何も出力せずに終了します。

この修正により、PrintError関数は、実際にエラーオブジェクトが存在し、かつそれがErrorList型ではない単一のエラーである場合にのみ、そのエラーメッセージを出力するようになります。エラーが全くない(errnil)場合には、何も出力されないという、よりクリーンで期待通りの動作が実現されました。

関連リンク

参考にした情報源リンク

  • このコミットのGerrit Change-ID: https://golang.org/cl/6489059 (コミットメッセージに記載されているリンク)
  • Go言語のソースコードリポジトリ: https://github.com/golang/go
  • Go言語の公式ドキュメント全般
  • Go言語のエラーハンドリングに関する一般的な知識