Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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言語およびデータベース関連の概念を理解しておく必要があります。

  1. database/sqlパッケージ (旧 exp/sql): Go言語の標準ライブラリの一部であり、SQLデータベースとのインタラクションのための汎用的なインターフェースを提供します。このパッケージ自体は特定のデータベースドライバを含まず、ドライバは別途実装され、このインターフェースに準拠します。
  2. Rows.Scan()メソッド: database/sqlパッケージにおいて、クエリ結果セットの現在の行のデータをGoの変数に読み込むために使用されるメソッドです。引数として、スキャンするカラムの数に対応するポインタのリストを受け取ります。
  3. []byte: Go言語におけるバイトスライスです。可変長で、バイナリデータやUTF-8エンコードされた文字列などを扱うのに一般的に使用されます。
  4. ポインタ (*T): Goにおいて、変数のメモリアドレスを指し示す型です。Scanメソッドは、値を直接受け取るのではなく、ポインタを介して変数の内容を更新します。
  5. データの所有権とライフサイクル: プログラミングにおいて、データがどのメモリ領域に存在し、いつそのメモリが有効であるか、誰がそのメモリを解放する責任を持つか、という概念です。特に、共有される可能性のあるデータや、内部バッファから提供されるデータの場合に重要になります。
  6. ディープコピーとシャローコピー:
    • シャローコピー: データの参照(ポインタ)のみをコピーし、実際のデータは元の場所を共有します。元のデータが変更されると、コピーされたデータも影響を受けます。
    • ディープコピー: データのすべての内容を新しいメモリ領域にコピーします。元のデータとコピーされたデータは完全に独立しており、一方の変更がもう一方に影響を与えることはありません。
  7. 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メソッドに渡された各引数についてループし、以下の処理を行います。

  1. 引数が*[]byte型であるかどうかを確認します。
  2. もし*[]byte型であれば、さらにその引数が新しく導入された*RawBytes型であるかどうかを確認します。
  3. *RawBytes型でない*[]byte型の場合(つまり、通常の[]byteへのポインタの場合)、makecopyを使って、スキャンされたデータのディープコピーを作成します。
  4. 作成されたディープコピーを、元の[]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スライスの内容は、次のNextScan、またはCloseの呼び出しまでしか有効でないという制約を負うことになります。

Row.Scanの制約

Row.Scanメソッド(単一の行をスキャンするためのショートカット)には、RawBytesを使用できないという制約が追加されました。

if _, ok := dp.(*RawBytes); ok {
    return errors.New("sql: RawBytes isn't allowed on Row.Scan")
}

これは、Row.Scanが内部的にQueryNextを呼び出し、すぐに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の変更点

  1. RawBytes型の定義: type RawBytes []byte この新しい型は、データベースドライバの内部バッファへの直接参照を保持する[]byteスライスであることを明示するために導入されました。これにより、開発者はパフォーマンスと安全性のトレードオフを意識的に選択できるようになります。

  2. 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を使用することでコピーを回避できるが、その際にはライフサイクルの制約があることを明確に説明しています。

  3. 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の場合はこのコピー処理をスキップし、内部バッファへの参照をそのまま渡します。

  4. 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の変更点

  1. TestQueryの修正: t.Logfからt.Errorfへの変更は、テストの失敗をより明確に報告するための一般的な改善です。

  2. TestByteOwnershipの追加: この新しいテストは、[]byteRawBytesの所有権の違いを実証するために非常に重要です。

    • name []bytephoto RawBytesを持つrow構造体を定義し、データベースからデータをスキャンします。
    • nameフィールド(通常の[]byte)がディープコピーされているため、外部のメモリ変更(corruptMemory)の影響を受けないことを検証します。
    • photoフィールド(RawBytes)が内部バッファへの参照を保持しているため、そのライフサイクルがRows.Next()などに依存することを示唆します(このテストでは直接的なメモリ破壊は行っていませんが、概念的な所有権の違いを強調しています)。
    • QueryRow().Scan(&photo)RawBytesに対してエラーを返すことを検証し、Row.ScanでのRawBytesの使用禁止が正しく機能していることを確認します。

これらの変更により、database/sqlパッケージは、[]byte型へのスキャンにおいて、デフォルトでより安全な挙動を提供するようになりました。開発者は、パフォーマンスが最優先される場合にのみ、RawBytesを明示的に使用するという選択肢を持つことになります。

関連リンク

参考にした情報源リンク