[インデックス 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 error
convertAssign
関数内で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言語におけるポインタとリフレクションに関する一般的な知識