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

[インデックス 11655] ファイルの概要

このコミットは、Go言語の標準ライブラリであるdatabase/sqlパッケージにおいて、データベースから取得した値をinterface{}型にスキャン(変換)する機能を追加するものです。これにより、開発者は特定の型を事前に指定することなく、汎用的にデータベースの値を扱うことが可能になります。

コミット

  • コミットハッシュ: 9c060b8d60f14d930e5eadd7c9968ee2ba4f4131
  • Author: Brad Fitzpatrick bradfitz@golang.org
  • Date: Mon Feb 6 10:06:22 2012 -0800
  • コミットメッセージ:
    database/sql: permit scanning into interface{}
    
    See thread http://goo.gl/7zzzU for background.
    
    R=rsc
    CC=golang-dev
    https://golang.org/cl/5624051
    

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/9c060b8d60f14d930e5eadd7c9968ee2ba4f4131

元コミット内容

database/sql: permit scanning into interface{}

See thread http://goo.gl/7zzzU for background.

R=rsc
CC=golang-dev
https://golang.org/cl/5624051

変更の背景

この変更の背景には、database/sqlパッケージのRows.Scanメソッドが、データベースから取得した値をGoの変数に変換する際に、より柔軟な型指定を求めるニーズがありました。以前は、Scanメソッドの引数には具体的な型へのポインタ(例: *string, *int64)を渡す必要がありましたが、これにより、取得するデータの型が実行時まで不明な場合や、汎用的なデータ処理を行う場合に不便が生じていました。

コミットメッセージにあるhttp://goo.gl/7zzzUのリンクは、この変更に関する議論が行われたスレッドへの参照ですが、私は外部のウェブサイトに直接アクセスしてその内容を読み取ることはできません。しかし、コミットメッセージとコードの変更内容から、この変更がScanメソッドの引数として*interface{}を受け入れるようにすることで、この柔軟性の問題を解決しようとしていることは明らかです。これにより、データベースの値を任意の型として受け取り、後で型アサーションなどを用いて適切な型に変換するといった、より動的な処理が可能になります。

前提知識の解説

Go言語のdatabase/sqlパッケージ

database/sqlパッケージは、Go言語でSQLデータベースを操作するための汎用的なインターフェースを提供します。このパッケージ自体は特定のデータベースドライバーを含まず、データベースドライバーは別途実装され、このパッケージのインターフェースに準拠することで利用可能になります。

主要な概念:

  • DB: データベースへの接続プールを表します。
  • Rows: クエリの結果セットを表します。
  • Row: 単一の行の結果セットを表します。
  • Scanメソッド: RowsまたはRowオブジェクトのメソッドで、データベースから取得した値をGoの変数に変換(スキャン)するために使用されます。引数には、スキャン先の変数のポインタを渡します。

interface{}

Go言語におけるinterface{}(空インターフェース)は、任意の型の値を保持できる特別な型です。これは、他の言語におけるObject型やAny型に似ています。interface{}型は、コンパイル時には型が特定できないが、実行時に様々な型の値を扱う必要がある場合に非常に有用です。

型変換と型アサーション

Goでは、異なる型間で値を変換する際には明示的な型変換が必要です。また、interface{}型に格納された値の元の型を取り出すには、型アサーション(value.(Type))を使用します。

例:

var i interface{}
i = "hello"
s := i.(string) // 型アサーション

技術的詳細

このコミットは、主にdatabase/sqlパッケージ内の以下の3つのファイルに影響を与えています。

  1. src/pkg/database/sql/convert.go: このファイルは、データベースから読み取られた値をGoの特定の型に変換するロジックを扱います。今回の変更では、convertAssign関数に*interface{}型へのスキャンを許可するケースが追加されました。

    • []byte型のソース値を*interface{}にスキャンする場合、元のバイトスライスをコピーしてinterface{}に格納します。これは、元のバイトスライスがデータベースドライバーによって再利用される可能性があるため、データの安全性を確保するための防御的コピーです。
    • その他の型のソース値を*interface{}にスキャンする場合、ソース値をそのままinterface{}に格納します。
  2. src/pkg/database/sql/convert_test.go: convert.goの変更を検証するためのテストが追加されました。

    • conversionTest構造体にwantiface interface{}フィールドが追加され、interface{}型へのスキャン結果を期待値として設定できるようになりました。
    • scaniface interface{}というグローバル変数が追加され、*interface{}型へのスキャンターゲットとして使用されます。
    • conversionTestsスライスに、float64, int64, string, []byte, bool, nilといった様々な型の値を*interface{}にスキャンするテストケースが追加されました。
    • TestConversions関数内で、*interface{}型へのスキャン結果が期待値と一致するかどうかをreflect.DeepEqualを用いて検証するロジックが追加されました。
    • 特に[]byte*interface{}にスキャンした場合に、元のバイトスライスがコピーされていることを確認するテストが追加されています。これは、[]byteが参照型であるため、コピーが行われないと元のデータが変更された場合にスキャン結果も意図せず変更されてしまう可能性があるためです。
  3. src/pkg/database/sql/sql.go: Rows.Scanメソッドのドキュメンテーションが更新されました。

    • *interface{}型へのスキャンに関する新しい説明が追加されました。これにより、*interface{}にスキャンする場合、基になるドライバーが提供する値が変換なしでコピーされること、そして値が[]byte型の場合はコピーが作成され、呼び出し元がその結果を所有することが明記されました。

