[インデックス 15357] ファイルの概要
このコミットは、Go言語の database/sql パッケージにおいて、Scan メソッドが nil ポインタを宛先として受け取った際にパニック(panic)するのではなく、より適切なエラーを返すように修正するものです。これにより、データベースからの値の読み込み時に nil ポインタが渡された場合でも、プログラムが予期せず終了することなく、エラーハンドリングが可能になります。
コミット
commit bca3f5fca030599c41523570a3be9527448e73a9
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Thu Feb 21 10:43:00 2013 -0800
database/sql: check for nil Scan pointers
Return nice errors and don't panic.
Fixes #4859
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/7383046
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/bca3f5fca030599c41523570a3be9527448e73a9
元コミット内容
database/sql: check for nil Scan pointers
Return nice errors and don't panic.
Fixes #4859
変更の背景
この変更の背景には、Go言語の database/sql パッケージの Scan メソッドが、結果セットの値をGoの変数にスキャンする際に、nil ポインタを宛先として受け取るとパニックを引き起こすという問題がありました。具体的には、Rows.Scan() や QueryRow.Scan() メソッドに nil ポインタ(例: var s *string; rows.Scan(s))を渡した場合、内部で reflect パッケージを使用して値の割り当てを行おうとした際に、reflect.Value.Set() メソッドが nil ポインタに対して呼び出され、ランタイムパニックが発生していました。
パニックはGoプログラムの予期せぬ終了を意味し、堅牢なアプリケーション開発においては避けるべき挙動です。開発者は、このような不正な入力に対しては、パニックではなく明確なエラーを返すことを期待します。このコミットは、golang.org/issue/4859 で報告されたこの問題を解決し、よりユーザーフレンドリーで予測可能なエラーハンドリングを提供することを目的としています。
前提知識の解説
database/sqlパッケージ: Go言語の標準ライブラリの一部で、SQLデータベースとの一般的なインターフェースを提供します。このパッケージ自体は特定のデータベースドライバを含まず、ドライバは外部パッケージとして提供されます。QueryRowやScanなどのメソッドを通じて、データベースからの結果の取得とGoの変数へのマッピングを行います。Scanメソッド:RowsまたはRowオブジェクトのメソッドで、データベースの行の列の値をGoの変数にコピーするために使用されます。Scanメソッドには、スキャン先の変数のポインタを渡す必要があります。例えば、row.Scan(&myVar)のように使用します。- ポインタ (Pointers) in Go: Goにおけるポインタは、変数のメモリアドレスを保持する変数です。ポインタを介して、そのアドレスに格納されている値を間接的に操作できます。
nilポインタは、どの有効なメモリアドレスも指していないポインタを意味します。 reflectパッケージ: Goのreflectパッケージは、実行時にプログラムの構造を検査(リフレクション)するための機能を提供します。これにより、変数の型や値を動的に調べたり、操作したりすることが可能になります。database/sqlパッケージでは、Scanメソッドが任意の型の変数に値をスキャンするためにreflectを内部的に利用しています。特に、reflect.ValueOf()は任意のインターフェース値からreflect.Valueを取得し、reflect.Value.Kind()はその値の基本的な種類(ポインタ、構造体など)を返します。reflect.Value.IsNil()は、ポインタ、インターフェース、マップ、スライス、関数、チャネルなどの値がnilであるかどうかをチェックします。
技術的詳細
このコミットの主要な変更は、src/pkg/database/sql/convert.go ファイル内の convertAssign 関数に集中しています。convertAssign 関数は、database/sql パッケージの内部で、データベースから読み取られた値をGoの宛先変数に変換して割り当てる役割を担っています。
変更前は、convertAssign 関数が dest 引数(スキャン先のポインタ)が nil であるかどうかを明示的にチェックしていませんでした。そのため、dest が nil ポインタである場合、reflect パッケージの操作(特に reflect.ValueOf(dest) から得られる reflect.Value が IsNil() で true を返すような状況)において、その後の値の割り当て処理でパニックが発生する可能性がありました。
このコミットでは、以下の2つの主要な方法で nil ポインタのチェックが導入されました。
-
共通ケースでの明示的な
nilポインタチェック:convertAssign関数内のswitch s := src.(type)ブロックにおいて、string、[]byte、nilの各src型に対するdestの型アサーション(例:case *string:)の直後に、if d == nil { return errNilPtr }というチェックが追加されました。これにより、reflectを使用する前の段階で、一般的な型(*string,*[]byte,*interface{})へのスキャンにおいてnilポインタが検出された場合に、カスタムエラーerrNilPtrを即座に返すようになりました。これは、reflectを介した処理よりも高速にエラーを検出できるため、パフォーマンス上の利点もあります。 -
reflectを使用するケースでのnilポインタチェック:convertAssign関数の後半部分で、reflectパッケージを使用して汎用的な型変換と割り当てを行う前に、dpv.IsNil()メソッドによるチェックが追加されました。if dpv.Kind() != reflect.Ptr { return errors.New("destination not a pointer") } if dpv.IsNil() { return errNilPtr }ここで
dpvはreflect.ValueOf(dest)から得られたreflect.Valueです。このチェックにより、destがポインタ型であり、かつnilである場合に、errNilPtrエラーが返されるようになりました。これにより、reflect.Value.Set()がnilポインタに対して呼び出されることを防ぎ、パニックを回避します。
新しいエラー変数 errNilPtr は、var errNilPtr = errors.New("destination pointer is nil") として定義され、このエラーが返されることで、Scan メソッドの呼び出し元は nil ポインタが渡されたことを明確に認識し、適切にエラーハンドリングできるようになります。
また、src/pkg/database/sql/sql_test.go には、この修正が正しく機能することを確認するための新しいテストケース TestQueryRowNilScanDest が追加されました。このテストは、*string 型の nil ポインタを Scan メソッドに渡し、期待されるエラーメッセージ ("sql: Scan error on column index 0: destination pointer is nil") が返されることを検証しています。
コアとなるコードの変更箇所
src/pkg/database/sql/convert.go
--- a/src/pkg/database/sql/convert.go
+++ b/src/pkg/database/sql/convert.go
@@ -14,6 +14,8 @@ import (
"strconv"
)
+var errNilPtr = errors.New("destination pointer is nil") // embedded in descriptive error
+
// driverArgs converts arguments from callers of Stmt.Exec and
// Stmt.Query into driver Values.
//
@@ -75,34 +77,52 @@ func driverArgs(si driver.Stmt, args []interface{}) ([]driver.Value, error) {
// An error is returned if the copy would result in loss of information.
// dest should be a pointer type.
func convertAssign(dest, src interface{}) error {
- // Common cases, without reflect. Fall through.
+ // Common cases, without reflect.
switch s := src.(type) {
case string:
switch d := dest.(type) {
case *string:
+ if d == nil {
+ return errNilPtr
+ }
*d = s
return nil
case *[]byte:
+ if d == nil {
+ return errNilPtr
+ }
*d = []byte(s)
return nil
}
case []byte:
switch d := dest.(type) {
case *string:
+ if d == nil {
+ return errNilPtr
+ }
*d = string(s)
return nil
case *interface{}:
+ if d == nil {
+ return errNilPtr
+ }
bcopy := make([]byte, len(s))
copy(bcopy, s)
*d = bcopy
return nil
case *[]byte:
+ if d == nil {
+ return errNilPtr
+ }
*d = s
return nil
}
case nil:
switch d := dest.(type) {
case *[]byte:
+ if d == nil {
+ return errNilPtr
+ }
*d = nil
return nil
}
@@ -140,6 +160,9 @@ func convertAssign(dest, src interface{}) error {
if dpv.Kind() != reflect.Ptr {
return errors.New("destination not a pointer")
}
+ if dpv.IsNil() {
+ return errNilPtr
+ }
if !sv.IsValid() {
sv = reflect.ValueOf(src)
src/pkg/database/sql/sql_test.go
--- a/src/pkg/database/sql/sql_test.go
+++ b/src/pkg/database/sql/sql_test.go
@@ -696,3 +696,15 @@ func nullTestRun(t *testing.T, spec nullTestSpec) {
}
}
}
+
+// golang.org/issue/4859
+func TestQueryRowNilScanDest(t *testing.T) {
+ db := newTestDB(t, "people")
+ defer closeDB(t, db)
+ var name *string // nil pointer
+ err := db.QueryRow("SELECT|people|name|").Scan(name)
+ want := "sql: Scan error on column index 0: destination pointer is nil"
+ if err == nil || err.Error() != want {
+ t.Errorf("error = %q; want %q", err.Error(), want)
+ }
+}
コアとなるコードの解説
src/pkg/database/sql/convert.go
-
errNilPtr変数の追加:var errNilPtr = errors.New("destination pointer is nil") // embedded in descriptive errorconvertAssign関数内でnilポインタが検出された際に返される新しいエラーメッセージを定義しています。このエラーは、より詳細なエラーメッセージの一部として埋め込まれることを想定しています。 -
convertAssign関数内のnilポインタチェックの追加:convertAssign関数は、dest(スキャン先のポインタ)とsrc(データベースから読み取られた値)を受け取り、srcの値をdestに変換して割り当てる役割を担います。- 共通ケース(
string,[]byte,nilのsrc型):switch s := src.(type)ブロック内で、destが特定のポインタ型(*string,*[]byte,*interface{})に型アサーションされる直後に、if d == nil { return errNilPtr }というチェックが追加されました。これにより、destがnilポインタである場合に、reflectを使用する前にエラーを捕捉し、errNilPtrを返します。これは、頻繁に発生する可能性のあるケースで早期にエラーを検出するための最適化です。 - 汎用ケース(
reflectを使用):convertAssign関数の後半部分では、reflectパッケージを使用して、より汎用的な型変換と割り当てが行われます。この部分にもnilポインタのチェックが追加されました。if dpv.Kind() != reflect.Ptr { return errors.New("destination not a pointer") } if dpv.IsNil() { return errNilPtr }dpvはreflect.ValueOf(dest)の結果です。まず、destがポインタ型であることを確認し(dpv.Kind() != reflect.Ptr)、次にdpv.IsNil()を呼び出して、そのポインタがnilであるかどうかをチェックします。nilであれば、errNilPtrを返します。これにより、reflect.Value.Set()がnilポインタに対して呼び出されることによるパニックが防止されます。
- 共通ケース(
src/pkg/database/sql/sql_test.go
TestQueryRowNilScanDestテスト関数の追加:
このテストは、// golang.org/issue/4859 func TestQueryRowNilScanDest(t *testing.T) { db := newTestDB(t, "people") defer closeDB(t, db) var name *string // nil pointer err := db.QueryRow("SELECT|people|name|").Scan(name) want := "sql: Scan error on column index 0: destination pointer is nil" if err == nil || err.Error() != want { t.Errorf("error = %q; want %q", err.Error(), want) } }golang.org/issue/4859で報告された問題を再現し、修正が正しく機能することを確認するために追加されました。var name *stringを宣言することで、nameはnilポインタとして初期化されます。db.QueryRow(...).Scan(name)を呼び出すことで、nilポインタをScanメソッドに渡します。- テストは、返されたエラーが
nilではなく、かつ期待されるエラーメッセージ"sql: Scan error on column index 0: destination pointer is nil"と一致することを検証します。これにより、パニックが発生せず、適切なエラーが返されることが保証されます。
これらの変更により、database/sql パッケージは nil ポインタを Scan の宛先として受け取った際に、パニックではなく明確なエラーを返すようになり、より堅牢なデータベース操作が可能になりました。
関連リンク
- Go Issue #4859: https://golang.org/issue/4859
- Go CL 7383046: https://golang.org/cl/7383046
参考にした情報源リンク
- Go言語の公式ドキュメント (
database/sql,reflectパッケージ) - Go言語のIssueトラッカー (
golang.org/issue/4859) - Go言語のコードレビューシステム (
golang.org/cl/7383046) - Go言語におけるポインタとリフレクションに関する一般的な知識