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

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

このコミットは、Go言語のstrconvパッケージにおいて、数値変換エラー(NumError)の際に返されるエラーメッセージの可読性を向上させるための変更です。具体的には、エラーメッセージに含まれる入力文字列をstrconv.Quote関数を使用してエスケープすることで、制御文字や非表示文字の存在を明確に表示できるようにしています。また、NumErrorの単体テストが追加され、この変更の動作が検証されています。

コミット

commit 1300fb54491496ac19b33c5ef3b4e92fbe89b4e4
Author: Matt Brown <mdbrown@google.com>
Date:   Thu Feb 28 10:08:05 2013 -0800

    strconv: use Quote to escape the input string for failed conversion errors
    
    This reveals the presence of control and non-printable characters in the
    errors returned by the Parse functions.  Also add unit tests for NumError.
    
    R=golang-dev, r, rsc
    CC=golang-dev
    https://golang.org/cl/7393075

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

https://github.com/golang/go/commit/1300fb54491496ac19b33c5ef3b4e92fbe89b4e4

元コミット内容

strconv: 変換失敗エラーの入力文字列をエスケープするためにQuoteを使用 これにより、Parse関数によって返されるエラー内の制御文字および非表示文字の存在が明らかになります。また、NumErrorの単体テストも追加します。

変更の背景

Go言語のstrconvパッケージは、文字列と数値の間の変換(例: ParseInt, ParseFloat)を提供します。これらの関数が変換に失敗した場合、NumError型のエラーが返されます。従来のNumErrorのエラーメッセージは、変換に失敗した入力文字列をそのまま表示していました。

この挙動には問題がありました。もし入力文字列にタブ、改行、NULL文字などの制御文字や、表示できない非ASCII文字が含まれていた場合、エラーメッセージ上ではこれらの文字が視覚的に区別できず、デバッグが困難になる可能性がありました。例えば、"1\x00.2"のような文字列が与えられた場合、エラーメッセージでは"parsing "1.2": failed"のように表示され、途中にNULL文字があることが一見して分かりませんでした。

このコミットは、このようなデバッグの困難さを解消するために、エラーメッセージ内の入力文字列をGoの文字列リテラル形式でエスケープ表示するように変更することを目的としています。これにより、非表示文字や制御文字が\x00\nのようなエスケープシーケンスとして明示的に表示され、開発者が問題の原因を特定しやすくなります。

前提知識の解説

strconvパッケージ

strconvパッケージは、Go言語の標準ライブラリの一部であり、基本的なデータ型(文字列、整数、浮動小数点数、真偽値)間の変換機能を提供します。例えば、ParseIntは文字列を整数に、ParseFloatは文字列を浮動小数点数に変換します。

strconv.NumError

NumErrorは、strconvパッケージ内で数値変換が失敗した場合に返されるエラー型です。この構造体は以下のフィールドを持ちます。

  • Func: エラーが発生した関数名(例: "ParseInt", "ParseFloat")。
  • Num: 変換に失敗した入力文字列。
  • Err: 根本的なエラー(例: ErrSyntaxErrRange)。

NumErrorError()メソッドを実装しており、これによりerrorインターフェースを満たします。このError()メソッドが、ユーザーに表示されるエラーメッセージの文字列を生成します。

strconv.Quote関数

