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

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

このコミットは、Go言語の標準ライブラリであるstrconvパッケージにおけるエラーメッセージの改善を目的としています。具体的には、数値変換エラー(NumError)が発生した際に、エラー文字列にどのstrconvパッケージの関数(例: ParseInt, ParseUint, ParseFloat)でエラーが発生したかを含めるように変更されています。これにより、エラーの発生源をより明確にし、デバッグを容易にすることが意図されています。

コミット

commit 02f6719d44a0a70f0fddfa3228ca9225f06d766c
Author: Rob Pike <r@golang.org>
Date:   Tue Dec 13 10:42:05 2011 -0800

    strconv: include package and function name in error strings
    Fixes #2548.
    
    R=golang-dev, rsc
    CC=golang-dev
    https://golang.org/cl/5484062

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

https://github.com/golang/go/commit/02f6719d44a0a70f0fddfa3228ca9225f06d766c

元コミット内容

strconv: include package and function name in error strings Fixes #2548.

このコミットは、strconvパッケージのエラー文字列にパッケージ名と関数名を含めるように変更します。これはIssue #2548を修正するものです。

変更の背景

Go言語のstrconvパッケージは、文字列と数値の相互変換を行うための機能を提供します。例えば、ParseIntは文字列を整数に、ParseFloatは文字列を浮動小数点数に変換します。これらの変換処理中に、入力文字列が不正な形式であったり、変換結果が指定された型や範囲に収まらない場合、エラーが発生します。

このコミット以前は、strconvパッケージから返されるエラーメッセージは、エラーの原因(例: invalid syntaxvalue out of range)と不正な入力文字列は示していましたが、どのstrconv関数がエラーを発生させたのかが明確ではありませんでした。

例えば、strconv.ParseInt("foo", 10, 64)strconv.ParseUint("bar", 10, 64)の両方がparsing "..." invalid syntaxのようなエラーを返す場合、エラーメッセージだけではどちらの関数が問題を引き起こしたのかを特定するのが困難でした。特に、複雑なアプリケーションでは、複数の場所でstrconv関数が使用されるため、エラーのデバッグや原因特定に時間がかかる可能性がありました。

この問題を解決するため、エラーメッセージにパッケージ名(strconv)とエラーを発生させた関数名(例: ParseIntParseUint)を含めることで、エラーの発生源をより具体的にし、開発者が問題を迅速に特定・解決できるようにすることが、この変更の背景にあります。これは、Go言語のエラーハンドリングの原則である「エラーは詳細であるべき」という考え方にも合致しています。

前提知識の解説

このコミットを理解するためには、以下のGo言語の基本的な概念とstrconvパッケージの知識が必要です。

Go言語のエラーハンドリング

Go言語では、エラーは通常、関数の最後の戻り値としてerrorインターフェース型で返されます。errorインターフェースは、Error() stringという単一のメソッドを持つシンプルなインターフェースです。このメソッドは、エラーに関する説明的な文字列を返します。

type error interface {
    Error() string
}

開発者は、カスタムエラー型を定義することで、より詳細なエラー情報を提供できます。カスタムエラー型は、errorインターフェースを実装する任意の構造体です。

strconvパッケージ

strconvパッケージは、文字列と基本的なデータ型(ブール値、整数、浮動小数点数)の間で変換を行うための関数を提供します。主な関数には以下のようなものがあります。

  • ParseBool(s string) (bool, error): 文字列をブール値に変換します。
  • ParseInt(s string, base int, bitSize int) (int64, error): 文字列を符号付き整数に変換します。baseは基数(2から36)、bitSizeは結果のビットサイズ(0, 8, 16, 32, 64)を指定します。
  • ParseUint(s string, base int, bitSize int) (uint64, error): 文字列を符号なし整数に変換します。
  • ParseFloat(s string, bitSize int) (float64, error): 文字列を浮動小数点数に変換します。bitSizeは結果のビットサイズ(32, 64)を指定します。

strconv.NumError

strconvパッケージの変換関数が失敗した場合、通常は*strconv.NumError型のエラーを返します。このNumError構造体は、変換が失敗した原因に関する情報を含んでいます。

このコミット以前のNumErrorの定義は以下のようでした。

type NumError struct {
    Num string // the input string that caused the error
    Err error  // the reason the conversion failed (ErrRange, ErrSyntax)
}

