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

[インデックス 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*atoiatofなど)関数群が、処理の成功・失敗を示す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言語のfloat32float64は、この標準に準拠しています。この標準では、無限大(Inf)や非数(NaN)といった特殊な値も定義されており、オーバーフロー時にはInfが返されることがあります。

技術的詳細

このコミットの主要な技術的変更点は、strconvパッケージ内の数値変換関数のシグネチャとエラー処理ロジックの変更です。

1. 関数シグネチャの変更

以前は、多くのstrconv.ato*関数がbool okという戻り値を含んでいました。これは、変換が成功したかどうかを示すものでした。

変更前(例: atof64):

export func atof64(s string) (f float64, overflow bool, ok bool)

このシグネチャでは、okfalseの場合に変換失敗、overflowtrueの場合にオーバーフローを示していました。

変更後(例: atof64):

export func atof64(s string) (f float64, err *os.Error)

変更後は、okoverflowの代わりに単一の*os.Error型が返されます。エラーがない場合はnilが返されます。

2. エラー値のマッピング

  • 不正な入力 (Invalid Argument): 文字列が数値としてパースできない形式である場合(例: "abc"を整数に変換しようとする場合)、以前はok = falseが返されていましたが、変更後はos.EINVALが返されます。
  • 範囲外の値 (Out of Range): 変換結果がターゲットの数値型で表現できる範囲を超えた場合(オーバーフローやアンダーフロー)、以前はoverflow = trueが返されていましたが、変更後はos.ERANGEが返されます。

この変更により、呼び出し元はerr == os.EINVALerr == os.ERANGEのように、特定のエラー条件をチェックできるようになり、よりきめ細やかなエラーハンドリングが可能になります。

3. 整数変換におけるオーバーフローチェックの追加

src/lib/strconv/atoi.goでは、以前// TODO: Doesn't check for overflow.というコメントがあったように、整数変換関数に明示的なオーバーフローチェックが欠けていました。このコミットでは、atoui64およびatoi64関数に、uint64int64の最大値・最小値を超えるかどうかのチェックが追加され、オーバーフロー時にはos.ERANGEが返されるようになりました。

特に、atoui64では、n > (1<<64)/10のようなチェックで、次の桁を処理する前にオーバーフローが発生するかどうかを事前に確認しています。また、n*10 + d < nのようなチェックは、加算によるオーバーフローを検出する一般的なイディオムです。

4. IntSize()関数の導入

src/lib/strconv/atoi.goIntSize()という新しい関数が追加されました。これは、現在のアーキテクチャにおける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 okbool overflowの戻り値が削除され、代わりに*os.Error errが追加されました。
  • エラー処理ロジックの変更:
    • StringToDecimalからの戻り値okfalseの場合、以前は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型のビットサイズを動的に取得するためのヘルパー関数が追加されました。これは、intuintの最大値・最小値がアーキテクチャによって異なるため、正確な範囲チェックを行うために必要です。
  • 関数シグネチャの変更: 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が返されます。
      • atouiatoiでは、atoui64atoi64の結果がそれぞれuintintの範囲に収まるかどうかがチェックされ、収まらない場合はos.ERANGEが返されます。これにより、より小さいサイズの整数型への変換時のオーバーフローも適切に検出されます。
    • 成功した場合は、nilがエラー値として返されます。

テストファイルの変更点

test/bugs/bug120.go, test/chan/goroutines.go, test/stringslib.goなどのテストファイルでは、strconv.ato*関数の呼び出し箇所が、新しいエラーハンドリングのシグネチャに合わせて修正されています。具体的には、ok変数の代わりにerr変数を宣言し、!okのチェックをerr != nilに変更しています。これにより、新しいAPIの利用方法がテストコードにも反映されています。

関連リンク

参考にした情報源リンク

  • Go言語の初期のコミット履歴と設計に関する一般的な知識
  • strconvパッケージの機能とエラーハンドリングの標準的なパターン
  • IEEE 754 浮動小数点数標準に関する一般的な情報
  • Go言語におけるos.Errorからerrorインターフェースへの進化に関する情報(Go言語の歴史的背景)
  • Go言語のソースコード(特にsrc/builtin/builtin.gosrc/runtime/error.goなど、エラーインターフェースの定義に関連する部分)