[インデックス 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:Scaninto[]byteshould copy by default - Go CL 5539060:
exp/sql: copy when scanning into[]byteby default
参考にした情報源リンク
- Go
database/sqlパッケージのドキュメント (現在のバージョン): - Go
database/sqlパッケージのRows.Scanメソッドに関する情報: - Go
database/sqlパッケージのRawBytes型に関する情報: - Go言語におけるスライスとメモリ管理に関する一般的な情報。
- Go言語のポインタに関する一般的な情報。
- Go言語のテストに関する一般的な情報。