そして、そのError()メソッドは、parsing "入力文字列": エラー原因のような形式の文字列を返していました。

  • ErrSyntax: 入力文字列が正しい構文ではない場合に発生します(例: "abc"を整数に変換しようとした場合)。
  • ErrRange: 変換結果が指定された型や範囲に収まらない場合に発生します(例: "256"をuint8に変換しようとした場合)。

このコミットは、このNumError構造体とError()メソッドの挙動を変更し、エラーメッセージにコンテキストを追加します。

技術的詳細

このコミットの技術的な核心は、strconv.NumError構造体の変更と、エラー生成ヘルパー関数の導入、そして既存のstrconv関数からのエラー生成箇所の更新です。

  1. NumError構造体の変更: NumError構造体にFunc stringフィールドが追加されました。このフィールドは、エラーを発生させたstrconvパッケージ内の関数名(例: "ParseInt", "ParseUint", "ParseFloat", "ParseBool")を格納するために使用されます。

    変更前:

    type NumError struct {
        Num string // the input
        Err error  // the reason the conversion failed (ErrRange, ErrSyntax)
    }
    

    変更後:

    type NumError struct {
        Func string // the failing function (ParseBool, ParseInt, ParseUint, ParseFloat)
        Num  string // the input
        Err  error  // the reason the conversion failed (ErrRange, ErrSyntax)
    }
    
  2. NumError.Error()メソッドの変更: NumErrorError()メソッドが変更され、Funcフィールドの値がエラー文字列の先頭に追加されるようになりました。これにより、エラーメッセージはstrconv.関数名: parsing "入力文字列": エラー原因という形式になります。

    変更前:

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

    変更後:

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

    この変更により、例えばstrconv.ParseUintが範囲外エラーを返した場合、エラーメッセージはstrconv.ParseUint: parsing "256": value out of rangeのようになります。

  3. エラー生成ヘルパー関数の導入: エラー生成を簡素化し、一貫性を保つために、以下の2つのヘルパー関数がstrconv/atoi.goに導入されました。

    • syntaxError(fn, str string) *NumError: 構文エラー(ErrSyntax)を表すNumErrorを生成します。
    • rangeError(fn, str string) *NumError: 範囲エラー(ErrRange)を表すNumErrorを生成します。
    func syntaxError(fn, str string) *NumError {
        return &NumError{fn, str, ErrSyntax}
    }
    
    func rangeError(fn, str string) *NumError {
        return &NumError{fn, str, ErrRange}
    }
    

    これらのヘルパー関数は、NumErrorFuncフィールドに適切な関数名を自動的に設定します。

  4. 既存のstrconv関数の更新: strconvパッケージ内のParseBool, ParseInt, ParseUint, ParseFloatなどの関数で、エラーを生成する箇所が、新しいNumError構造体とヘルパー関数を使用するように更新されました。これにより、これらの関数が返すエラーには、常に適切な関数名が含まれるようになります。

    例えば、ParseUintでは&NumError{s0, err}&NumError{"ParseUint", s0, err}に、ParseIntでは&NumError{s, ErrSyntax}syntaxError(fnParseInt, s)に置き換えられています。また、ParseIntのように内部でParseUintを呼び出す関数では、ParseUintから返されたNumErrorFuncフィールドをParseIntに上書きする処理も追加されています。

  5. テストコードの更新: エラーメッセージの形式が変更されたため、strconvパッケージのテストコード(特にatof_test.go, atoi_test.go, exp/sql/convert_test.go)も、新しいエラー文字列の形式に合うように更新されました。これにより、テストが引き続き正しく機能し、新しいエラー形式が期待通りであることを保証します。

これらの変更により、strconvパッケージから返されるエラーは、より情報豊富でデバッグしやすいものになりました。

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

このコミットで変更された主要なファイルとコードスニペットは以下の通りです。

src/pkg/strconv/atoi.go

NumError構造体の定義が変更され、Funcフィールドが追加されました。 Error()メソッドが変更され、Funcフィールドの値がエラー文字列に含まれるようになりました。 syntaxErrorrangeErrorヘルパー関数が追加されました。 ParseUintおよびParseInt関数内でNumErrorを生成する箇所が更新されました。

--- a/src/pkg/strconv/atoi.go
+++ b/src/pkg/strconv/atoi.go
@@ -14,11 +14,22 @@ var ErrSyntax = errors.New("invalid syntax")
 
 // A NumError records a failed conversion.
 type NumError struct {
-	Num string // the input
-	Err error  // the reason the conversion failed (ErrRange, ErrSyntax)
+	Func string // the failing function (ParseBool, ParseInt, ParseUint, ParseFloat)
+	Num  string // the input
+	Err  error  // the reason the conversion failed (ErrRange, ErrSyntax)
 }
 
