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

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

このコミットは、Go言語の標準ライブラリである database/sql パッケージにおける、バイトスライス ([]byte) のコピー処理の最適化と、nil のバイトスライスが *interface{} 型にスキャンされる際の挙動の修正を目的としています。具体的には、[]byte のコピーをより効率的に行い、データベースの NULL 値が nil[]byte として正しく扱われるように改善しています。

コミット

commit 5e74f5029b15decee67095532a5c60cb98bbea52
Author: Julien Schmidt <google@julienschmidt.com>
Date:   Mon Mar 25 17:43:30 2013 -0700

    database/sql: optimized []byte copy + []byte(nil) -> *interface fix
    
    Make the copy directly in the convert switch instead of an extra loop.
    Also stops converting nil-[]byte to zero-[]byte when assigning to *interface
    
    R=golang-dev, bradfitz
    CC=golang-dev
    https://golang.org/cl/7962044

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

https://github.com/golang/go/commit/5e74f5029b15decee67095532a5c60cb98bbea52

元コミット内容

このコミットの元のメッセージは以下の通りです。

database/sql: optimized []byte copy + []byte(nil) -> *interface fix

Make the copy directly in the convert switch instead of an extra loop.
Also stops converting nil-[]byte to zero-[]byte when assigning to *interface

これは、database/sql パッケージにおけるバイトスライス ([]byte) のコピー処理を最適化し、nil のバイトスライスが *interface{} 型にスキャンされる際に、誤ってゼロ長 ([]byte{}) のバイトスライスに変換されてしまう問題を修正したことを示しています。

変更の背景

Goの database/sql パッケージは、データベースから取得したデータをGoの型に変換(スキャン)する際に、柔軟な型変換メカニズムを提供します。しかし、特にバイトスライス ([]byte) の扱いには注意が必要です。

  1. []byte のコピーの必要性: データベースから取得したバイトデータは、通常、ドライバ内部のバッファを指す []byte として提供されます。この []byte を直接ユーザーに渡すと、ドライバが内部バッファを再利用したり解放したりした際に、ユーザーが保持する []byte の内容が意図せず変更されたり、無効になったりする可能性があります。これを防ぐため、database/sql パッケージは、ユーザーに渡す前に []byte の内容を新しいメモリ領域にコピーする必要があります。このコミット以前は、このコピー処理が複数の場所で行われており、冗長性や非効率性がありました。

  2. nil []byte とゼロ長 []byte の区別: Goにおいて、nil[]byte とゼロ長 ([]byte{}) の []byte は異なる意味を持ちます。

    • nil[]byte は、通常、データベースの NULL 値に対応します。
    • ゼロ長の []byte は、空のバイト列(例: 空文字列のバイト表現)に対応します。 データベースの NULL 値をGoの nil []byte として正しく表現することは、データの意味論を維持するために非常に重要です。しかし、このコミット以前は、nil[]byte*interface{} 型にスキャンされる際に、誤ってゼロ長の []byte に変換されてしまうバグが存在していました。これにより、NULL 値と空のバイト列が区別できなくなるという問題が発生していました。

このコミットは、これらの問題を解決し、database/sql パッケージの堅牢性と正確性を向上させることを目的としています。

前提知識の解説

このコミットを理解するためには、以下のGo言語および database/sql パッケージに関する知識が必要です。

  1. Goのバイトスライス ([]byte):

    • Goのスライスは、配列への参照、長さ、容量を持つデータ構造です。
    • nil のスライスは、基盤となる配列への参照が nil で、長さと容量が0のスライスです。これは、メモリを割り当てられていない状態を示します。
    • ゼロ長のスライスは、基盤となる配列への参照が非nil で、長さが0のスライスです(例: make([]byte, 0))。これは、空のコレクションを表します。
    • nil とゼロ長のスライスは、等価演算子 (==) で比較すると false になります(ただし、nil スライスは nil と比較すると true)。この区別は、データベースの NULL 値を扱う上で重要です。
  2. database/sql パッケージの Scan メソッド:

    • Rows.Scan(dest ...interface{}) メソッドは、データベースの行の各カラムの値を、指定されたGoの変数 (dest) にスキャンします。
    • dest 引数は interface{} 型の可変長引数として渡されます。これは、任意の型のポインタを受け入れることを可能にします。
    • Scan メソッドの内部では、データベースの型とGoの変数の型に基づいて適切な型変換が行われます。
  3. interface{}:

    • Goの interface{} (空インターフェース) は、任意の型の値を保持できる型です。
    • *interface{} は、interface{} 型の値を指すポインタです。Scan メソッドでは、しばしば *interface{} を使用して、スキャン先の変数の型が実行時に決定されるような柔軟な処理を可能にします。
  4. reflect パッケージ:

    • database/sql パッケージの型変換ロジックは、Goの reflect パッケージを多用しています。reflect パッケージは、プログラムの実行時に型情報を検査し、値を操作するための機能を提供します。
    • convertAssign 関数は、reflect パッケージを使用して、ソース値 (src) をデスティネーション値 (dest) に変換して割り当てる主要なロジックを担っています。

