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

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

このコミットは、Go言語の標準ライブラリであるmath/randパッケージ内の乱数生成関数Intn, Int31n, Int63nの挙動を変更するものです。具体的には、これらの関数に引数nとして0以下の値が渡された場合に、以前は0を返していた動作から、panic(パニック)を引き起こすように修正されています。

コミット

commit 7d1c5328ed2b0082acdc0b47e6565e4b254e9f8c
Author: Rob Pike <r@golang.org>
Date:   Sat Feb 18 08:53:03 2012 +1100

    math/rand: Intn etc. should panic if their argument is <= 0.
    
    I am making a unilateral decision here. I could also settle for returning 0,
    as long it's documented, but I argue that it's equivalent to an index
    out of bounds.
    
    Fixes #2892.
    
    R=golang-dev, dsymonds, rsc
    CC=golang-dev
    https://golang.org/cl/5676079

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

https://github.com/golang/go/commit/7d1c5328ed2b0082acdc0b47e6565e4b254e9f8c

元コミット内容

math/rand: Intn etc. should panic if their argument is <= 0.

このコミットメッセージは、math/randパッケージのIntn関数などが、引数に0以下の値が渡された場合にパニックを起こすべきであるという変更の意図を明確に示しています。作者のRob Pikeは、この決定が一方的なものであることを認めつつも、0を返すという選択肢も考えられたが、それは配列の範囲外アクセス(index out of bounds)に相当すると主張しています。これは、乱数生成の範囲が不正であるという状況が、プログラムの論理的な誤りを示すものであるという考えに基づいています。

また、Fixes #2892という記述から、このコミットがGoのIssueトラッカーで報告されていた問題2892を解決するものであることがわかります。

変更の背景

この変更の背景には、Go言語のmath/randパッケージにおける乱数生成関数の引数検証に関する設計思想の明確化があります。特にIntn(n int)のような関数は、[0, n)の範囲で乱数を生成することを意図しています。ここでnが0以下である場合、この範囲は数学的に意味をなさなくなります。

以前の挙動では、n <= 0の場合にこれらの関数が0を返していました。しかし、これは以下のような問題を引き起こす可能性がありました。

  1. サイレントなエラー: 0が返されることで、呼び出し元は不正な引数が渡されたことに気づかず、プログラムが意図しない動作を続ける可能性がありました。例えば、Intn(0)が常に0を返す場合、それを基にしたロジックが誤った前提で進んでしまうことがあります。
  2. 混乱: [0, n)という定義から逸脱した結果が返されるため、関数の契約が曖昧になり、開発者が期待する動作と異なる結果になる可能性がありました。
  3. デバッグの困難さ: 不正な引数によって発生した問題が、0が返された時点では表面化せず、後になって別の場所で予期せぬバグとして現れるため、原因の特定が難しくなることがありました。

Rob Pikeがコミットメッセージで「index out of boundsに相当する」と述べているのは、乱数生成の範囲が不正であることは、配列のインデックスが範囲外であることと同様に、プログラムの論理的な誤りであり、即座に検出して修正すべき問題であるという考えを示しています。Go言語では、このようなプログラミング上の回復不能なエラーに対してはpanicを使用することが推奨されるパターンの一つです。これにより、問題が早期に発見され、開発者が修正を強いられることになります。

この変更は、Go言語の設計哲学である「明示的なエラーハンドリング」と「早期失敗(fail fast)」の原則に沿ったものと言えます。

前提知識の解説

math/randパッケージ

math/randパッケージは、Go言語の標準ライブラリの一部であり、擬似乱数ジェネレータ(PRNG)を提供します。これは、シード値に基づいて決定論的に乱数のようなシーケンスを生成するものです。暗号学的に安全な乱数が必要な場合は、crypto/randパッケージを使用する必要があります。

