[インデックス 11427] ファイルの概要
このコミットは、Go言語の標準ライブラリ database/sql
パッケージにおける、データベースのNULL値をGoの[]byte
型にスキャンする際の挙動を修正し、string
型の値を[]byte
型にスキャンできるように拡張するものです。具体的には、SQLのNULL値が[]byte{}
(空のバイトスライス)ではなくnil
(ゼロ値)としてGoの[]byte
変数に変換されるように改善され、Goにおけるnil
と空のスライスのセマンティックな違いが正しく反映されるようになりました。
コミット
commit 2a22f35598bba353f13d4808b4c4d710fa125f43
Author: James P. Cooper <jamespcooper@gmail.com>
Date: Thu Jan 26 15:12:48 2012 -0800
database/sql: convert SQL null values to []byte as nil.
Also allow string values to scan into []byte.
Fixes #2788.
R=golang-dev, bradfitz
CC=golang-dev
https://golang.org/cl/5577054
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/2a22f35598bba353f13d4808b4c4d710fa125f43
元コミット内容
database/sql: convert SQL null values to []byte as nil.
Also allow string values to scan into []byte.
Fixes #2788.
変更の背景
このコミットの主な背景は、Go言語のdatabase/sql
パッケージがSQLのNULL値を[]byte
型にスキャンする際の不適切な挙動を修正することにありました。以前のバージョンでは、データベースから取得したNULL値がGoの[]byte
変数にスキャンされると、nil
ではなく長さ0のバイトスライス([]byte{}
)として扱われていました。
Go言語において、nil
スライスと長さ0のスライスは異なる意味を持ちます。
nil
スライスは、そのスライスが何も参照していない状態、つまり「値が存在しない」ことを示唆します。これはSQLのNULL値のセマンティクスと一致します。- 長さ0のスライス(
[]byte{}
)は、スライス自体は存在し、メモリを占有しているが、要素が一つもない状態、つまり「空のデータ」を示します。これはSQLの空文字列や空のバイナリデータに相当します。
この違いは、アプリケーションロジックにおいてNULL値と空の値を区別する必要がある場合に重要となります。例えば、データベースの特定のカラムがNULLを許容し、そのNULLが「データがない」ことを意味する場合、[]byte{}
としてスキャンされると、それが空の文字列なのか、それとも本当にデータがないのかを区別できなくなり、誤った処理につながる可能性がありました。
この問題はGoのIssue #2788として報告されており、このコミットはその修正を目的としています。また、副次的な改善として、データベースから取得したstring
型の値を直接[]byte
型にスキャンできるようにすることで、開発者の利便性を向上させています。
前提知識の解説
Go言語のdatabase/sql
パッケージ
database/sql
パッケージは、Go言語でSQLデータベースを操作するための標準インターフェースを提供します。このパッケージは、特定のデータベースドライバに依存しない汎用的なAPIを提供し、開発者はドライバを切り替えるだけで異なるデータベース(PostgreSQL, MySQL, SQLiteなど)と連携できます。
主要な概念は以下の通りです。
DB
: データベースへの接続プールを表します。Stmt
: プリペアドステートメントを表します。Rows
: クエリ結果の行をイテレートするためのインターフェースです。Row
: 単一の行をスキャンするためのヘルパー型です。Scan
メソッド:Rows
やRow
から取得したデータベースの値をGoの変数に変換(スキャン)するために使用されます。この変換プロセスが本コミットの主要な変更点です。
SQLのNULL値
SQLにおけるNULL
は、「値がない」「不明な値」「適用できない値」を表す特別なマーカーです。これは、空文字列(''
)やゼロ(0
)とは明確に区別されます。例えば、VARCHAR
型のカラムがNULL
を許容する場合、そのカラムに値が設定されていない状態はNULL
であり、空文字列とは異なります。
Go言語におけるnil
と空のスライス
Go言語では、スライスは基底配列へのポインタ、長さ、容量の3つの要素から構成されるデータ構造です。
nil
スライス: スライス変数がどの基底配列も参照していない状態です。var s []byte
と宣言した直後のs
はnil
です。len(s)
とcap(s)
は両方とも0になります。nil
スライスは、値が存在しないことを明確に示します。- 空のスライス: スライス変数が長さ0の基底配列を参照している状態です。
make([]byte, 0)
や[]byte{}
のように作成されます。len(s)
とcap(s)
は両方とも0になりますが、スライス自体はnil
ではありません。これは、空のデータセットや空のコレクションを表す際に使用されます。
nil
と空のスライスの区別は、特にデータベースのNULL値を扱う際に重要です。SQLのNULLはGoのnil
に、SQLの空文字列はGoの空のスライスにマッピングされるのが自然なセマンティクスです。
Scan
メソッドの内部動作
database/sql
パッケージのScan
メソッドは、データベースドライバから取得した生データを、ユーザーが指定したGoの変数型に変換する役割を担います。この変換は、内部的にconvertAssign
関数のようなヘルパー関数を通じて行われます。Scan
は、ターゲット変数の型を検査し、それに応じて適切な型変換ロジックを適用します。例えば、データベースのINT
型をGoのint
型に、VARCHAR
型をstring
型に変換します。本コミットでは、この変換ロジック、特に[]byte
型への変換パスが修正されました。
技術的詳細
このコミットは、主にsrc/pkg/database/sql/convert.go
とsrc/pkg/database/sql/sql.go
の2つのファイルにわたる変更によって、SQLのNULL値から[]byte
への変換と、string
から[]byte
への変換の挙動を改善しています。
src/pkg/database/sql/convert.go
の変更
convert.go
ファイルは、database/sql
パッケージ内でデータベースの値をGoの型に変換するロジックをカプセル化しています。特にconvertAssign
関数は、ソースの型とデスティネーションの型に基づいて適切な変換を行います。
-
string
から[]byte
への変換の追加: 以前は、string
型のソースを[]byte
型のデスティネーションに直接スキャンするパスが明示的に存在しませんでした。このコミットにより、convertAssign
関数内のcase string:
ブロックに、*[]byte
型への変換ケースが追加されました。case *[]byte: *d = []byte(s) return nil
これにより、データベースから取得した文字列データが、Goの
[]byte
変数に直接コピーされるようになります。 -
nil
から[]byte
への変換の修正: 最も重要な変更は、convertAssign
関数内のcase nil:
ブロックに*[]byte
型への変換ケースが追加されたことです。case nil: switch d := dest.(type) { case *[]byte: *d = nil return nil }
この変更により、データベースから取得した値がSQLのNULLである場合(Goの
nil
として表現される)、デスティネーションが*[]byte
型であれば、その[]byte
変数は明示的にnil
に設定されるようになりました。これにより、以前の[]byte{}
(空のスライス)ではなく、nil
スライスが割り当てられるようになり、SQLのNULLのセマンティクスがGoの型システムに正しくマッピングされます。
src/pkg/database/sql/sql.go
の変更
sql.go
ファイルは、Rows
やRow
といった主要なデータ構造と、それらのScan
メソッドの実装を含んでいます。
-
Rows.Scan
におけるNULL[]byte
の処理:Rows.Scan
メソッド内で、[]byte
型のデスティネーションに対する追加のチェックが導入されました。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 }
このコードは、
convert.go
で既にnil
に設定された[]byte
変数が、Rows.Scan
内の後続の処理(例えば、RawBytes
の処理や、バイトスライスが一時メモリを参照している場合の防御的コピー)によって誤って非nil
の長さ0のバイトスライスに変換されるのを防ぎます。*b
がnil
であれば、その後の処理をスキップし、nil
の状態を維持します。 -
Row.Scan
の簡素化と再配置:Row.Scan
メソッドは、単一の行をスキャンするためのヘルパーです。このメソッド内の[]byte
の防御的コピーに関するロジックが変更されました。以前は、Row.Scan
内で[]byte
の値を防御的にコピーするロジックが存在しましたが、このコミットではそのロジックが削除され、Rows.Scan
(またはconvertAssign
)がnil
の[]byte
を正しく処理するようになったため、Row.Scan
での特別な扱いは不要になりました。 また、defer r.rows.Close()
、r.rows.Next()
、r.rows.Scan(dest...)
といった行を処理する主要なロジックの順序が、[]byte
のコピー処理(削除された部分)の後に移動されました。これは、Rows.Scan
がRawBytes
を許可しないというコメントの更新と合わせて、database/sql
パッケージ全体でのバイトスライスの扱いの一貫性を高めるための変更です。
src/pkg/database/sql/sql_test.go
の変更
このコミットには、TestNullByteSlice
という新しいテスト関数が追加されました。このテストは、Issue #2788で報告された問題を具体的に検証するために設計されています。
-
NULL値のテスト:
CREATE
文でnullstring
カラムを持つテーブルを作成します。INSERT
文でname
カラムにnil
(SQLのNULLに相当)を挿入します。QueryRow().Scan(&name)
を使って、このNULL値を[]byte
変数name
にスキャンします。if name != nil
というアサーションで、name
がnil
であることを確認します。これにより、SQLのNULLが正しくGoのnil
[]byte
に変換されることが検証されます。
-
非NULL値(文字列)のテスト:
INSERT
文でname
カラムに文字列"bob"
を挿入します。- 同様に
QueryRow().Scan(&name)
でname
にスキャンします。 if string(name) != "bob"
というアサーションで、name
が正しく"bob"
というバイトスライスに変換されていることを確認します。これにより、string
から[]byte
への変換機能も検証されます。
このテストの追加により、NULL値の[]byte
へのスキャンが正しく行われることが保証され、将来のリグレッションを防ぐことができます。
コアとなるコードの変更箇所
src/pkg/database/sql/convert.go
--- a/src/pkg/database/sql/convert.go
+++ b/src/pkg/database/sql/convert.go
@@ -40,6 +40,9 @@ func convertAssign(dest, src interface{}) error {
case *string:
*d = s
return nil
+ case *[]byte:
+ *d = []byte(s)
+ return nil
}
case []byte:
switch d := dest.(type) {
@@ -50,6 +53,12 @@ func convertAssign(dest, src interface{}) error {
*d = s
return nil
}
+ case nil:
+ switch d := dest.(type) {
+ case *[]byte:
+ *d = nil
+ return nil
+ }
}
var sv reflect.Value
src/pkg/database/sql/sql.go
--- a/src/pkg/database/sql/sql.go
+++ b/src/pkg/database/sql/sql.go
@@ -904,6 +904,12 @@ func (rs *Rows) Scan(dest ...interface{}) error {
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
}
@@ -945,17 +951,10 @@ func (r *Row) Scan(dest ...interface{}) error {
if r.err != nil {
return r.err
}
- defer r.rows.Close()
- if !r.rows.Next() {
- return ErrNoRows
- }
- err := r.rows.Scan(dest...)
- if err != nil {
- return err
- }
// TODO(bradfitz): for now we need to defensively clone all
- // []byte that the driver returned, since we're about to close
+ // []byte that the driver returned (not permitting
+ // *RawBytes in Rows.Scan), since we're about to close
// the Rows in our defer, when we return from this function.
// the contract with the driver.Next(...) interface is that it
// can return slices into read-only temporary memory that's
@@ -970,14 +969,17 @@ func (r *Row) Scan(dest ...interface{}) error {
if _, ok := dp.(*RawBytes); ok {
return errors.New("sql: RawBytes isn't allowed on Row.Scan")
}
- b, ok := dp.(*[]byte)
- if !ok {
- continue
- }
- clone := make([]byte, len(*b))
- copy(clone, *b)
- *b = clone
}
+
+ defer r.rows.Close()
+ if !r.rows.Next() {
+ return ErrNoRows
+ }
+ err := r.rows.Scan(dest...)
+ if err != nil {
+ return err
+ }
+
return nil
}
src/pkg/database/sql/sql_test.go
--- a/src/pkg/database/sql/sql_test.go
+++ b/src/pkg/database/sql/sql_test.go
@@ -358,6 +358,34 @@ func TestIssue2542Deadlock(t *testing.T) {
}
}
+// Tests fix for issue 2788, that we bind nil to a []byte if the
+// value in the column is sql null
+func TestNullByteSlice(t *testing.T) {
+ db := newTestDB(t, "")
+ defer closeDB(t, db)
+ exec(t, db, "CREATE|t|id=int32,name=nullstring")
+ exec(t, db, "INSERT|t|id=10,name=?", nil)
+
+ var name []byte
+
+ err := db.QueryRow("SELECT|t|name|id=?", 10).Scan(&name)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if name != nil {
+ t.Fatalf("name []byte should be nil for null column value, got: %#v", name)
+ }
+
+ exec(t, db, "INSERT|t|id=11,name=?", "bob")
+ err = db.QueryRow("SELECT|t|name|id=?", 11).Scan(&name)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if string(name) != "bob" {
+ t.Fatalf("name []byte should be bob, got: %q", string(name))
+ }
+}
+
func TestQueryRowClosingStmt(t *testing.T) {
db := newTestDB(t, "people")
defer closeDB(t, db)
コアとなるコードの解説
src/pkg/database/sql/convert.go
-
case *[]byte:
(string source): この追加により、convertAssign
関数は、ソースデータがstring
型であり、かつデスティネーションが*[]byte
型(バイトスライスへのポインタ)である場合に、文字列をバイトスライスに変換してデスティネーションに割り当てることができるようになりました。具体的には、*d = []byte(s)
という行が、文字列s
をバイトスライスに型変換し、それをポインタd
が指す[]byte
変数に代入します。これにより、データベースから取得した文字列を直接[]byte
変数にスキャンする際の柔軟性が向上します。 -
case *[]byte:
(nil source): この変更は、SQLのNULL値のセマンティクスをGoの[]byte
型に正しくマッピングするための最も重要な部分です。ソースデータがnil
(SQLのNULLに相当)であり、デスティネーションが*[]byte
型である場合、*d = nil
という行が実行されます。これにより、デスティネーションの[]byte
変数は明示的にnil
に設定され、以前のように長さ0のバイトスライス([]byte{}
)になることを防ぎます。この修正により、GoのアプリケーションはSQLのNULL値を正確に区別できるようになります。
src/pkg/database/sql/sql.go
-
Rows.Scan
内のif *b == nil
チェック:Rows.Scan
メソッドは、データベースから取得した各カラムの値をGoの変数にスキャンする中心的なロジックを含んでいます。このコミットで追加されたif *b == nil
チェックは、convert.go
で既にnil
に設定された[]byte
変数が、Rows.Scan
内の後続の処理によって意図せず非nil
の空のスライスに変換されるのを防ぐための防御的な措置です。もし*b
がnil
であれば、それはSQLのNULL値が正しく変換されたことを意味するため、それ以上の処理は不要であり、continue
して次のデスティネーションの処理に移ります。 -
Row.Scan
の変更:Row.Scan
メソッドは、単一の行をスキャンするための簡便なラッパーです。このコミットでは、Row.Scan
内の[]byte
の防御的コピーに関するロジックが削除されました。これは、Rows.Scan
(およびconvertAssign
)がnil
の[]byte
を正しく処理し、またRawBytes
の扱いに関するコメントが更新されたことと関連しています。Row.Scan
の主要なロジック(defer r.rows.Close()
、r.rows.Next()
、r.rows.Scan(dest...)
)の配置が変更され、よりクリーンな構造になりました。これにより、database/sql
パッケージ全体でのバイトスライスのメモリ管理とNULL値の処理が一貫性を持ち、簡素化されました。
src/pkg/database/sql/sql_test.go
TestNullByteSlice
関数: この新しいテスト関数は、本コミットの主要な修正(SQL NULL値から[]byte
へのnil
変換)と、副次的な改善(string
から[]byte
へのスキャン)の両方を検証します。- NULL値の検証: データベースにNULL値を挿入し、それを
[]byte
変数にスキャンした後、if name != nil
という条件でname
がnil
であることを確認します。これにより、NULL値が正しくnil
[]byte
として扱われることが保証されます。 - 文字列値の検証: データベースに文字列
"bob"
を挿入し、それを[]byte
変数にスキャンした後、if string(name) != "bob"
という条件でname
が"bob"
というバイトスライスとして正しく変換されていることを確認します。
- NULL値の検証: データベースにNULL値を挿入し、それを
このテストの追加は、修正が正しく機能していることを確認し、将来のコード変更によるリグレッションを防ぐための重要なステップです。
関連リンク
- Go Issue #2788: database/sql: Scan of NULL into []byte results in []byte{} instead of nil
- Go CL 5577054: database/sql: convert SQL null values to []byte as nil.
参考にした情報源リンク
- Go言語
database/sql
パッケージ公式ドキュメント: - Go言語におけるスライス(
nil
と空のスライスの違い)に関する解説記事:- https://go.dev/blog/slices-intro (Go公式ブログのスライス入門)
- https://yourbasic.org/golang/slice-syntax-examples/ (Goのスライスに関する一般的な情報)
- SQL NULLの概念: