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

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

このコミットは、Go言語の実験的なexp/sqlパッケージにおいて、データベースの行(Rows)を走査する際に、データが終端(EOF: End Of File)に達した時点で自動的にリソースをクローズするように修正するものです。これにより、データベース接続や関連リソースのリークを防ぎ、より堅牢なデータベース操作を実現します。

コミット

commit 4435c8bf2a7d4fcc33fd15903487958590a157f9
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Tue Jan 10 12:51:27 2012 -0800

    exp/sql: close Rows on EOF
    
    Fixes #2624
    
    R=rsc
    CC=golang-dev
    https://golang.org/cl/5530068

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

https://github.com/golang/go/commit/4435c8bf2a7d4fcc33fd15903487958590a157f9

元コミット内容

exp/sql: close Rows on EOF

Fixes #2624

変更の背景

Go言語のexp/sqlパッケージ(後のdatabase/sqlパッケージの原型)では、データベースから取得した結果セット(Rowsオブジェクト)をイテレートする際に、すべての行を読み終えた後でも、明示的にRows.Close()を呼び出す必要がありました。この明示的なクローズを忘れると、データベース接続が解放されず、リソースリークや接続プールの枯渇といった問題を引き起こす可能性がありました。

このコミットは、GitHub Issue #2624で報告された問題を解決するために行われました。この問題は、Rows.Next()メソッドがこれ以上行がないことを示すio.EOFエラーを返した際に、Rowsオブジェクトが自動的にクローズされないという挙動に関するものでした。ユーザーがすべての行を読み終えたことを検出した時点で、自動的にリソースが解放されるべきであるという考えに基づき、この修正が導入されました。

前提知識の解説

  • exp/sqlパッケージ: Go言語の標準ライブラリdatabase/sqlパッケージの初期の実験的なバージョンです。データベース操作のための汎用的なインターフェースを提供します。
  • Rowsオブジェクト: database/sqlパッケージにおいて、SQLクエリの結果セットを表すオブジェクトです。データベースから取得した行データを1行ずつ読み出すために使用されます。
  • Rows.Next()メソッド: Rowsオブジェクトのメソッドで、結果セットの次の行に移動し、その行を読み込む準備をします。次の行がない場合、falseを返し、内部的にio.EOFなどのエラーを記録します。
  • io.EOF: Go言語のioパッケージで定義されているエラー変数で、入力の終端(End Of File)に達したことを示します。ストリームやファイルからデータを読み込む際に、これ以上データがない場合に返されます。
  • リソース管理: プログラミングにおいて、ファイルハンドル、ネットワーク接続、データベース接続などのシステムリソースを適切に取得し、使用し、そして解放するプロセスを指します。リソースの解放を怠ると、メモリリークやシステムパフォーマンスの低下、リソース枯渇などの問題が発生します。データベース接続のようなリソースは特に有限であり、適切に管理されないとアプリケーション全体の安定性に影響を与えます。
  • deferステートメント: Go言語のキーワードで、関数がリターンする直前に実行される関数呼び出しをスケジュールします。リソースのクリーンアップ処理(例: file.Close(), rows.Close()) を確実に行うためによく使用されます。しかし、このコミットの背景にある問題は、defer rows.Close()を記述しても、Next()io.EOFを返した時点で即座にクローズされないという点にありました。

技術的詳細

このコミットの主要な変更は、src/pkg/exp/sql/sql.goファイルのRows.Next()メソッドにあります。以前のNext()メソッドは、次の行がない場合にfalseを返し、内部的にエラー(io.EOFを含む)をセットするだけでした。しかし、この変更により、Next()io.EOFを検出した場合、その場でRows.Close()メソッドを呼び出すようになりました。

これにより、ユーザーがfor rows.Next() { ... }のようなループで結果セットを処理し、すべての行を読み終えてループが終了した時点で、明示的にrows.Close()を呼び出す必要がなくなります。Next()メソッドがio.EOFを返した瞬間に、内部的にリソースが解放されるため、リソースリークのリスクが低減されます。

また、src/pkg/exp/sql/fakedb_test.gosrc/pkg/exp/sql/sql_test.goには、この変更の動作を検証するためのテストが追加・修正されています。特にsql_test.goでは、TestQuery関数に、io.EOFに達した後に接続が適切に解放されているかを確認するアサーションが追加されています。fakedb_test.goでは、fakeDriverOpenメソッドからgetDBメソッドへのロジックの分離が行われ、テストの構造が改善されています。これは直接的な機能変更ではありませんが、テストの準備とクリーンアップのロジックをより明確にするためのリファクタリングです。

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

src/pkg/exp/sql/sql.go