math/randパッケージの主な関数には以下のようなものがあります。

  • Intn(n int): [0, n)の範囲の非負のint型の擬似乱数を返します。
  • Int31n(n int32): [0, n)の範囲の非負のint32型の擬似乱数を返します。
  • Int63n(n int64): [0, n)の範囲の非負のint64型の擬似乱数を返します。
  • Float64(): [0.0, 1.0)の範囲のfloat64型の擬似乱数を返します。
  • Seed(seed int64): 乱数ジェネレータのシードを設定します。同じシード値からは常に同じ乱数シーケンスが生成されます。

panicrecover

Go言語には、エラーハンドリングのメカニズムとしてerrorインターフェースとpanic/recoverメカニズムがあります。

  • error: 予期されるエラーや、プログラムが回復できる可能性のあるエラーに対して使用されます。関数はerror型の値を返すことでエラーを通知し、呼び出し元はそれをチェックして適切に処理します。
  • panic: プログラムが正常に続行できないような、予期しない、回復不能なエラーに対して使用されます。panicが発生すると、現在の関数の実行が中断され、遅延関数(deferで登録された関数)が実行された後、呼び出しスタックを遡ってpanicが伝播します。もし途中でrecoverが呼び出されなければ、プログラムはクラッシュします。
  • recover: panicが発生した際に、defer関数内でrecover()を呼び出すことで、パニックを捕捉し、プログラムの実行を再開させることができます。これは、通常、サーバーアプリケーションなどで、特定のゴルーチンがクラッシュしても全体が停止しないようにするために使用されます。

このコミットでは、n <= 0という引数は、Intnなどの関数の契約を根本的に破るものであり、プログラムがその状態から回復して意味のある結果を生成することができないため、panicが適切なメカニズムとして選択されました。

技術的詳細

このコミットの技術的な核心は、math/randパッケージ内のIntn, Int31n, Int63n関数が、引数nが0以下の場合にpanicを引き起こすように変更された点です。

これらの関数は、[0, n)という半開区間(0を含み、nを含まない)で乱数を生成することを目的としています。

  • n > 0の場合: 期待通りの範囲で乱数が生成されます。
  • n = 0の場合: [0, 0)という範囲になり、これは空集合です。この範囲で乱数を生成することはできません。
  • n < 0の場合: [0, 負の数)という範囲になり、これも数学的に意味をなしません。

以前のバージョンでは、n <= 0の場合にこれらの関数は単にreturn 0していました。これは、呼び出し元が不正な引数を渡したにもかかわらず、エラーを明示的に通知せず、あたかも有効な結果であるかのように0を返していました。

この変更により、n <= 0の条件が満たされた場合、以下のようにpanicが呼び出されます。

panic("invalid argument to Int63n") // または Int31n, Intn

これにより、不正な引数が渡された瞬間にプログラムの実行が中断され、開発者はその問題を即座に認識し、修正することができます。これは、Go言語の「早期失敗(fail fast)」の原則に合致しています。不正な状態を放置して後で予期せぬバグとして現れるよりも、問題が発生した時点で明確に通知する方が、堅牢なソフトウェア開発には有利であるという考え方です。

また、この変更は、Intnなどの関数のドキュメントにも反映され、It panics if n <= 0.という記述が追加されています。これにより、関数の利用者はこの新しい挙動を明確に理解し、適切な引数を渡すように促されます。

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

変更はsrc/pkg/math/rand/rand.goファイルに対して行われています。

--- a/src/pkg/math/rand/rand.go
+++ b/src/pkg/math/rand/rand.go
@@ -49,9 +49,10 @@ func (r *Rand) Int() int {
 }
 
 // Int63n returns, as an int64, a non-negative pseudo-random number in [0,n).