strconv.Quote(s string) string関数は、Go言語のstrconvパッケージに属する関数です。この関数は、与えられた文字列sをGoのダブルクォートで囲まれた文字列リテラル形式に変換します。この変換の際、文字列内の特殊文字(例: \n\t"\)や非表示文字、制御文字は、Goのエスケープシーケンス(例: \n, \t, \", \\, \xNN, \uNNNN, \UNNNNNNNN)を使用して適切にエスケープされます。

例えば、strconv.Quote("Hello\nWorld")"\"Hello\\nWorld\""を返します。この機能は、文字列をログに出力したり、デバッグ情報を表示したりする際に、文字列の内容を正確に表現するために非常に有用です。特に、非表示文字や制御文字が含まれる場合に、それらの存在を明確に視覚化できます。

技術的詳細

このコミットの主要な変更点は、strconv.NumError型のError()メソッドの実装です。以前は、エラーメッセージを生成する際に、変換に失敗した入力文字列(e.Num)をそのまま埋め込んでいました。

変更後、e.Numstrconv.Quote(e.Num)によってエスケープされてからエラーメッセージに組み込まれるようになりました。これにより、以下のような効果が得られます。

  1. 非表示文字の可視化: 入力文字列にNULL文字(\x00)やその他の制御文字が含まれていた場合、これらが\x00のようなエスケープシーケンスとして明示的に表示されるようになります。これにより、エラーの原因が入力文字列の予期せぬ文字にある場合に、開発者がその問題を迅速に特定できるようになります。
  2. デバッグの容易性: エラーメッセージがより詳細で正確になるため、デバッグ作業が大幅に効率化されます。特に、ユーザーからの入力や外部システムからのデータに起因するパースエラーの場合、問題のある文字がどこにあるのかをすぐに把握できます。
  3. 一貫性: Goの文字列リテラル形式で表示されるため、Goのコードベース全体での文字列表現の一貫性が保たれます。

また、この変更の動作を検証するために、strconv/atof_test.gostrconv/atoi_test.goに新しいテストケースが追加されています。特にatoi_test.goにはTestNumErrorという新しいテスト関数が追加され、NumErrorQuote関数を正しく使用してエラーメッセージを生成していることを確認しています。これにより、将来的な回帰を防ぎ、変更の堅牢性を保証しています。

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

このコミットによる主要なコード変更は以下のファイルに集中しています。

  1. src/pkg/strconv/atoi.go:

    • NumError型のError()メソッドの実装が変更されました。
      --- a/src/pkg/strconv/atoi.go
      +++ b/src/pkg/strconv/atoi.go
      @@ -20,7 +20,7 @@ type NumError struct {
       }
       
       func (e *NumError) Error() string {
      -	return "strconv." + e.Func + ": " + `parsing "` + e.Num + `": ` + e.Err.Error()
      +	return "strconv." + e.Func + ": " + "parsing " + Quote(e.Num) + ": " + e.Err.Error()
       }
       
       func syntaxError(fn, str string) *NumError {
      
      この変更により、e.Num(変換に失敗した入力文字列)がQuote()関数によってエスケープされてからエラーメッセージに組み込まれるようになりました。
  2. src/pkg/strconv/atof_test.go:

    • atof_testスライスに新しいテストケースが追加されました。
      --- a/src/pkg/strconv/atof_test.go
      +++ b/src/pkg/strconv/atof_test.go
      @@ -110,6 +110,7 @@ var atoftests = []atofTest{
       	{"1e", "0", ErrSyntax},
       	{"1e-", "0", ErrSyntax},
       	{".e-1", "0", ErrSyntax},
      +\t{"1\x00.2", "0", ErrSyntax},
       
       	// http://www.exploringbinary.com/java-hangs-when-converting-2-2250738585072012e-308/
       	{"2.2250738585072012e-308", "2.2250738585072014e-308", nil},
      
      "1\x00.2"というNULL文字を含む文字列がErrSyntaxとなることを確認するテストが追加されました。
  3. src/pkg/strconv/atoi_test.go:

    • errorsパッケージのインポートが追加されました。
    • numErrorTestという新しい構造体とnumErrorTestsというテストデータスライスが定義されました。
    • TestNumErrorという新しいテスト関数が追加されました。
      --- a/src/pkg/strconv/atoi_test.go
      +++ b/src/pkg/strconv/atoi_test.go
      @@ -5,6 +5,7 @@
       package strconv_test
       
       import (
      +\t"errors"
       	"reflect"
       	. "strconv"
       	"testing"
      @@ -146,6 +147,16 @@ var atoi32tests = []atoi32Test{
       	{"-2147483649", -1 << 31, ErrRange},
       }
       
      +type numErrorTest struct {
      +\tnum, want string
      +}
      +
      +var numErrorTests = []numErrorTest{
      +\t{"0", `strconv.ParseFloat: parsing "0": failed`},
      +\t{"`", "strconv.ParseFloat: parsing \\\"`\\\": failed"},
      +\t{"1\x00.2", `strconv.ParseFloat: parsing "1\x00.2": failed`},
      +}
      +
       func init() {
       	// The atoi routines return NumErrors wrapping
       	// the error and the string.  Convert the tables above.
      @@ -277,6 +288,19 @@ func TestParseInt(t *testing.T) {
       	}
       }
       
      +func TestNumError(t *testing.T) {
      +\tfor _, test := range numErrorTests {
      +\t\terr := &NumError{
      +\t\t\tFunc: "ParseFloat",
      +\t\t\tNum:  test.num,\n+\t\t\tErr:  errors.New("failed"),
      +\t\t}
      +\t\tif got := err.Error(); got != test.want {
      +\t\t\tt.Errorf(`(&NumError{"ParseFloat", %q, "failed"}).Error() = %v, want %v`, test.num, got, test.want)
      +\t\t}\n+\t}\n+}\n+\n func BenchmarkAtoi(b *testing.B) {
       	for i := 0; i < b.N; i++ {
       	\tParseInt("12345678", 10, 0)
      
      TestNumErrorは、様々な入力文字列(通常の文字列、バッククォートを含む文字列、NULL文字を含む文字列)に対してNumErrorが生成するエラーメッセージが期待通りにQuote関数によってエスケープされているかを確認します。

コアとなるコードの解説

このコミットの核心は、strconv.NumErrorError()メソッドの変更です。

変更前:

func (e *NumError) Error() string {
	return "strconv." + e.Func + ": " + `parsing "` + e.Num + `": ` + e.Err.Error()
}

このコードでは、e.Num(変換に失敗した元の文字列)がそのままエラーメッセージの文字列リテラル内に埋め込まれていました。例えば、ParseInt("abc", 10, 0)が失敗した場合、strconv.ParseInt: parsing "abc": invalid syntaxのようなエラーメッセージが生成されます。しかし、もしe.Num"1\x002"のような非表示文字を含む場合、エラーメッセージはstrconv.ParseInt: parsing "12": invalid syntaxのように表示され、NULL文字の存在が隠蔽されていました。

変更後:

func (e *NumError) Error() string {
	return "strconv." + e.Func + ": " + "parsing " + Quote(e.Num) + ": " + e.Err.Error()
}

この変更では、e.Numを直接埋め込む代わりに、strconv.Quote(e.Num)の戻り値を使用しています。strconv.Quote関数は、入力文字列をGoの文字列リテラル形式にエスケープします。これにより、例えば"1\x002"という入力文字列の場合、Quote("1\x002")"\"1\\x002\""を返します。結果として、エラーメッセージはstrconv.ParseInt: parsing "1\x002": invalid syntaxのように表示され、NULL文字が\x00として明示的に示されるようになります。

この変更は、エラーメッセージのデバッグ情報を大幅に改善し、特に不正な入力データに起因するパースエラーのトラブルシューティングを容易にします。

追加されたテストケース、特にTestNumErrorは、この新しい挙動が期待通りに機能することを確認しています。 numErrorTestsスライスには、以下の3つのシナリオが含まれています。

  1. "0": 通常の数値文字列。エスケープ後も"0"として表示されることを確認。
  2. "`": バッククォートを含む文字列。バッククォートがエスケープされ、"\""\""のように表示されることを確認。
  3. "1\x00.2": NULL文字を含む文字列。NULL文字が\x00としてエスケープされ、"\"1\\x00.2\""のように表示されることを確認。

これらのテストは、Quote関数が様々な種類の文字を正しくエスケープし、それがNumErrorのエラーメッセージに反映されることを保証します。

関連リンク

参考にした情報源リンク