-func (e *NumError) Error() string { return `parsing "` + e.Num + `": ` + e.Err.Error() }
+func (e *NumError) Error() string {
+	return "strconv." + e.Func + ": " + `parsing "` + e.Num + `": ` + e.Err.Error()
+}
+
+func syntaxError(fn, str string) *NumError {
+	return &NumError{fn, str, ErrSyntax}
+}
+
+func rangeError(fn, str string) *NumError {
+	return &NumError{fn, str, ErrRange}
+}
 
 const intSize = 32 << uint(^uint(0)>>63)
 
@@ -116,7 +127,7 @@ func ParseUint(s string, b int, bitSize int) (n uint64, err error) {
 	return n, nil
 
 Error:
-	return n, &NumError{s0, err}
+	return n, &NumError{"ParseUint", s0, err}
 }
 
 // ParseInt interprets a string s in the given base (2 to 36) and
@@ -134,13 +145,15 @@ Error:
 // to s cannot be represented by a signed integer of the
 // given size, err.Error = ErrRange.
 func ParseInt(s string, base int, bitSize int) (i int64, err error) {
+	const fnParseInt = "ParseInt"
+
 	if bitSize == 0 {
 		bitSize = int(IntSize)
 	}
 
 	// Empty string bad.
 	if len(s) == 0 {
-		return 0, &NumError{s, ErrSyntax}
+		return 0, syntaxError(fnParseInt, s)
 	}
 
 	// Pick off leading sign.
@@ -157,15 +170,16 @@ func ParseInt(s string, base int, bitSize int) (i int64, err error) {
 	var un uint64
 	un, err = ParseUint(s, base, bitSize)
 	if err != nil && err.(*NumError).Err != ErrRange {
+		err.(*NumError).Func = fnParseInt
 		err.(*NumError).Num = s0
 		return 0, err
 	}
 	cutoff := uint64(1 << uint(bitSize-1))
 	if !neg && un >= cutoff {
-		return int64(cutoff - 1), &NumError{s0, ErrRange}
+		return int64(cutoff - 1), rangeError(fnParseInt, s0)
 	}
 	if neg && un > cutoff {
-		return -int64(cutoff), &NumError{s0, ErrRange}
+		return -int64(cutoff), rangeError(fnParseInt, s0)
 	}
 	n := int64(un)
 	if neg {

src/pkg/strconv/atob.go

ParseBool関数内でNumErrorを生成する箇所が更新されました。

--- a/src/pkg/strconv/atob.go
+++ b/src/pkg/strconv/atob.go
@@ -14,7 +14,7 @@ func ParseBool(str string) (value bool, err error) {
 	case "0", "f", "F", "false", "FALSE", "False":
 		return false, nil
 	}\n-	return false, &NumError{str, ErrSyntax}\n+	return false, syntaxError("ParseBool", str)\n }\n \n // FormatBool returns "true" or "false" according to the value of b

src/pkg/strconv/atof.go

atof32およびatof64関数内でNumErrorを生成する箇所が更新されました。

--- a/src/pkg/strconv/atof.go
+++ b/src/pkg/strconv/atof.go
@@ -338,6 +338,8 @@ func (d *decimal) atof32() (f float32, ok bool) {
 	return
 }
 
+const fnParseFloat = "ParseFloat"
+
 func atof32(s string) (f float32, err error) {
 	if val, ok := special(s); ok {
 		return float32(val), nil
@@ -345,7 +347,7 @@ func atof32(s string) (f float32, err error) {
 
 	var d decimal
 	if !d.set(s) {
-		return 0, &NumError{s, ErrSyntax}
+		return 0, syntaxError(fnParseFloat, s)
 	}
 	if optimize {
 		if f, ok := d.atof32(); ok {
@@ -355,7 +357,7 @@ func atof32(s string) (f float32, err error) {
 	b, ovf := d.floatBits(&float32info)
 	f = math.Float32frombits(uint32(b))
 	if ovf {
-		err = &NumError{s, ErrRange}
+		err = rangeError(fnParseFloat, s)
 	}
 	return f, err
 }
@@ -367,7 +369,7 @@ func atof64(s string) (f float64, err error) {
 
 	var d decimal
 	if !d.set(s) {
-		return 0, &NumError{s, ErrSyntax}
+		return 0, syntaxError(fnParseFloat, s)
 	}
 	if optimize {
 		if f, ok := d.atof64(); ok {
@@ -377,7 +379,7 @@ func atof64(s string) (f float64, err error) {
 	b, ovf := d.floatBits(&float64info)
 	f = math.Float64frombits(b)
 	if ovf {
-		err = &NumError{s, ErrRange}
+		err = rangeError(fnParseFloat, s)
 	}
 	return f, err
 }

テストファイルの変更

src/pkg/exp/sql/convert_test.go, src/pkg/strconv/atof_test.go, src/pkg/strconv/atoi_test.goでは、期待されるエラー文字列が新しい形式に合わせて更新されています。

例: src/pkg/exp/sql/convert_test.go

--- a/src/pkg/exp/sql/convert_test.go
+++ b/src/pkg/exp/sql/convert_test.go
@@ -55,10 +55,10 @@ var conversionTests = []conversionTest{
 
 	// Strings to integers
 	{s: "255", d: &scanuint8, wantuint: 255},
-	{s: "256", d: &scanuint8, wanterr: `converting string "256" to a uint8: parsing "256": value out of range`},
+	{s: "256", d: &scanuint8, wanterr: `converting string "256" to a uint8: strconv.ParseUint: parsing "256": value out of range`},
 	{s: "256", d: &scanuint16, wantuint: 256},
 	{s: "-1", d: &scanint, wantint: -1},
-	{s: "foo", d: &scanint, wanterr: `converting string "foo" to a int: parsing "foo": invalid syntax`},
+	{s: "foo", d: &scanint, wanterr: `converting string "foo" to a int: strconv.ParseInt: parsing "foo": invalid syntax`},
 
 	// True bools
 	{s: true, d: &scanbool, wantbool: true},

コアとなるコードの解説

NumError構造体の変更とError()メソッド

NumError構造体にFunc stringフィールドが追加されたことで、エラーが発生したstrconv関数名を格納できるようになりました。これは、エラーメッセージにコンテキストを追加するための最も重要な変更点です。

Error()メソッドの変更は、このFuncフィールドを活用して、エラーメッセージのフォーマットをstrconv.関数名: parsing "入力文字列": エラー原因という形式に統一します。これにより、エラーログやデバッグ時に、どのstrconv関数が問題を引き起こしたのかが一目でわかるようになります。例えば、以前は単にparsing "256": value out of rangeと表示されていたエラーが、strconv.ParseUint: parsing "256": value out of rangeと表示されるようになります。

syntaxErrorrangeErrorヘルパー関数

これらのヘルパー関数は、NumErrorインスタンスを生成する際のボイラープレートコードを削減し、エラー生成の一貫性を保証します。strconvパッケージ内の複数の場所でエラーが生成されるため、これらのヘルパー関数を使用することで、コードの重複を避け、将来的なエラーメッセージのフォーマット変更にも柔軟に対応できるようになります。

例えば、以前はreturn 0, &NumError{s, ErrSyntax}のように直接NumErrorを生成していましたが、新しいコードではreturn 0, syntaxError(fnParseFloat, s)のようにヘルパー関数を呼び出すことで、より簡潔かつ意図が明確なコードになります。また、Funcフィールドに適切な関数名が自動的に設定されるため、開発者が手動で設定する手間が省け、ミスも減ります。

ParseIntにおけるFuncフィールドの上書き

ParseInt関数は内部でParseUintを呼び出すことがあります。ParseUintがエラーを返した場合、そのエラーは*NumError型であり、そのFuncフィールドは"ParseUint"に設定されています。しかし、ユーザーが直接呼び出したのはParseIntであるため、エラーメッセージにはParseIntが原因であることが示されるべきです。

このため、ParseInt内でParseUintから返されたエラーがNumError型であり、かつErrRange以外のエラーである場合に、err.(*NumError).Func = fnParseIntという行でFuncフィールドを"ParseInt"に上書きしています。これにより、エラーメッセージがユーザーの期待に沿ったものになります。

この変更は、Go言語のエラーハンドリングにおける「エラーはコンテキストを持つべきである」という原則を具体的に示しています。エラーメッセージに詳細なコンテキストを含めることで、開発者は問題をより迅速に診断し、解決することができます。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • GitHubのGoリポジトリのコミット履歴とIssueトラッカー
  • Go言語のエラーハンドリングに関する一般的な知識
  • strconvパッケージのソースコード分析