技術的詳細

このコミットの技術的な変更点は主に以下の3つです。

  1. cloneBytes ヘルパー関数の導入: src/pkg/database/sql/convert.gocloneBytes([]byte) []byte という新しい関数が追加されました。この関数は、与えられたバイトスライス b を安全にコピーします。

    • もし bnil であれば、nil を返します。これは、nil[]bytenil のまま維持するために重要です。
    • もし bnil でなければ、len(b) と同じ長さの新しいバイトスライスを作成し、copy 関数を使って b の内容を新しいスライスにコピーして返します。 この関数により、[]byte のコピー処理が一元化され、コードの重複が排除されました。
  2. convertAssign 関数における []byte コピーの最適化と一元化: src/pkg/database/sql/convert.goconvertAssign 関数内で、[]byte*[]byte*interface{} にスキャンする際のコピーロジックが変更されました。

    • 以前は、make([]byte, len(s))copy(bcopy, s) を直接呼び出していましたが、これが新しく導入された cloneBytes(s) の呼び出しに置き換えられました。
    • これにより、[]byte のコピー処理が convertAssign 関数内の switch 文の適切な case ブロック内で直接行われるようになり、以前 (*Rows) Scan メソッドで行われていた冗長なループ処理が不要になりました。
  3. nil []byte から *interface{} へのスキャン修正: src/pkg/database/sql/convert.goconvertAssign 関数内の case nil: ブロックに、*interface{} 型へのスキャンを特別に処理する新しい case が追加されました。

    • case *interface{}: の追加により、ソース値が nil で、デスティネーションが *interface{} の場合、明示的に *d = nil が設定されるようになりました。
    • この変更により、データベースの NULL 値が nil[]byte として *interface{} にスキャンされる際に、誤ってゼロ長の []byte に変換されることがなくなりました。これは、NULL と空のバイト列の区別を正確に保つ上で非常に重要です。
  4. (*Rows) Scan からの冗長な []byte コピーロジックの削除: src/pkg/database/sql/sql.go(*Rows) Scan メソッドから、[]byte のコピーを行うための追加の for ループが完全に削除されました。このコピー処理は、convertAssign 関数内の cloneBytes の呼び出しによって一元的に処理されるようになったため、不要になりました。

これらの変更により、database/sql パッケージは []byte のコピーをより効率的に行い、nil[]byte のセマンティクスを *interface{} 型にスキャンする際にも正しく維持できるようになりました。

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

src/pkg/database/sql/convert.go