+// It panics if n <= 0.
 func (r *Rand) Int63n(n int64) int64 {
 	if n <= 0 {
-		return 0
+		panic("invalid argument to Int63n")
 	}
 	max := int64((1 << 63) - 1 - (1<<63)%uint64(n))
 	v := r.Int63()
@@ -62,9 +63,10 @@ func (r *Rand) Int63n(n int64) int64 {
 }
 
 // Int31n returns, as an int32, a non-negative pseudo-random number in [0,n).
+// It panics if n <= 0.
 func (r *Rand) Int31n(n int32) int32 {
 	if n <= 0 {
-		return 0
+		panic("invalid argument to Int31n")
 	}
 	max := int32((1 << 31) - 1 - (1<<31)%uint32(n))
 	v := r.Int31()
@@ -75,7 +77,11 @@ func (r *Rand) Int31n(n int32) int32 {
 }
 
 // Intn returns, as an int, a non-negative pseudo-random number in [0,n).
+// It panics if n <= 0.
 func (r *Rand) Intn(n int) int {
+	if n <= 0 {
+		panic("invalid argument to Intn")
+	}
 	if n <= 1<<31-1 {
 		return int(r.Int31n(int32(n)))
 	}
@@ -125,12 +131,15 @@ func Int31() int32 { return globalRand.Int31() }\n func Int() int { return globalRand.Int() }\n \n // Int63n returns, as an int64, a non-negative pseudo-random number in [0,n).\n+// It panics if n <= 0.\n func Int63n(n int64) int64 { return globalRand.Int63n(n) }\n \n // Int31n returns, as an int32, a non-negative pseudo-random number in [0,n).\n+// It panics if n <= 0.\n func Int31n(n int32) int32 { return globalRand.Int31n(n) }\n \n // Intn returns, as an int, a non-negative pseudo-random number in [0,n).\n+// It panics if n <= 0.\n func Intn(n int) int { return globalRand.Intn(n) }\n \n // Float64 returns, as a float64, a pseudo-random number in [0.0,1.0).\n```

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

上記のdiffを見ると、以下の3つの関数とそのグローバル版に対して変更が加えられています。

1.  **`func (r *Rand) Int63n(n int64) int64`**:
    *   変更前: `if n <= 0 { return 0 }`
    *   変更後: `if n <= 0 { panic("invalid argument to Int63n") }`
    *   コメントに`// It panics if n <= 0.`が追加されました。

2.  **`func (r *Rand) Int31n(n int32) int32`**:
    *   変更前: `if n <= 0 { return 0 }`
    *   変更後: `if n <= 0 { panic("invalid argument to Int31n") }`
    *   コメントに`// It panics if n <= 0.`が追加されました。

3.  **`func (r *Rand) Intn(n int) int`**:
    *   変更前: `if n <= 1<<31-1 { ... }` の前に`n <= 0`のチェックがありませんでした。
    *   変更後: `if n <= 0 { panic("invalid argument to Intn") }` が追加されました。
    *   コメントに`// It panics if n <= 0.`が追加されました。

これらの変更は、各関数の冒頭で引数`n`が0以下であるかをチェックし、その条件が満たされた場合に`panic`を発生させるという共通のパターンに従っています。これにより、不正な引数が渡された場合に、関数が0を返すというサイレントなエラーではなく、明確な実行時エラーとして通知されるようになります。

また、各関数のドキュメンテーションコメントに「`It panics if n <= 0.`」という記述が追加されたことで、この新しい挙動がAPIの契約として明示されました。これは、開発者がこれらの関数を安全に利用するための重要な情報となります。

グローバル関数である`Int63n`, `Int31n`, `Intn`についても、それぞれが内部で`globalRand`インスタンスの対応するメソッドを呼び出しているため、同様にコメントが更新されています。これにより、グローバル関数を直接呼び出す場合でも、引数`n`が0以下であればパニックが発生することが明確に示されています。

## 関連リンク

*   Go Issue 2892: [https://github.com/golang/go/issues/2892](https://github.com/golang/go/issues/2892)
*   Go CL 5676079: [https://golang.org/cl/5676079](https://golang.org/cl/5676079)

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

*   Go言語の公式ドキュメント: `math/rand`パッケージ
*   Go言語の公式ドキュメント: `panic`と`recover`
*   GitHubのGoリポジトリ
*   Go言語のIssueトラッカー