コアとなるコードの変更箇所

src/pkg/database/sql/convert.go

--- a/src/pkg/database/sql/convert.go
+++ b/src/pkg/database/sql/convert.go
@@ -49,6 +49,11 @@ func convertAssign(dest, src interface{}) error {
 		case *string:
 			*d = string(s)
 			return nil
+		case *interface{}:
+			bcopy := make([]byte, len(s))
+			copy(bcopy, s)
+			*d = bcopy
+			return nil
 		case *[]byte:
 			*d = s
 			return nil
@@ -80,6 +85,9 @@ func convertAssign(dest, src interface{}) error {
 			*d = bv.(bool)
 		}
 		return err
+	case *interface{}:
+		*d = src
+		return nil
 	}
 
 	if scanner, ok := dest.(ScannerInto); ok {

src/pkg/database/sql/convert_test.go

--- a/src/pkg/database/sql/convert_test.go
+++ b/src/pkg/database/sql/convert_test.go
@@ -18,14 +18,15 @@ type conversionTest struct {
 	s, d interface{} // source and destination
 
 	// following are used if they're non-zero
-	wantint  int64
-	wantuint uint64
-	wantstr  string
-	wantf32  float32
-	wantf64  float64
-	wanttime time.Time
-	wantbool bool // used if d is of type *bool
-	wanterr  string
+	wantint   int64
+	wantuint  uint64
+	wantstr   string
+	wantf32   float32
+	wantf64   float64
+	wanttime  time.Time
+	wantbool  bool // used if d is of type *bool
+	wanterr   string
+	wantiface interface{}
 }
 
 // Target variables for scanning into.
@@ -41,6 +42,7 @@ var (
 	scanf32    float32
 	scanf64    float64
 	scantime   time.Time
+	scaniface  interface{}
 )
 
 var conversionTests = []conversionTest{
@@ -95,6 +97,14 @@ var conversionTests = []conversionTest{
 	{s: float64(1.5), d: &scanf32, wantf32: float32(1.5)},
 	{s: "1.5", d: &scanf32, wantf32: float32(1.5)},
 	{s: "1.5", d: &scanf64, wantf64: float64(1.5)},
+
+	// To interface{}
+	{s: float64(1.5), d: &scaniface, wantiface: float64(1.5)},
+	{s: int64(1), d: &scaniface, wantiface: int64(1)},
+	{s: "str", d: &scaniface, wantiface: "str"},
+	{s: []byte("byteslice"), d: &scaniface, wantiface: []byte("byteslice")},
+	{s: true, d: &scaniface, wantiface: true},
+	{s: nil, d: &scaniface},
 }
 
 func intValue(intptr interface{}) int64 {
@@ -152,6 +162,18 @@ func TestConversions(t *testing.T) {
 		if !ct.wanttime.IsZero() && !ct.wanttime.Equal(timeValue(ct.d)) {
 			errf("want time %v, got %v", ct.wanttime, timeValue(ct.d))
 		}
+		if ifptr, ok := ct.d.(*interface{}); ok {
+			if !reflect.DeepEqual(ct.wantiface, scaniface) {
+				errf("want interface %#v, got %#v", ct.wantiface, scaniface)
+				continue
+			}
+			if srcBytes, ok := ct.s.([]byte); ok {
+				dstBytes := (*ifptr).([]byte)
+				if &dstBytes[0] == &srcBytes[0] {
+					errf("copy into interface{} didn't copy []byte data")
+				}
+			}
+		}
 	}
 }

src/pkg/database/sql/sql.go

--- a/src/pkg/database/sql/sql.go
+++ b/src/pkg/database/sql/sql.go
@@ -880,6 +880,10 @@ func (rs *Rows) Columns() ([]string, error) {
 // 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.
+//
+// If an argument has type *interface{}, Scan copies the value
+// provided by the underlying driver without conversion. If the value
+// is of type []byte, a copy is made and the caller owns the result.
 func (rs *Rows) Scan(dest ...interface{}) error {
 	if rs.closed {
 		return errors.New("sql: Rows closed")

コアとなるコードの解説

src/pkg/database/sql/convert.goの変更

convertAssign関数は、データベースから読み取ったsrc(ソース)値を、Goの変数dest(デスティネーション)に変換して割り当てる役割を担っています。

  • case *interface{}: (バイトスライスからの変換)

    case *interface{}:
        bcopy := make([]byte, len(s))
        copy(bcopy, s)
        *d = bcopy
        return nil
    

    このブロックは、ソース値s[]byte型であり、かつデスティネーションd*interface{}型である場合に実行されます。 database/sqlパッケージでは、データベースドライバーが提供する[]byteは、内部バッファへの参照である可能性があり、そのバッファは次の行の読み取りや他の操作によって上書きされる可能性があります。そのため、[]byteinterface{}にスキャンする際には、データの整合性を保つために防御的コピーが行われます。make([]byte, len(s))で新しいバイトスライスを作成し、copy(bcopy, s)でソースの内容をコピーしています。これにより、呼び出し元はコピーされたバイトスライスを安全に所有し、元のドライバーのバッファの変更に影響されなくなります。

  • case *interface{}: (その他の型からの変換)

    case *interface{}:
        *d = src
        return nil
    

    このブロックは、ソース値src[]byte型以外であり、かつデスティネーションd*interface{}型である場合に実行されます。 この場合、srcの値はそのまま*dinterface{}型のポインタが指す実体)に代入されます。[]byte型のように参照のセマンティクスを持つ型ではないため、防御的コピーは不要です。これにより、データベースドライバーが提供する元の値が、型変換なしでinterface{}に格納されます。

src/pkg/database/sql/convert_test.goの変更

テストコードの変更は、convert.goで追加された*interface{}へのスキャン機能が正しく動作することを検証するためのものです。

  • conversionTest構造体へのwantifaceフィールドの追加: これにより、interface{}型へのスキャンが期待する結果をテストケースで指定できるようになりました。

  • scaniface変数の追加: *interface{}型へのスキャンターゲットとして使用されるグローバル変数です。

  • conversionTestsへの新しいテストケースの追加: float64, int64, string, []byte, bool, nilといった様々なGoの基本型が、*interface{}にスキャンされたときに正しく値が格納されることを確認します。

  • TestConversions関数内の検証ロジックの追加:

    if ifptr, ok := ct.d.(*interface{}); ok {
        if !reflect.DeepEqual(ct.wantiface, scaniface) {
            errf("want interface %#v, got %#v", ct.wantiface, scaniface)
            continue
        }
        if srcBytes, ok := ct.s.([]byte); ok {
            dstBytes := (*ifptr).([]byte)
            if &dstBytes[0] == &srcBytes[0] {
                errf("copy into interface{} didn't copy []byte data")
            }
        }
    }
    

    この部分では、スキャンターゲットが*interface{}型である場合に、以下の2つの重要な検証を行います。

    1. reflect.DeepEqualを使用して、スキャンされたscanifaceの値が、テストケースで定義された期待値ct.wantifaceと完全に一致するかどうかを確認します。これは、interface{}に格納された値の型と値の両方が正しいことを保証します。
    2. ソース値が[]byte型であった場合、スキャンされたinterface{}内の[]byteが、元のソースの[]byteとは異なるメモリ領域にコピーされていることを確認します。&dstBytes[0] == &srcBytes[0]という比較は、スライスの基底配列の先頭アドレスが同じかどうかをチェックすることで、コピーが行われたかどうかを判断しています。これにより、前述の防御的コピーが正しく機能していることを保証します。

src/pkg/database/sql/sql.goの変更

Rows.Scanメソッドのコメントが更新され、*interface{}型へのスキャンに関する挙動が明示されました。

  • 新しいコメントの追加:
    // If an argument has type *interface{}, Scan copies the value
    // provided by the underlying driver without conversion. If the value
    // is of type []byte, a copy is made and the caller owns the result.
    
    このコメントは、Scanメソッドが*interface{}型の引数を受け取った場合の動作を明確にしています。
    • 「基になるドライバーが提供する値を変換なしでコピーする」:これは、interface{}にスキャンする場合、database/sqlパッケージが追加の型変換ロジックを適用せず、ドライバーが提供する生のGoの型(例: int64, string, float64など)をそのままinterface{}に格納することを意味します。
    • 「値が[]byte型の場合、コピーが作成され、呼び出し元がその結果を所有する」:これは、convert.goで実装された防御的コピーの挙動を説明しており、[]byteがスキャンされた場合に、呼び出し元が安全にそのデータを扱えることを保証します。

これらの変更により、database/sqlパッケージは、より柔軟なデータスキャン機能を提供し、特に動的な型を持つデータを扱うアプリケーションにおいて、開発の利便性と堅牢性を向上させました。

関連リンク

  • Go CL (Code Review) へのリンク: https://golang.org/cl/5624051

参考にした情報源リンク

コミットメッセージに記載されているhttp://goo.gl/7zzzUのリンクは、この変更の背景に関する議論スレッドへの参照ですが、私は外部のウェブサイトに直接アクセスしてその内容を読み取ることはできませんでした。