[インデックス 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パッケージのチュートリアル)