[インデックス 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
) の扱いには注意が必要です。
-
[]byte
のコピーの必要性: データベースから取得したバイトデータは、通常、ドライバ内部のバッファを指す[]byte
として提供されます。この[]byte
を直接ユーザーに渡すと、ドライバが内部バッファを再利用したり解放したりした際に、ユーザーが保持する[]byte
の内容が意図せず変更されたり、無効になったりする可能性があります。これを防ぐため、database/sql
パッケージは、ユーザーに渡す前に[]byte
の内容を新しいメモリ領域にコピーする必要があります。このコミット以前は、このコピー処理が複数の場所で行われており、冗長性や非効率性がありました。 -
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
パッケージに関する知識が必要です。
-
Goのバイトスライス (
[]byte
):- Goのスライスは、配列への参照、長さ、容量を持つデータ構造です。
nil
のスライスは、基盤となる配列への参照がnil
で、長さと容量が0のスライスです。これは、メモリを割り当てられていない状態を示します。- ゼロ長のスライスは、基盤となる配列への参照が非
nil
で、長さが0のスライスです(例:make([]byte, 0)
)。これは、空のコレクションを表します。 nil
とゼロ長のスライスは、等価演算子 (==
) で比較するとfalse
になります(ただし、nil
スライスはnil
と比較するとtrue
)。この区別は、データベースのNULL
値を扱う上で重要です。
-
database/sql
パッケージのScan
メソッド:Rows.Scan(dest ...interface{})
メソッドは、データベースの行の各カラムの値を、指定されたGoの変数 (dest
) にスキャンします。dest
引数はinterface{}
型の可変長引数として渡されます。これは、任意の型のポインタを受け入れることを可能にします。Scan
メソッドの内部では、データベースの型とGoの変数の型に基づいて適切な型変換が行われます。
-
interface{}
型:- Goの
interface{}
(空インターフェース) は、任意の型の値を保持できる型です。 *interface{}
は、interface{}
型の値を指すポインタです。Scan
メソッドでは、しばしば*interface{}
を使用して、スキャン先の変数の型が実行時に決定されるような柔軟な処理を可能にします。
- Goの
-
reflect
パッケージ:database/sql
パッケージの型変換ロジックは、Goのreflect
パッケージを多用しています。reflect
パッケージは、プログラムの実行時に型情報を検査し、値を操作するための機能を提供します。convertAssign
関数は、reflect
パッケージを使用して、ソース値 (src
) をデスティネーション値 (dest
) に変換して割り当てる主要なロジックを担っています。
技術的詳細
このコミットの技術的な変更点は主に以下の3つです。
-
cloneBytes
ヘルパー関数の導入:src/pkg/database/sql/convert.go
にcloneBytes([]byte) []byte
という新しい関数が追加されました。この関数は、与えられたバイトスライスb
を安全にコピーします。- もし
b
がnil
であれば、nil
を返します。これは、nil
の[]byte
をnil
のまま維持するために重要です。 - もし
b
がnil
でなければ、len(b)
と同じ長さの新しいバイトスライスを作成し、copy
関数を使ってb
の内容を新しいスライスにコピーして返します。 この関数により、[]byte
のコピー処理が一元化され、コードの重複が排除されました。
- もし
-
convertAssign
関数における[]byte
コピーの最適化と一元化:src/pkg/database/sql/convert.go
のconvertAssign
関数内で、[]byte
を*[]byte
や*interface{}
にスキャンする際のコピーロジックが変更されました。- 以前は、
make([]byte, len(s))
とcopy(bcopy, s)
を直接呼び出していましたが、これが新しく導入されたcloneBytes(s)
の呼び出しに置き換えられました。 - これにより、
[]byte
のコピー処理がconvertAssign
関数内のswitch
文の適切なcase
ブロック内で直接行われるようになり、以前(*Rows) Scan
メソッドで行われていた冗長なループ処理が不要になりました。
- 以前は、
-
nil
[]byte
から*interface{}
へのスキャン修正:src/pkg/database/sql/convert.go
のconvertAssign
関数内のcase nil:
ブロックに、*interface{}
型へのスキャンを特別に処理する新しいcase
が追加されました。case *interface{}:
の追加により、ソース値がnil
で、デスティネーションが*interface{}
の場合、明示的に*d = nil
が設定されるようになりました。- この変更により、データベースの
NULL
値がnil
の[]byte
として*interface{}
にスキャンされる際に、誤ってゼロ長の[]byte
に変換されることがなくなりました。これは、NULL
と空のバイト列の区別を正確に保つ上で非常に重要です。
-
(*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:
の両方で、以前のmake
とcopy
の直接呼び出しが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
パッケージの内部的な堅牢性と正確性を高め、特に []byte
と NULL
値の扱いにおける潜在的なバグを修正しています。
関連リンク
- Go
database/sql
パッケージのドキュメント: https://pkg.go.dev/database/sql - Go
reflect
パッケージのドキュメント: https://pkg.go.dev/reflect
参考にした情報源リンク
- Goの公式リポジトリのコミット: https://github.com/golang/go/commit/5e74f5029b15decee67095532a5c60cb98bbea52
- Gerrit Change-ID:
https://golang.org/cl/7962044
(このコミットの元のコードレビューへのリンク) - Go slices: usage and internals: https://go.dev/blog/slices-usage-and-internals (Goのスライスの内部構造に関する公式ブログ記事)
- Effective Go - Slices: https://go.dev/doc/effective_go#slices (Goのスライスの効果的な使い方に関する公式ドキュメント)
- Go database/sql tutorial: https://go.dev/doc/database/ (Goの
database/sql
パッケージのチュートリアル)