--- a/src/pkg/database/sql/convert.go
+++ b/src/pkg/database/sql/convert.go
@@ -112,15 +112,13 @@ func convertAssign(dest, src interface{}) error {
 			if d == nil {
 				return errNilPtr
 			}
-			bcopy := make([]byte, len(s))
-			copy(bcopy, s)
-			*d = bcopy
+			*d = cloneBytes(s)
 			return nil
 		case *[]byte:
 			if d == nil {
 				return errNilPtr
 			}
-			*d = s
+			*d = cloneBytes(s)
 			return nil
 		case *RawBytes:
 			if d == nil {
@@ -131,6 +129,12 @@ func convertAssign(dest, src interface{}) error {
 		}
 	case nil:
 		switch d := dest.(type) {
+		case *interface{}:
+			if d == nil {
+				return errNilPtr
+			}
+			*d = nil
+			return nil
 		case *[]byte:
 			if d == nil {
 				return errNilPtr
@@ -250,6 +254,16 @@ func convertAssign(dest, src interface{}) error {
 	return fmt.Errorf("unsupported driver -> Scan pair: %T -> %T", src, dest)
 }
 
+func cloneBytes(b []byte) []byte {
+	if b == nil {
+		return nil
+	} else {
+		c := make([]byte, len(b))
+		copy(c, b)
+		return c
+	}
+}
+
 func asString(src interface{}) string {
 	switch v := src.(type) {
 	case string:

src/pkg/database/sql/sql.go

--- a/src/pkg/database/sql/sql.go
+++ b/src/pkg/database/sql/sql.go
@@ -1301,24 +1301,6 @@ func (rs *Rows) Scan(dest ...interface{}) error {
 			return fmt.Errorf("sql: Scan error on column index %d: %v", i, err)
 		}
 	}
-	for _, dp := range dest {
-		b, ok := dp.(*[]byte)
-		if !ok {
-			continue
-		}
-		if *b == nil {
-			// If the []byte is now nil (for a NULL value),
-			// don't fall through to below which would
-			// turn it into a non-nil 0-length byte slice
-			continue
-		}
-		if _, ok = dp.(*RawBytes); ok {
-			continue
-		}
-		clone := make([]byte, len(*b))
-		copy(clone, *b)
-		*b = clone
-	}
 	return nil
 }
 

src/pkg/database/sql/convert_test.go

--- a/src/pkg/database/sql/convert_test.go
+++ b/src/pkg/database/sql/convert_test.go
@@ -143,6 +143,7 @@ var conversionTests = []conversionTest{
 	{s: []byte("byteslice"), d: &scaniface, wantiface: []byte("byteslice")},
 	{s: true, d: &scaniface, wantiface: true},
 	{s: nil, d: &scaniface},
+	{s: []byte(nil), d: &scaniface, wantiface: []byte(nil)},
 }
 
 func intPtrValue(intptr interface{}) interface{} {
@@ -221,7 +222,7 @@ func TestConversions(t *testing.T) {
 			}
 			if srcBytes, ok := ct.s.([]byte); ok {
 				dstBytes := (*ifptr).([]byte)
-				if &dstBytes[0] == &srcBytes[0] {
+				if len(srcBytes) > 0 && &dstBytes[0] == &srcBytes[0] {
 					errf("copy into interface{} didn't copy []byte data")
 				}
 			}

コアとなるコードの解説

src/pkg/database/sql/convert.go の変更

  • cloneBytes 関数の追加: この新しいヘルパー関数は、[]byte のコピー処理をカプセル化します。nil[]byte が渡された場合は nil を返し、それ以外の場合は新しいスライスを作成して内容をコピーします。これにより、[]byte のコピーが常に安全かつ意図通りに行われるようになります。

  • convertAssign 関数内の []byte コピーの変更: case *interface{}:case *[]byte: の両方で、以前の makecopy の直接呼び出しが cloneBytes(s) に置き換えられました。これにより、[]byte のコピーロジックが一元化され、コードの可読性と保守性が向上しました。

  • case nil: ブロック内の *interface{}: の追加: これは、nil のソース値が *interface{} 型のデスティネーションにスキャンされる際の挙動を修正する最も重要な変更です。以前は、nil[]byte*interface{} にスキャンされると、nil ではなくゼロ長の []byte に変換されてしまう可能性がありました。この新しい case により、ソースが nil でデスティネーションが *interface{} の場合、明示的に *d = nil が設定され、NULL 値が正しく nil として表現されるようになりました。

src/pkg/database/sql/sql.go の変更

  • (*Rows) Scan メソッドからの []byte コピーロジックの削除: 以前の Scan メソッドには、スキャンされた []byte 値をさらにコピーするための for ループが含まれていました。このループは、convertAssign 関数が cloneBytes を使用してコピーを適切に処理するようになったため、冗長になりました。このループを削除することで、コードが簡素化され、[]byte のコピーが convertAssign 内で一元的に行われるようになりました。

src/pkg/database/sql/convert_test.go の変更

  • conversionTests へのテストケース追加: {s: []byte(nil), d: &scaniface, wantiface: []byte(nil)} という新しいテストケースが追加されました。これは、nil[]byte*interface{} にスキャンされたときに、結果が nil[]byte であることを明示的に検証します。これにより、nil のバイトスライスがゼロ長に変換されるバグが修正されたことを確認できます。

  • TestConversions 内のポインタ比較の修正: if len(srcBytes) > 0 && &dstBytes[0] == &srcBytes[0] という条件が追加されました。これは、[]byte のコピーが正しく行われたことを検証する際に、ゼロ長の srcBytes の場合に &srcBytes[0] がパニックを起こすのを防ぐためのガードです。len(srcBytes) > 0 のチェックにより、有効なバイトスライスに対してのみポインタ比較が行われるようになり、テストの堅牢性が向上しました。

これらの変更は、database/sql パッケージの内部的な堅牢性と正確性を高め、特に []byteNULL 値の扱いにおける潜在的なバグを修正しています。

関連リンク

参考にした情報源リンク