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

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

このコミットは、Go言語の標準ライブラリであるstrconvパッケージにおけるエラーハンドリングの改善を目的としています。具体的には、数値変換や文字列操作の際に発生するエラーについて、汎用的なos.EINVAL(無効な引数)やos.ERANGE(範囲外)ではなく、より具体的で意味のあるエラー型を導入し、それらを使用するように変更しています。これにより、エラーの原因をより明確に特定できるようになり、開発者にとってデバッグやエラー処理の実装が容易になります。

コミット

commit c1178aae865455b94a7b5c90c601a5719d96593b
Author: Russ Cox <rsc@golang.org>
Date:   Thu Oct 27 19:46:31 2011 -0700

    strconv: use better errors than os.EINVAL, os.ERANGE
    
    R=golang-dev, adg
    CC=golang-dev
    https://golang.org/cl/5327052

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

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

元コミット内容

strconv: use better errors than os.EINVAL, os.ERANGE

このコミットの目的は、strconvパッケージ内で使用されるエラーを、os.EINVALos.ERANGEといった一般的なエラーから、より具体的なエラーに置き換えることです。

変更の背景

Go言語の初期のバージョンでは、システムコールや一般的な操作に関連するエラーを示すためにosパッケージ内のエラー定数(例: os.EINVAL, os.ERANGE)が広く使用されていました。しかし、これらのエラーは非常に汎用的であり、エラーが発生した具体的な原因を特定するのが難しいという問題がありました。

例えば、文字列を数値に変換する際に「無効な引数」エラー(os.EINVAL)が発生した場合、それが文字列のフォーマットが間違っているためなのか、それとも他の何らかの理由によるものなのかが、エラーメッセージだけでは判別しにくいという課題がありました。同様に、「範囲外」エラー(os.ERANGE)も、数値がオーバーフローしたのか、アンダーフローしたのか、あるいはターゲットの型で表現できないほど大きすぎる/小さすぎるのか、といった詳細が不明瞭でした。

このような背景から、strconvパッケージのように特定のデータ型変換を扱うライブラリにおいては、よりセマンティックな(意味のある)エラー型を導入することで、開発者がエラーをより正確に理解し、適切なエラーハンドリングロジックを実装できるようにする必要がありました。このコミットは、その改善の一環として、ErrSyntaxErrRangeという2つの新しいエラー型を導入し、既存の汎用エラーを置き換えることで、エラー情報の粒度を高めています。

前提知識の解説

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

  • os.Error インターフェース: Go言語におけるエラーは、組み込みのerrorインターフェース(Go 1.0以前はos.Error)によって表現されます。このインターフェースは、Error() stringというメソッドを1つだけ持ち、エラーメッセージを文字列として返します。
  • os.EINVAL: osパッケージで定義されているエラー定数の一つで、「無効な引数」を意味します。関数に渡された引数が期待される形式や値ではない場合に返されることがあります。
  • os.ERANGE: osパッケージで定義されているエラー定数の一つで、「範囲外」を意味します。数値変換などで、結果がターゲットの型の表現可能な範囲を超えた場合に返されることがあります。
  • strconvパッケージ: Go言語の標準ライブラリの一つで、文字列と基本的なデータ型(ブール値、整数、浮動小数点数など)との間の変換機能を提供します。例えば、Atoi(文字列を整数に変換)、ParseFloat(文字列を浮動小数点数に変換)などの関数があります。
  • NumError構造体: strconvパッケージ内で定義されているエラー型で、数値変換が失敗した際に詳細な情報を提供するために使用されます。この構造体は、変換を試みた元の文字列(Numフィールド)と、変換が失敗した具体的な理由を示すエラー(Errorフィールド)を含みます。

このコミットの変更は、NumError構造体のErrorフィールドに設定される値が、より具体的なErrSyntaxErrRangeになるという点にあります。

技術的詳細

