[インデックス 1163] ファイルの概要
このコミットは、Go言語の標準ライブラリであるstrconv
パッケージにおける数値変換関数のエラーハンドリングメカニズムを根本的に変更するものです。具体的には、strconv.atof
(文字列から浮動小数点数への変換)およびstrconv.atoi
(文字列から整数への変換)系の関数が、成功/失敗を示すbool
値を返す代わりに、より詳細なエラー情報を提供する*os.Error
型を返すように修正されています。
変更された主なファイルは以下の通りです。
src/lib/strconv/atof.go
: 浮動小数点数変換関数(atof64
,atof32
,atof
)の変更。src/lib/strconv/atoi.go
: 整数変換関数(atoui64
,atoi64
,atoui
,atoi
)の変更。test/bugs/bug120.go
: 既存のバグテストの修正。test/chan/goroutines.go
: テストコードの修正。test/golden.out
: テスト出力のゴールデンファイルの更新。test/stringslib.go
: 文字列ライブラリのテストコードの修正。
コミット
commit 6cc001c31254bccdb0e0b40c271b09504f97df28
Author: Russ Cox <rsc@golang.org>
Date: Tue Nov 18 17:12:07 2008 -0800
return *os.Error instead of bool from strconv.ato*
R=r
DELTA=137 (56 added, 4 deleted, 77 changed)
OCL=19505
CL=19522
---
src/lib/strconv/atof.go | 57 +++++++++++++++++-------------\n src/lib/strconv/atoi.go | 93 +++++++++++++++++++++++++++++++++++--------------\n test/bugs/bug120.go | 6 ++--\n test/chan/goroutines.go | 7 ++--\n test/golden.out | 5 +--\n test/stringslib.go | 22 ++++++------\n 6 files changed, 121 insertions(+), 69 deletions(-)\n
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/6cc001c31254bccdb0e0b40c271b09504f97df28
元コミット内容
このコミットは、strconv
パッケージ内のato*
(atoi
やatof
など)関数群が、処理の成功・失敗を示すbool
値を返す代わりに、*os.Error
型のエラーオブジェクトを返すように変更することを目的としています。これにより、エラーが発生した場合に、単に失敗したことだけでなく、どのような種類のエラーが発生したのか(例:不正な入力、範囲外の値など)をより詳細に呼び出し元に伝えることが可能になります。
変更の背景
Go言語の初期開発段階において、エラーハンドリングのパターンは進化の途上にありました。当初は、関数が成功したかどうかを単純なbool
値で示す設計も存在しましたが、これはエラーの種類を区別できないという欠点がありました。例えば、文字列が数値として不正な形式である場合と、数値としては正しいがターゲットの型で表現できないほど大きい(オーバーフロー)場合とでは、呼び出し元が取るべき対応が異なります。
このコミットが行われた2008年11月は、Go言語がまだ一般に公開される前の非常に初期の段階です。この時期に、より堅牢で表現力豊かなエラーハンドリングメカニズムである*os.Error
(後にGoのエラーハンドリングの標準となるerror
インターフェースの原型)への移行が進められました。この変更は、Go言語が単なる成功/失敗の通知を超えて、エラーの具体的な原因をプログラム的に識別し、それに応じた適切な処理を記述できるような言語設計を目指していたことを示しています。これにより、ライブラリの利用者にとって、より信頼性の高いコードを書くための情報が提供されるようになります。
前提知識の解説
Go言語のエラーハンドリングの進化
Go言語のエラーハンドリングは、error
インターフェースを中心としています。このインターフェースは、Error() string
メソッドを持つ任意の型が実装できます。このコミットが行われた時期には、*os.Error
がエラーを表す主要な型の一つとして使われていました。これは、エラーが単なる真偽値ではなく、具体的な情報を持つオブジェクトとして扱われるべきだというGoの設計思想の初期の現れです。
bool
によるエラー通知: 非常に単純な成功/失敗の通知に用いられます。エラーの種類を区別できません。*os.Error
:os
パッケージで定義されていたエラー型で、より具体的なエラー情報(例:os.EINVAL
(Invalid argument),os.ERANGE
(Result too large))を持つことができました。これは、後のerror
インターフェースの設計に大きな影響を与えました。
strconv
パッケージ
strconv
パッケージは、Go言語の標準ライブラリの一部であり、文字列と基本的なデータ型(整数、浮動小数点数、真偽値など)との間の変換を提供します。
Atoi
(ASCII to Integer): 文字列を整数に変換します。ParseInt
/ParseUint
: より柔軟な基数指定やビットサイズ指定が可能な整数変換関数です。Atof
(ASCII to Float): 文字列を浮動小数点数に変換します。ParseFloat
: より柔軟なビットサイズ指定が可能な浮動小数点数変換関数です。
これらの関数は、ユーザー入力のパースや設定ファイルの読み込みなど、様々な場面で利用されます。
数値のオーバーフローとアンダーフロー
数値型には表現できる値の範囲が定められています。
- オーバーフロー (Overflow): 計算結果がその型の最大値を超えてしまうこと。例えば、
int8
(-128から127)で127に1を加えるとオーバーフローします。 - アンダーフロー (Underflow): 計算結果がその型の最小値を下回ってしまうこと。浮動小数点数では、非常に小さい値が0に丸められる場合も指します。
これらの状況は、プログラムの予期せぬ動作やセキュリティ上の脆弱性につながる可能性があるため、適切に検出・処理する必要があります。os.ERANGE
は、このような範囲外のエラーを示すために使用されます。
IEEE 754 浮動小数点数標準
IEEE 754は、浮動小数点数の表現と演算に関する国際標準です。Go言語のfloat32
とfloat64
は、この標準に準拠しています。この標準では、無限大(Inf
)や非数(NaN
)といった特殊な値も定義されており、オーバーフロー時にはInf
が返されることがあります。
技術的詳細
このコミットの主要な技術的変更点は、strconv
パッケージ内の数値変換関数のシグネチャとエラー処理ロジックの変更です。
1. 関数シグネチャの変更
以前は、多くのstrconv.ato*
関数がbool ok
という戻り値を含んでいました。これは、変換が成功したかどうかを示すものでした。
変更前(例: atof64
):
export func atof64(s string) (f float64, overflow bool, ok bool)
このシグネチャでは、ok
がfalse
の場合に変換失敗、overflow
がtrue
の場合にオーバーフローを示していました。
変更後(例: atof64
):
export func atof64(s string) (f float64, err *os.Error)
変更後は、ok
とoverflow
の代わりに単一の*os.Error
型が返されます。エラーがない場合はnil
が返されます。
2. エラー値のマッピング
- 不正な入力 (Invalid Argument): 文字列が数値としてパースできない形式である場合(例: "abc"を整数に変換しようとする場合)、以前は
ok = false
が返されていましたが、変更後はos.EINVAL
が返されます。 - 範囲外の値 (Out of Range): 変換結果がターゲットの数値型で表現できる範囲を超えた場合(オーバーフローやアンダーフロー)、以前は
overflow = true
が返されていましたが、変更後はos.ERANGE
が返されます。
この変更により、呼び出し元はerr == os.EINVAL
やerr == os.ERANGE
のように、特定のエラー条件をチェックできるようになり、よりきめ細やかなエラーハンドリングが可能になります。
3. 整数変換におけるオーバーフローチェックの追加
src/lib/strconv/atoi.go
では、以前// TODO: Doesn't check for overflow.
というコメントがあったように、整数変換関数に明示的なオーバーフローチェックが欠けていました。このコミットでは、atoui64
およびatoi64
関数に、uint64
やint64
の最大値・最小値を超えるかどうかのチェックが追加され、オーバーフロー時にはos.ERANGE
が返されるようになりました。
特に、atoui64
では、n > (1<<64)/10
のようなチェックで、次の桁を処理する前にオーバーフローが発生するかどうかを事前に確認しています。また、n*10 + d < n
のようなチェックは、加算によるオーバーフローを検出する一般的なイディオムです。
4. IntSize()
関数の導入
src/lib/strconv/atoi.go
にIntSize()
という新しい関数が追加されました。これは、現在のアーキテクチャにおけるint
型のビットサイズを動的に決定するためのものです。
func IntSize() uint {
siz := uint(8);
for 1<<siz != 0 {
siz *= 2
}
return siz
}
var intsize = IntSize();
この関数は、int
型が32ビットか64ビットかによって最大値が異なるため、atoi
関数がプラットフォームに依存しない正確な範囲チェックを行うために利用されます。例えば、atoi
関数内でint64(i) != i1
のようなチェックが行われ、変換された値がint
型に収まらない場合にos.ERANGE
を返すロジックにintsize
が使われています。
コアとなるコードの変更箇所
src/lib/strconv/atof.go
--- a/src/lib/strconv/atof.go
+++ b/src/lib/strconv/atof.go
@@ -10,7 +10,10 @@
package strconv
-import "strconv"
+import (
+ "os";
+ "strconv";
+)
// TODO(rsc): Better truncation handling.
func StringToDecimal(s string) (neg bool, d *Decimal, trunc bool, ok bool) {
@@ -314,43 +317,49 @@ func DecimalToFloat32(neg bool, d *Decimal, trunc bool) (f float32, ok bool) {
// returns f, false, true, where f is the nearest floating point
// number rounded using IEEE754 unbiased rounding.
//
-// If s is not syntactically well-formed, returns ok == false.
+// If s is not syntactically well-formed, returns err = os.EINVAL.
//
// If s is syntactically well-formed but is more than 1/2 ULP
// away from the largest floating point number of the given size,
-// returns f = ±Inf, overflow = true, ok = true.
-export func atof64(s string) (f float64, overflow bool, ok bool) {
- neg, d, trunc, ok1 := StringToDecimal(s);
- if !ok1 {
- return 0, false, false;
+// returns f = ±Inf, err = os.ERANGE.
+export func atof64(s string) (f float64, err *os.Error) {
+ neg, d, trunc, ok := StringToDecimal(s);
+ if !ok {
+ return 0, os.EINVAL;
}
if f, ok := DecimalToFloat64(neg, d, trunc); ok {
- return f, false, true;
+ return f, nil;
}
- b, overflow1 := DecimalToFloatBits(neg, d, trunc, &float64info);
- return sys.float64frombits(b), overflow1, true;
+ b, ovf := DecimalToFloatBits(neg, d, trunc, &float64info);
+ f = sys.float64frombits(b);
+ if ovf {
+ err = os.ERANGE;
+ }
+ return f, err
}
-export func atof32(s string) (f float32, overflow bool, ok bool) {
- neg, d, trunc, ok1 := StringToDecimal(s);
- if !ok1 {
- return 0, false, false;
+export func atof32(s string) (f float32, err *os.Error) {
+ neg, d, trunc, ok := StringToDecimal(s);
+ if !ok {
+ return 0, os.EINVAL;
}
if f, ok := DecimalToFloat32(neg, d, trunc); ok {
- return f, false, true;
+ return f, nil;
+ }
+ b, ovf := DecimalToFloatBits(neg, d, trunc, &float32info);
+ f = sys.float32frombits(uint32(b));
+ if ovf {
+ err = os.ERANGE;
}
- b, overflow1 := DecimalToFloatBits(neg, d, trunc, &float32info);
- return sys.float32frombits(uint32(b)), overflow1, true;
+ return f, err
}
-export func atof(s string) (f float, overflow bool, ok bool) {
+export func atof(s string) (f float, err *os.Error) {
if floatsize == 32 {
- var f1 float32;
- f1, overflow, ok = atof32(s);
- return float(f1), overflow, ok;
+ f1, err1 := atof32(s);
+ return float(f1), err1;
}
- var f1 float64;
- f1, overflow, ok = atof64(s);
- return float(f1), overflow, ok;
+ f1, err1 := atof64(s);
+ return float(f1), err1;
}
src/lib/strconv/atoi.go
--- a/src/lib/strconv/atoi.go
+++ b/src/lib/strconv/atoi.go
@@ -3,42 +3,59 @@
// license that can be found in the LICENSE file.
package strconv
+import "os"
++
+func IntSize() uint {
++ siz := uint(8);
++ for 1<<siz != 0 {
++ siz *= 2
++ }
++ return siz
++}
++var intsize = IntSize();
// Convert decimal string to unsigned integer.
-// TODO: Doesn't check for overflow.
-export func atoui64(s string) (i uint64, ok bool) {
+export func atoui64(s string) (i uint64, err *os.Error) {
// empty string bad
- if len(s) == 0 {
- return 0, false
+ if len(s) == 0 {
+ return 0, os.EINVAL
}
// pick off zero
if s == "0" {
- return 0, true
+ return 0, nil
}
-
- // otherwise, leading zero bad
+
+ // otherwise, leading zero bad:
+ // don't want to take something intended as octal.
if s[0] == '0' {
- return 0, false
+ return 0, os.EINVAL
}
// parse number
n := uint64(0);
for i := 0; i < len(s); i++ {
\tif s[i] < '0' || s[i] > '9' {
- return 0, false
+ return 0, os.EINVAL
+ }
+ if n > (1<<64)/10 {
+ return 1<<64-1, os.ERANGE
}
- n = n*10 + uint64(s[i] - '0')
+ n = n*10;
+ d := uint64(s[i] - '0');
+ if n+d < n {
+ return 1<<64-1, os.ERANGE
+ }
+ n += d;
}
- return n, true
+ return n, nil
}
// Convert decimal string to integer.
-// TODO: Doesn't check for overflow.
-export func atoi64(s string) (i int64, ok bool) {
+export func atoi64(s string) (i int64, err *os.Error) {
// empty string bad
if len(s) == 0 {
- return 0, false
+ return 0, os.EINVAL
}
// pick off leading sign
@@ -51,25 +68,49 @@ export func atoi64(s string) (i int64, ok bool) {
}
var un uint64;
- un, ok = atoui64(s);
- if !ok {
- return 0, false
+ un, err = atoui64(s);
+ if err != nil && err != os.ERANGE {
+ return 0, err
+ }
+ if !neg && un >= 1<<63 {
+ return 1<<63-1, os.ERANGE
+ }
+ if neg && un > 1<<63 {
+ return -1<<63, os.ERANGE
}
n := int64(un);
if neg {
n = -n
}
- return n, true
+ return n, nil
}
-export func atoui(s string) (i uint, ok bool) {
- ii, okok := atoui64(s);
- i = uint(ii);
- return i, okok
+export func atoui(s string) (i uint, err *os.Error) {
+ i1, e1 := atoui64(s);
+ if e1 != nil && e1 != os.ERANGE {
+ return 0, e1
+ }
+ i = uint(i1);
+ if uint64(i) != i1 {
+ // TODO: return uint(^0), os.ERANGE.
+ i1 = 1<<64-1;
+ return uint(i1), os.ERANGE
+ }
+ return i, nil
}
-export func atoi(s string) (i int, ok bool) {
- ii, okok := atoi64(s);
- i = int(ii);
- return i, okok
+export func atoi(s string) (i int, err *os.Error) {
+ i1, e1 := atoi64(s);
+ if e1 != nil && e1 != os.ERANGE {
+ return 0, e1
+ }
+ i = int(i1);
+ if int64(i) != i1 {
+ if i1 < 0 {
+ return -1<<(intsize-1), os.ERANGE
+ }
+ return 1<<(intsize-1) - 1, os.ERANGE
+ }
+ return i, nil
}
コアとなるコードの解説
atof.go
の変更点
import "os"
の追加:*os.Error
型を使用するためにos
パッケージがインポートされました。- 関数シグネチャの変更:
atof64
,atof32
,atof
の各関数で、bool ok
とbool overflow
の戻り値が削除され、代わりに*os.Error err
が追加されました。 - エラー処理ロジックの変更:
StringToDecimal
からの戻り値ok
がfalse
の場合、以前はreturn 0, false, false
(値、オーバーフロー、OK)でしたが、return 0, os.EINVAL
(値、エラー)に変更されました。これは、入力文字列が数値として不正であることを示します。DecimalToFloatBits
からの戻り値ovf
(オーバーフロー)がtrue
の場合、以前はoverflow1
というbool
で示されていましたが、変更後はif ovf { err = os.ERANGE }
としてos.ERANGE
エラーが設定されます。これは、変換結果が浮動小数点数の表現範囲を超えたことを示します。- 成功した場合は、
nil
がエラー値として返されます。
atoi.go
の変更点
import "os"
の追加:*os.Error
型を使用するためにos
パッケージがインポートされました。IntSize()
関数の追加: 実行環境のint
型のビットサイズを動的に取得するためのヘルパー関数が追加されました。これは、int
やuint
の最大値・最小値がアーキテクチャによって異なるため、正確な範囲チェックを行うために必要です。- 関数シグネチャの変更:
atoui64
,atoi64
,atoui
,atoi
の各関数で、bool ok
の戻り値が削除され、代わりに*os.Error err
が追加されました。 - エラー処理ロジックの変更:
- 空文字列や不正な形式の文字列(例: 先頭が'0'で始まる非ゼロの数値、非数字文字を含む)の場合、以前は
return 0, false
でしたが、return 0, os.EINVAL
に変更されました。 - オーバーフローチェックの追加:
atoui64
では、n > (1<<64)/10
(次の桁を加える前にオーバーフローするかどうか)やn+d < n
(加算後にオーバーフローするかどうか)といったチェックが追加され、オーバーフロー時にはreturn 1<<64-1, os.ERANGE
が返されます。atoi64
では、符号付き整数の最大値・最小値(1<<63-1
や-1<<63
)との比較によりオーバーフローがチェックされ、os.ERANGE
が返されます。atoui
とatoi
では、atoui64
やatoi64
の結果がそれぞれuint
やint
の範囲に収まるかどうかがチェックされ、収まらない場合はos.ERANGE
が返されます。これにより、より小さいサイズの整数型への変換時のオーバーフローも適切に検出されます。
- 成功した場合は、
nil
がエラー値として返されます。
- 空文字列や不正な形式の文字列(例: 先頭が'0'で始まる非ゼロの数値、非数字文字を含む)の場合、以前は
テストファイルの変更点
test/bugs/bug120.go
, test/chan/goroutines.go
, test/stringslib.go
などのテストファイルでは、strconv.ato*
関数の呼び出し箇所が、新しいエラーハンドリングのシグネチャに合わせて修正されています。具体的には、ok
変数の代わりにerr
変数を宣言し、!ok
のチェックをerr != nil
に変更しています。これにより、新しいAPIの利用方法がテストコードにも反映されています。
関連リンク
- Go言語の公式ドキュメント(
strconv
パッケージ): https://pkg.go.dev/strconv - Go言語の公式ドキュメント(
os
パッケージ): https://pkg.go.dev/os - Go言語のエラーハンドリングに関する公式ブログ記事(より現代的なエラーハンドリングについてですが、背景理解に役立ちます): https://go.dev/blog/error-handling-and-go
参考にした情報源リンク
- Go言語の初期のコミット履歴と設計に関する一般的な知識
strconv
パッケージの機能とエラーハンドリングの標準的なパターン- IEEE 754 浮動小数点数標準に関する一般的な情報
- Go言語における
os.Error
からerror
インターフェースへの進化に関する情報(Go言語の歴史的背景) - Go言語のソースコード(特に
src/builtin/builtin.go
やsrc/runtime/error.go
など、エラーインターフェースの定義に関連する部分)