[インデックス 11207] ファイルの概要
このコミットは、Go言語の実験的なexp/sql
パッケージにおける、データベースからの[]byte
型へのスキャン時の挙動に関する重要な変更を導入しています。具体的には、デフォルトで[]byte
型へのスキャン時にデータのコピーを作成するようにし、データの所有権とライフサイクルに関する潜在的なバグを防ぐことを目的としています。
コミット
exp/sql
: []byte
へのスキャン時にデフォルトでコピーを作成
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/ebc8013edfc009b1190c656e738b15fe9729cc89
元コミット内容
exp/sql: copy when scanning into []byte by default
Fixes #2698
R=rsc
CC=golang-dev
https://golang.org/cl/5539060
変更の背景
Go言語のdatabase/sql
パッケージ(当時はexp/sql
として実験段階)において、データベースからバイナリデータ(例えばBLOBやTEXTカラムのバイト表現)を[]byte
型の変数にスキャンする際、その[]byte
スライスがデータベースドライバの内部バッファへの参照を保持していることがありました。この挙動は、パフォーマンスの観点からは効率的である一方で、深刻な問題を引き起こす可能性がありました。
具体的には、Rows.Next()
が次に呼び出されたり、Rows.Scan()
が再度呼び出されたり、あるいはRows
がクローズされたりすると、内部バッファが再利用されたり解放されたりするため、以前にスキャンされた[]byte
スライスの内容が突然変更されたり、無効になったりする「データ競合」や「無効なメモリ参照」の問題が発生する可能性がありました。これは、ユーザーがスキャンしたデータを後で利用しようとした際に、予期せぬ値になったり、クラッシュを引き起こしたりする原因となります。
この問題は、GoのIssue #2698として報告されており、このコミットはその問題を解決するために導入されました。開発者は、デフォルトの挙動として安全性を優先し、[]byte
へのスキャン時にはデータのコピーを作成することで、ユーザーが取得したデータのライフサイクルを完全に制御できるようにする必要があると判断しました。
前提知識の解説
このコミットを理解するためには、以下のGo言語およびデータベース関連の概念を理解しておく必要があります。
database/sql
パッケージ (旧exp/sql
): Go言語の標準ライブラリの一部であり、SQLデータベースとのインタラクションのための汎用的なインターフェースを提供します。このパッケージ自体は特定のデータベースドライバを含まず、ドライバは別途実装され、このインターフェースに準拠します。Rows.Scan()
メソッド:database/sql
パッケージにおいて、クエリ結果セットの現在の行のデータをGoの変数に読み込むために使用されるメソッドです。引数として、スキャンするカラムの数に対応するポインタのリストを受け取ります。[]byte
型: Go言語におけるバイトスライスです。可変長で、バイナリデータやUTF-8エンコードされた文字列などを扱うのに一般的に使用されます。- ポインタ (
*T
): Goにおいて、変数のメモリアドレスを指し示す型です。Scan
メソッドは、値を直接受け取るのではなく、ポインタを介して変数の内容を更新します。 - データの所有権とライフサイクル: プログラミングにおいて、データがどのメモリ領域に存在し、いつそのメモリが有効であるか、誰がそのメモリを解放する責任を持つか、という概念です。特に、共有される可能性のあるデータや、内部バッファから提供されるデータの場合に重要になります。
- ディープコピーとシャローコピー:
- シャローコピー: データの参照(ポインタ)のみをコピーし、実際のデータは元の場所を共有します。元のデータが変更されると、コピーされたデータも影響を受けます。
- ディープコピー: データのすべての内容を新しいメモリ領域にコピーします。元のデータとコピーされたデータは完全に独立しており、一方の変更がもう一方に影響を与えることはありません。
make([]byte, len(src))
とcopy(dst, src)
: Goでバイトスライスのディープコピーを作成する際の一般的なイディオムです。make
で新しいスライスを割り当て、copy
で元のスライスの内容を新しいスライスにコピーします。
技術的詳細
このコミットの核心は、Rows.Scan
メソッドの内部ロジックの変更と、新しい型RawBytes
の導入にあります。
Rows.Scan
の変更点
以前のRows.Scan
では、*[]byte
型の引数にスキャンする際、データベースドライバの内部バッファへの参照を直接[]byte
スライスに割り当てていました。これにより、パフォーマンスは向上しますが、内部バッファの再利用や解放によって、スキャンされた[]byte
スライスの内容が予期せず変更される可能性がありました。
このコミットでは、Rows.Scan
に以下のロジックが追加されました。
for _, dp := range dest {
b, ok := dp.(*[]byte) // 引数が *[]byte 型であるかチェック
if !ok {
continue // *[]byte 型でなければスキップ
}
if _, ok = dp.(*RawBytes); ok { // 引数が *RawBytes 型であればスキップ(コピーしない)
continue
}
clone := make([]byte, len(*b)) // 新しいバイトスライスを割り当て
copy(clone, *b) // データをコピー
*b = clone // コピーしたスライスを元の変数に割り当て
}
このコードは、Scan
メソッドに渡された各引数についてループし、以下の処理を行います。
- 引数が
*[]byte
型であるかどうかを確認します。 - もし
*[]byte
型であれば、さらにその引数が新しく導入された*RawBytes
型であるかどうかを確認します。 *RawBytes
型でない*[]byte
型の場合(つまり、通常の[]byte
へのポインタの場合)、make
とcopy
を使って、スキャンされたデータのディープコピーを作成します。- 作成されたディープコピーを、元の
[]byte
変数に割り当て直します。
これにより、デフォルトで[]byte
にスキャンされたデータは、呼び出し元が完全に所有する独立したコピーとなり、Rows.Next()
の呼び出しやRows
のクローズによって影響を受けることがなくなります。
RawBytes
型の導入
パフォーマンスが非常に重要で、かつユーザーがデータのライフサイクルを厳密に管理できる場合に、コピーのオーバーヘッドを避けるためのメカニズムとして、新しい型RawBytes
が導入されました。
type RawBytes []byte
// RawBytes is a byte slice that holds a reference to memory owned by
// the database itself. After a Scan into a RawBytes, the slice is only
// valid until the next call to Next, Scan, or Close.
RawBytes
は単なる[]byte
のエイリアスですが、Rows.Scan
の内部ロジックで特別に扱われます。Rows.Scan
は、引数が*RawBytes
型である場合、上記のコピーロジックをスキップします。これにより、RawBytes
を使用する開発者は、内部バッファへの参照を直接受け取ることができ、コピーのコストを回避できます。ただし、その代償として、RawBytes
スライスの内容は、次のNext
、Scan
、またはClose
の呼び出しまでしか有効でないという制約を負うことになります。
Row.Scan
の制約
Row.Scan
メソッド(単一の行をスキャンするためのショートカット)には、RawBytes
を使用できないという制約が追加されました。
if _, ok := dp.(*RawBytes); ok {
return errors.New("sql: RawBytes isn't allowed on Row.Scan")
}
これは、Row.Scan
が内部的にQuery
とNext
を呼び出し、すぐにRows
をクローズする可能性があるためです。この場合、RawBytes
が参照する内部バッファはすぐに無効になる可能性が高く、RawBytes
を使用すると非常に危険な状況を生み出すため、明示的に禁止されています。Row.Scan
を使用する場合は、常にデータのコピーが作成される通常の[]byte
を使用する必要があります。
コアとなるコードの変更箇所
src/pkg/exp/sql/sql.go
--- a/src/pkg/exp/sql/sql.go
+++ b/src/pkg/exp/sql/sql.go
@@ -30,6 +30,11 @@ func Register(name string, driver driver.Driver) {
drivers[name] = driver
}
+// RawBytes is a byte slice that holds a reference to memory owned by
+// the database itself. After a Scan into a RawBytes, the slice is only
+// valid until the next call to Next, Scan, or Close.
+type RawBytes []byte
+
// NullableString represents a string that may be null.
// NullableString implements the ScannerInto interface so
// it can be used as a scan destination:
@@ -760,9 +765,13 @@ func (rs *Rows) Columns() ([]string, error) {
}
// Scan copies the columns in the current row into the values pointed
-// at by dest. If dest contains pointers to []byte, the slices should
-// not be modified and should only be considered valid until the next
-// call to Next or Scan.
+// at by dest.
+//
+// If an argument has type *[]byte, Scan saves in that argument a copy
+// of the corresponding data. The copy is owned by the caller and can
+// be modified and held indefinitely. The copy can be avoided by using
+// an argument of type *RawBytes instead; see the documentation for
+// RawBytes for restrictions on its use.
func (rs *Rows) Scan(dest ...interface{}) error {
if rs.closed {
return errors.New("sql: Rows closed")
@@ -782,6 +791,18 @@ 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 _, ok = dp.(*RawBytes); ok {
+ continue
+ }
+ clone := make([]byte, len(*b))
+ copy(clone, *b)
+ *b = clone
+ }
return nil
}
@@ -838,6 +859,9 @@ func (r *Row) Scan(dest ...interface{}) error {
// they were obtained from the network anyway) But for now we
// don't care.
for _, dp := range dest {
+ if _, ok := dp.(*RawBytes); ok {
+ return errors.New("sql: RawBytes isn't allowed on Row.Scan")
+ }
b, ok := dp.(*[]byte)
if !ok {
continue
src/pkg/exp/sql/sql_test.go
--- a/src/pkg/exp/sql/sql_test.go
+++ b/src/pkg/exp/sql/sql_test.go
@@ -76,7 +76,7 @@ func TestQuery(t *testing.T) {
{age: 3, name: "Chris"},
}
if !reflect.DeepEqual(got, want) {
- t.Logf(" got: %#v\nwant: %#v", got, want)
+ t.Errorf("mismatch.\n got: %#v\nwant: %#v", got, want)
}
// And verify that the final rows.Next() call, which hit EOF,
@@ -86,6 +86,43 @@ func TestQuery(t *testing.T) {
}
}
+func TestByteOwnership(t *testing.T) {
+ db := newTestDB(t, "people")
+ defer closeDB(t, db)
+ rows, err := db.Query("SELECT|people|name,photo|")
+ if err != nil {
+ t.Fatalf("Query: %v", err)
+ }
+ type row struct {
+ name []byte
+ photo RawBytes
+ }
+ got := []row{}
+ for rows.Next() {
+ var r row
+ err = rows.Scan(&r.name, &r.photo)
+ if err != nil {
+ t.Fatalf("Scan: %v", err)
+ }
+ got = append(got, r)
+ }
+ corruptMemory := []byte("\xffPHOTO")
+ want := []row{
+ {name: []byte("Alice"), photo: corruptMemory},
+ {name: []byte("Bob"), photo: corruptMemory},
+ {name: []byte("Chris"), photo: corruptMemory},
+ }
+ if !reflect.DeepEqual(got, want) {
+ t.Errorf("mismatch.\n got: %#v\nwant: %#v", got, want)
+ }
+
+ var photo RawBytes
+ err = db.QueryRow("SELECT|people|photo|name=?\", \"Alice\").Scan(&photo)
+ if err == nil {
+ t.Error("want error scanning into RawBytes from QueryRow")
+ }
+}
+
func TestRowsColumns(t *testing.T) {
db := newTestDB(t, "people")
defer closeDB(t, db)
@@ -300,6 +337,6 @@ func TestQueryRowClosingStmt(t *testing.T) {
}
fakeConn := db.freeConn[0].(*fakeConn)
if made, closed := fakeConn.stmtsMade, fakeConn.stmtsClosed; made != closed {
- t.Logf("statement close mismatch: made %d, closed %d", made, closed)
+ t.Errorf("statement close mismatch: made %d, closed %d", made, closed)
}
}
コアとなるコードの解説
sql.go
の変更点
-
RawBytes
型の定義:type RawBytes []byte
この新しい型は、データベースドライバの内部バッファへの直接参照を保持する[]byte
スライスであることを明示するために導入されました。これにより、開発者はパフォーマンスと安全性のトレードオフを意識的に選択できるようになります。 -
Rows.Scan
メソッドのドキュメント更新:// If an argument has type *[]byte, Scan saves in that argument a copy
// of the corresponding data. The copy is owned by the caller and can
// be modified and held indefinitely. The copy can be avoided by using
// an argument of type *RawBytes instead; see the documentation for
// RawBytes for restrictions on its use.
このドキュメントの更新は非常に重要です。[]byte
へのスキャンがデフォルトでコピーを作成するようになったこと、そしてRawBytes
を使用することでコピーを回避できるが、その際にはライフサイクルの制約があることを明確に説明しています。 -
Rows.Scan
メソッドのコピーロジック追加:for _, dp := range dest { b, ok := dp.(*[]byte) if !ok { continue } if _, ok = dp.(*RawBytes); ok { continue } clone := make([]byte, len(*b)) copy(clone, *b) *b = clone }
これがこのコミットの最も重要な機能変更です。
Scan
の引数が通常の*[]byte
である場合、内部でディープコピーを作成し、そのコピーをユーザーの変数に割り当て直します。これにより、ユーザーはスキャンされた[]byte
データを安全に保持し、後で利用できるようになります。*RawBytes
の場合はこのコピー処理をスキップし、内部バッファへの参照をそのまま渡します。 -
Row.Scan
メソッドでのRawBytes
の使用禁止:if _, ok := dp.(*RawBytes); ok { return errors.New("sql: RawBytes isn't allowed on Row.Scan") }
Row.Scan
は単一の行を処理するためのものであり、内部的にRows
オブジェクトをすぐにクローズするため、RawBytes
が参照する内部バッファが即座に無効になる可能性が高いです。このため、安全上の理由からRow.Scan
でのRawBytes
の使用は禁止されました。
sql_test.go
の変更点
-
TestQuery
の修正:t.Logf
からt.Errorf
への変更は、テストの失敗をより明確に報告するための一般的な改善です。 -
TestByteOwnership
の追加: この新しいテストは、[]byte
とRawBytes
の所有権の違いを実証するために非常に重要です。name []byte
とphoto RawBytes
を持つrow
構造体を定義し、データベースからデータをスキャンします。name
フィールド(通常の[]byte
)がディープコピーされているため、外部のメモリ変更(corruptMemory
)の影響を受けないことを検証します。photo
フィールド(RawBytes
)が内部バッファへの参照を保持しているため、そのライフサイクルがRows.Next()
などに依存することを示唆します(このテストでは直接的なメモリ破壊は行っていませんが、概念的な所有権の違いを強調しています)。QueryRow().Scan(&photo)
がRawBytes
に対してエラーを返すことを検証し、Row.Scan
でのRawBytes
の使用禁止が正しく機能していることを確認します。
これらの変更により、database/sql
パッケージは、[]byte
型へのスキャンにおいて、デフォルトでより安全な挙動を提供するようになりました。開発者は、パフォーマンスが最優先される場合にのみ、RawBytes
を明示的に使用するという選択肢を持つことになります。
関連リンク
- Go Issue #2698:
database/sql
:Scan
into[]byte
should copy by default - Go CL 5539060:
exp/sql
: copy when scanning into[]byte
by default
参考にした情報源リンク
- Go
database/sql
パッケージのドキュメント (現在のバージョン): - Go
database/sql
パッケージのRows.Scan
メソッドに関する情報: - Go
database/sql
パッケージのRawBytes
型に関する情報: - Go言語におけるスライスとメモリ管理に関する一般的な情報。
- Go言語のポインタに関する一般的な情報。
- Go言語のテストに関する一般的な情報。