このコミットの主要な技術的変更点は以下の通りです。

  1. 新しいエラー定数の導入:

    • ErrRange: os.NewError("value out of range")として定義され、値がターゲットの型にとって範囲外であることを示します。これは主に数値のオーバーフローやアンダーフローの際に使用されます。
    • ErrSyntax: os.NewError("invalid syntax")として定義され、値がターゲットの型にとって正しい構文を持っていないことを示します。これは、例えば文字列が数値としてパースできない形式である場合などに使用されます。
  2. 既存コードでのエラー定数の置き換え:

    • strconvパッケージ内のatob.go(文字列からブール値への変換)、atof.go(文字列から浮動小数点数への変換)、atoi.go(文字列から整数への変換)、quote.go(文字列のクォート/アンクォート)など、広範囲にわたるファイルで、os.EINVALErrSyntaxに、os.ERANGEErrRangeに置き換えられています。
    • これにより、NumError構造体のErrorフィールドを通じて返されるエラーが、より具体的になります。例えば、以前はparsing "foo": invalid argumentのようなメッセージだったものが、parsing "foo": invalid syntaxのようになることで、エラーの原因が構文エラーであることが明確になります。
  3. テストコードの更新:

    • 上記のエラー定数の置き換えに伴い、各変換関数のテストファイル(例: atob_test.go, atof_test.go, atoi_test.go, quote_test.go)も更新され、期待されるエラーが新しいErrSyntaxErrRangeに修正されています。これにより、変更が正しく機能していることが保証されます。

この変更は、Go言語のエラーハンドリングの哲学、すなわち「エラーは値である」という考え方に基づいています。エラーを単なるブール値やnilでなく、具体的な情報を持つ値として扱うことで、より堅牢で分かりやすいプログラムを構築できるようになります。

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

このコミットで最も重要な変更は、src/pkg/strconv/atoi.goファイルにおける新しいエラー定数の定義と、それらの定数が各変換関数でどのように使用されるかを示す部分です。

--- a/src/pkg/strconv/atoi.go
+++ b/src/pkg/strconv/atoi.go
@@ -6,9 +6,16 @@ package strconv
 
 import "os"
 
+// ErrRange indicates that a value is out of range for the target type.
+var ErrRange = os.NewError("value out of range")
+// ErrSyntax indicates that a value does not have the right syntax for the target type.
+var ErrSyntax = os.NewError("invalid syntax")
+// A NumError records a failed conversion.
 type NumError struct {
-	Num   string
-	Error os.Error
+	Num   string   // the input
+	Error os.Error // the reason the conversion failed (ErrRange, ErrSyntax)
 }
 
 func (e *NumError) String() string { return `parsing "` + e.Num + `": ` + e.Error.String() }