--- a/src/pkg/exp/sql/sql.go
+++ b/src/pkg/exp/sql/sql.go
@@ -549,8 +549,8 @@ func (s *Stmt) Exec(args ...interface{}) (Result, error) {
 // statement, a function to call to release the connection, and a
 // statement bound to that connection.
 func (s *Stmt) connStmt() (ci driver.Conn, releaseConn func(), si driver.Stmt, err error) {
-	if s.stickyErr != nil {
-		return nil, nil, nil, s.stickyErr
+	if err = s.stickyErr; err != nil {
+		return
 	}
 	s.mu.Lock()
 	if s.closed {
@@ -726,6 +726,9 @@ func (rs *Rows) Next() bool {
 	rs.lastcols = make([]interface{}, len(rs.rowsi.Columns()))
 	}
 	rs.lasterr = rs.rowsi.Next(rs.lastcols)
+	if rs.lasterr == io.EOF {
+		rs.Close()
+	}
 	return rs.lasterr == nil
 }

src/pkg/exp/sql/sql_test.go

--- a/src/pkg/exp/sql/sql_test.go
+++ b/src/pkg/exp/sql/sql_test.go
@@ -10,8 +10,10 @@ import (
 	"testing"
 )
 
+const fakeDBName = "foo"
+
 func newTestDB(t *testing.T, name string) *DB {
-	db, err := Open("test", "foo")
+	db, err := Open("test", fakeDBName)
 	if err != nil {
 		t.Fatalf("Open: %v", err)
 	}
@@ -73,6 +75,12 @@ func TestQuery(t *testing.T) {
 	if !reflect.DeepEqual(got, want) {
 		t.Logf(" got: %#v\nwant: %#v", got, want)
 	}
+
+	// And verify that the final rows.Next() call, which hit EOF,
+	// also closed the rows connection.
+	if n := len(db.freeConn); n != 1 {
+		t.Errorf("free conns after query hitting EOF = %d; want 1", n)
+	}
 }
 
 func TestRowsColumns(t *testing.T) {

src/pkg/exp/sql/fakedb_test.go

--- a/src/pkg/exp/sql/fakedb_test.go
+++ b/src/pkg/exp/sql/fakedb_test.go
@@ -110,25 +110,34 @@ func init() {
 
 // Supports dsn forms:
 //    <dbname>
-//    <dbname>;wipe
+//    <dbname>;<opts>  (no currently supported options)
 func (d *fakeDriver) Open(dsn string) (driver.Conn, error) {
--	d.mu.Lock()
--	defer d.mu.Unlock()
--	d.openCount++
--	if d.dbs == nil {
--		d.dbs = make(map[string]*fakeDB)
--	}
 	parts := strings.Split(dsn, ";")
 	if len(parts) < 1 {
 		return nil, errors.New("fakedb: no database name")
 	}
 	name := parts[0]
+
+	db := d.getDB(name)
+
+	d.mu.Lock()
+	d.openCount++
+	d.mu.Unlock()
+	return &fakeConn{db: db}, nil
+}
+
+func (d *fakeDriver) getDB(name string) *fakeDB {
+	d.mu.Lock()
+	defer d.mu.Unlock()
+	if d.dbs == nil {
+		d.dbs = make(map[string]*fakeDB)
+	}
 	db, ok := d.dbs[name]
 	if !ok {
 		db = &fakeDB{name: name}
 		d.dbs[name] = db
 	}
--	return &fakeConn{db: db}, nil
+	return db
 }
 
 func (db *fakeDB) wipe() {

コアとなるコードの解説

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

  • Rows.Next()メソッド内の変更:

    	rs.lasterr = rs.rowsi.Next(rs.lastcols)
    	if rs.lasterr == io.EOF {
    		rs.Close()
    	}
    	return rs.lasterr == nil
    

    この部分がこのコミットの核心です。rs.rowsi.Next(rs.lastcols)が呼び出され、次の行のデータを読み込もうとします。もし、これ以上データがない場合(つまり、結果セットの終端に達した場合)、rs.lasterrio.EOFがセットされます。 追加されたif rs.lasterr == io.EOFの条件文は、このio.EOFが検出された場合に、即座にrs.Close()を呼び出すことを保証します。これにより、結果セットのイテレーションが終了した時点で、関連するデータベース接続やステートメントなどのリソースが自動的に解放されるようになります。

  • Stmt.connStmt()メソッド内の変更:

    -	if s.stickyErr != nil {
    -		return nil, nil, nil, s.stickyErr
    +	if err = s.stickyErr; err != nil {
    +		return
    	}
    

    これは、エラーハンドリングのスタイルを改善したものです。以前はs.stickyErrnilでない場合に直接return nil, nil, nil, s.stickyErrとしていましたが、新しいコードではerr = s.stickyErrと代入し、そのerrnilでない場合にreturnしています。これは機能的な変更ではなく、Goの慣用的なエラーハンドリングパターンに合わせたものです。

src/pkg/exp/sql/sql_test.go の変更

  • newTestDB関数の変更: Open("test", "foo")Open("test", fakeDBName)に変更されました。fakeDBNameは新しく定義された定数const fakeDBName = "foo"です。これはハードコードされた文字列を定数に置き換えることで、コードの可読性と保守性を向上させるための小さなリファクタリングです。

  • TestQuery関数へのアサーション追加:

    	// And verify that the final rows.Next() call, which hit EOF,
    	// also closed the rows connection.
    	if n := len(db.freeConn); n != 1 {
    		t.Errorf("free conns after query hitting EOF = %d; want 1", n)
    	}
    

    このテストコードは、Rows.Next()io.EOFを返した後にRows.Close()が正しく呼び出され、データベース接続が解放されたことを検証します。db.freeConnは、テスト用のfakeDBが管理する解放された接続のリストであり、クエリがEOFに達した後に1つの接続が解放されていることを期待しています。これにより、今回の変更が意図通りに機能していることが保証されます。

src/pkg/exp/sql/fakedb_test.go の変更

  • fakeDriver.OpenfakeDriver.getDBへの分割: 以前はfakeDriver.Openメソッド内にあったデータベースの初期化と取得ロジックが、新しく追加されたプライベートメソッドfakeDriver.getDBに分離されました。
    • getDBメソッドは、指定された名前のfakeDBインスタンスをfakeDriverのマップから取得または作成し、返します。このメソッドはミューテックス(d.mu)で保護されており、並行アクセスからマップを保護します。
    • Openメソッドは、DSN(Data Source Name)を解析し、getDBを呼び出してfakeDBインスタンスを取得し、その後、openCountをインクリメントしてfakeConnを返します。 このリファクタリングにより、Openメソッドの責務が明確になり、データベースインスタンスの取得ロジックが再利用可能かつテストしやすくなりました。

関連リンク

参考にした情報源リンク