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

[インデックス 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データベースとの一般的なインターフェースを提供します。このパッケージ自体は特定のデータベースドライバを含まず、ドライバは外部パッケージとして提供されます。QueryRowScan などのメソッドを通じて、データベースからの結果の取得と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 であるかどうかを明示的にチェックしていませんでした。そのため、destnil ポインタである場合、reflect パッケージの操作(特に reflect.ValueOf(dest) から得られる reflect.ValueIsNil()true を返すような状況)において、その後の値の割り当て処理でパニックが発生する可能性がありました。

このコミットでは、以下の2つの主要な方法で nil ポインタのチェックが導入されました。

  1. 共通ケースでの明示的な nil ポインタチェック: convertAssign 関数内の switch s := src.(type) ブロックにおいて、string[]bytenil の各 src 型に対する dest の型アサーション(例: case *string:)の直後に、if d == nil { return errNilPtr } というチェックが追加されました。これにより、reflect を使用する前の段階で、一般的な型(*string, *[]byte, *interface{})へのスキャンにおいて nil ポインタが検出された場合に、カスタムエラー errNilPtr を即座に返すようになりました。これは、reflect を介した処理よりも高速にエラーを検出できるため、パフォーマンス上の利点もあります。

  2. reflect を使用するケースでの nil ポインタチェック: convertAssign 関数の後半部分で、reflect パッケージを使用して汎用的な型変換と割り当てを行う前に、dpv.IsNil() メソッドによるチェックが追加されました。

    if dpv.Kind() != reflect.Ptr {
        return errors.New("destination not a pointer")
    }
    if dpv.IsNil() {
        return errNilPtr
    }
    

    ここで dpvreflect.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

  1. errNilPtr 変数の追加:

    var errNilPtr = errors.New("destination pointer is nil") // embedded in descriptive error
    

    convertAssign 関数内で nil ポインタが検出された際に返される新しいエラーメッセージを定義しています。このエラーは、より詳細なエラーメッセージの一部として埋め込まれることを想定しています。

  2. convertAssign 関数内の nil ポインタチェックの追加: convertAssign 関数は、dest(スキャン先のポインタ)と src(データベースから読み取られた値)を受け取り、src の値を dest に変換して割り当てる役割を担います。

    • 共通ケース(string, []byte, nilsrc 型): switch s := src.(type) ブロック内で、dest が特定のポインタ型(*string, *[]byte, *interface{})に型アサーションされる直後に、if d == nil { return errNilPtr } というチェックが追加されました。これにより、destnil ポインタである場合に、reflect を使用する前にエラーを捕捉し、errNilPtr を返します。これは、頻繁に発生する可能性のあるケースで早期にエラーを検出するための最適化です。
    • 汎用ケース(reflect を使用): convertAssign 関数の後半部分では、reflect パッケージを使用して、より汎用的な型変換と割り当てが行われます。この部分にも nil ポインタのチェックが追加されました。
      if dpv.Kind() != reflect.Ptr {
          return errors.New("destination not a pointer")
      }
      if dpv.IsNil() {
          return errNilPtr
      }
      
      dpvreflect.ValueOf(dest) の結果です。まず、dest がポインタ型であることを確認し(dpv.Kind() != reflect.Ptr)、次に dpv.IsNil() を呼び出して、そのポインタが nil であるかどうかをチェックします。nil であれば、errNilPtr を返します。これにより、reflect.Value.Set()nil ポインタに対して呼び出されることによるパニックが防止されます。

src/pkg/database/sql/sql_test.go

  1. 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 を宣言することで、namenil ポインタとして初期化されます。
    • db.QueryRow(...).Scan(name) を呼び出すことで、nil ポインタを Scan メソッドに渡します。
    • テストは、返されたエラーが nil ではなく、かつ期待されるエラーメッセージ "sql: Scan error on column index 0: destination pointer is nil" と一致することを検証します。これにより、パニックが発生せず、適切なエラーが返されることが保証されます。

これらの変更により、database/sql パッケージは nil ポインタを Scan の宛先として受け取った際に、パニックではなく明確なエラーを返すようになり、より堅牢なデータベース操作が可能になりました。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント (database/sql, reflect パッケージ)
  • Go言語のIssueトラッカー (golang.org/issue/4859)
  • Go言語のコードレビューシステム (golang.org/cl/7383046)
  • Go言語におけるポインタとリフレクションに関する一般的な知識