@@ -38,15 +45,15 @@ func cutoff64(base int) uint64 {
 //
 // The errors that Btoui64 returns have concrete type *NumError
 // and include err.Num = s.  If s is empty or contains invalid
-// digits, err.Error = os.EINVAL; if the value corresponding
-// to s cannot be represented by a uint64, err.Error = os.ERANGE.\n
+// digits, err.Error = ErrSyntax; if the value corresponding
+// to s cannot be represented by a uint64, err.Error = ErrRange.
 func Btoui64(s string, b int) (n uint64, err os.Error) {
 	var cutoff uint64
 
 	s0 := s
 	switch {
 	case len(s) < 1:
-		err = os.EINVAL
+		err = ErrSyntax
 		goto Error
 
 	case 2 <= b && b <= 36:
@@ -59,7 +66,7 @@ func Btoui64(s string, b int) (n uint64, err os.Error) {
 		case s[0] == '0':
 			b = 8
 			if len(s) < 1 {
-				err = os.EINVAL
+				err = ErrSyntax
 				goto Error
 			}
 		default:
@@ -88,19 +95,19 @@ func Btoui64(s string, b int) (n uint64, err os.Error) {
 			v = d - 'A' + 10
 		default:
 			n = 0
-			err = os.EINVAL
+			err = ErrSyntax
 			goto Error
 		}
 		if int(v) >= b {
 			n = 0
-			err = os.EINVAL
+			err = ErrSyntax
 			goto Error
 		}
 
 		if n >= cutoff {
 			// n*b overflows
 			n = 1<<64 - 1
-			err = os.ERANGE
+			err = ErrRange
 			goto Error
 		}
 		n *= uint64(b)
@@ -109,7 +116,7 @@ func Btoui64(s string, b int) (n uint64, err os.Error) {
 		if n1 < n {
 			// n+v overflows
 			n = 1<<64 - 1
-			err = os.ERANGE
+			err = ErrRange
 			goto Error
 		}
 		n = n1
@@ -124,8 +131,8 @@ Error:\n // Atoui64 interprets a string s as a decimal number and\n // returns the corresponding value n.\n //\n-// Atoui64 returns err == os.EINVAL if s is empty or contains invalid digits.\n-// It returns err == os.ERANGE if s cannot be represented by a uint64.\n+// Atoui64 returns err.Error = ErrSyntax if s is empty or contains invalid digits.\n+// It returns err.Error = ErrRange if s cannot be represented by a uint64.\n func Atoui64(s string) (n uint64, err os.Error) {\n 	return Btoui64(s, 10)\n }\n@@ -135,7 +142,7 @@ func Atoui64(s string) (n uint64, err os.Error) {\n func Btoi64(s string, base int) (i int64, err os.Error) {\n 	// Empty string bad.\n 	if len(s) == 0 {\n-\t\treturn 0, &NumError{s, os.EINVAL}\n+\t\treturn 0, &NumError{s, ErrSyntax}\n \t}\n \n 	// Pick off leading sign.\n@@ -151,15 +158,15 @@ func Btoi64(s string, base int) (i int64, err os.Error) {\n 	// Convert unsigned and check range.\n 	var un uint64\n 	un, err = Btoui64(s, base)\n-\tif err != nil && err.(*NumError).Error != os.ERANGE {\n+\tif err != nil && err.(*NumError).Error != ErrRange {\n \t\terr.(*NumError).Num = s0\n \t\treturn 0, err\n \t}\n \tif !neg && un >= 1<<63 {\n-\t\treturn 1<<63 - 1, &NumError{s0, os.ERANGE}\n+\t\treturn 1<<63 - 1, &NumError{s0, ErrRange}\n \t}\n \tif neg && un > 1<<63 {\n-\t\treturn -1 << 63, &NumError{s0, os.ERANGE}\n+\t\treturn -1 << 63, &NumError{s0, ErrRange}\n \t}\n \tn := int64(un)\n \tif neg {\n@@ -175,12 +182,12 @@ func Atoi64(s string) (i int64, err os.Error) { return Btoi64(s, 10) }\n // Atoui is like Atoui64 but returns its result as a uint.\n func Atoui(s string) (i uint, err os.Error) {\n 	i1, e1 := Atoui64(s)\n-\tif e1 != nil && e1.(*NumError).Error != os.ERANGE {\n+\tif e1 != nil && e1.(*NumError).Error != ErrRange {\n \t\treturn 0, e1\n \t}\n \ti = uint(i1)\n \tif uint64(i) != i1 {\n-\t\treturn ^uint(0), &NumError{s, os.ERANGE}\n+\t\treturn ^uint(0), &NumError{s, ErrRange}\n \t}\n \treturn i, nil\n }\n@@ -188,15 +195,15 @@ func Atoui(s string) (i uint, err os.Error) {\n // Atoi is like Atoi64 but returns its result as an int.\n func Atoi(s string) (i int, err os.Error) {\n 	i1, e1 := Atoi64(s)\n-\tif e1 != nil && e1.(*NumError).Error != os.ERANGE {\n+\tif e1 != nil && e1.(*NumError).Error != ErrRange {\n \t\treturn 0, e1\n \t}\n \ti = int(i1)\n \tif int64(i) != i1 {\n \t\tif i1 < 0 {\n-\t\t\treturn -1 << (IntSize - 1), &NumError{s, os.ERANGE}\n+\t\t\treturn -1 << (IntSize - 1), &NumError{s, ErrRange}\n \t\t}\n-\t\treturn 1<<(IntSize-1) - 1, &NumError{s, os.ERANGE}\n+\t\treturn 1<<(IntSize-1) - 1, &NumError{s, ErrRange}\n \t}\n \treturn i, nil\n }\n```

## コアとなるコードの解説

上記のコードスニペットは、`strconv`パッケージの`atoi.go`ファイルにおける変更を示しています。

1.  **`ErrRange`と`ErrSyntax`の定義**:
    ```go
    // ErrRange indicates that a value is out of range for the target type.
    var ErrRange = os.NewError("value out of range")
    // ErrSyntax indicates that a value does not have the right syntax for the target type.
    var ErrSyntax = os.NewError("invalid syntax")
    ```
    ここで、新しいエラー定数`ErrRange`と`ErrSyntax`が`os.NewError`関数を使って定義されています。これらは、それぞれ「値が範囲外」と「無効な構文」という具体的な意味を持つエラーオブジェクトです。

2.  **`NumError`構造体のコメント更新**:
    ```diff
    --- a/src/pkg/strconv/atoi.go
    +++ b/src/pkg/strconv/atoi.go
    @@ -6,9 +6,16 @@ package strconv
     
     import "os"
     
    +// ErrRange indicates that a value is out of range for the target type.
    +var ErrRange = os.NewError("value out of range")
    +// ErrSyntax indicates that a value does not have the right syntax for the target type.
    +var ErrSyntax = os.NewError("invalid syntax")
    +// A NumError records a failed conversion.
     type NumError struct {
    -	Num   string
    -	Error os.Error
    +	Num   string   // the input
    +	Error os.Error // the reason the conversion failed (ErrRange, ErrSyntax)
     }
    ```
    `NumError`構造体のコメントが更新され、`Error`フィールドが`ErrRange`または`ErrSyntax`のいずれかになることが明示されています。これにより、この構造体を利用する開発者は、どのような種類のエラーが返されるかを一目で理解できます。

3.  **`Btoui64`関数内のエラー置き換え**:
    `Btoui64`関数(文字列を指定された基数で符号なし64ビット整数に変換する関数)内で、文字列が空である場合や無効な文字を含む場合に`os.EINVAL`を返していた箇所が`ErrSyntax`に置き換えられています。また、数値が`uint64`の範囲を超過する場合に`os.ERANGE`を返していた箇所が`ErrRange`に置き換えられています。

    例:
    ```diff
    --- a/src/pkg/strconv/atoi.go
    +++ b/src/pkg/strconv/atoi.go
    @@ -38,15 +45,15 @@ func cutoff64(base int) uint64 {
     //
     // The errors that Btoui64 returns have concrete type *NumError
     // and include err.Num = s.  If s is empty or contains invalid
    -// digits, err.Error = os.EINVAL; if the value corresponding
    -// to s cannot be represented by a uint64, err.Error = os.ERANGE.\n
    +// digits, err.Error = ErrSyntax; if the value corresponding
    +// to s cannot be represented by a uint64, err.Error = ErrRange.
     func Btoui64(s string, b int) (n uint64, err os.Error) {
     	var cutoff uint64
     
     	s0 := s
     	switch {
     	case len(s) < 1:
    -		err = os.EINVAL
    +		err = ErrSyntax
     		goto Error
     ```
     この変更により、`Btoui64`が返すエラーがより具体的になり、エラーハンドリングの精度が向上します。

同様の変更が、`atob.go`、`atof.go`、`quote.go`、および関連するテストファイルにも適用されています。このコミットは、Go言語のエラーハンドリングのベストプラクティスを推進し、ライブラリの使いやすさと堅牢性を高める重要な一歩と言えます。

## 関連リンク

*   Go言語の`strconv`パッケージ公式ドキュメント: [https://pkg.go.dev/strconv](https://pkg.go.dev/strconv)
*   Go言語のエラーハンドリングに関する公式ブログ記事("Error handling and Go" by Rob Pike): [https://go.dev/blog/error-handling-and-go](https://go.dev/blog/error-handling-and-go)

## 参考にした情報源リンク

*   Go言語の`os`パッケージ公式ドキュメント: [https://pkg.go.dev/os](https://pkg.go.dev/os)
*   Go言語のコミット履歴(GitHub): [https://github.com/golang/go/commits/master](https://github.com/golang/go/commits/master)
*   Go Code Review Comments (Error Handling): [https://go.dev/doc/effective_go#errors](https://go.dev/doc/effective_go#errors)
*   Go CL 5327052: strconv: use better errors than os.EINVAL, os.ERANGE: [https://golang.org/cl/5327052](https://golang.org/cl/5327052)
    *   これはコミットメッセージに記載されているGoのコードレビューシステム(Gerrit)のリンクです。当時の詳細な議論や変更の経緯が確認できます。