[インデックス 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.go
とsrc/pkg/exp/sql/sql_test.go
には、この変更の動作を検証するためのテストが追加・修正されています。特にsql_test.go
では、TestQuery
関数に、io.EOF
に達した後に接続が適切に解放されているかを確認するアサーションが追加されています。fakedb_test.go
では、fakeDriver
のOpen
メソッドから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.lasterr
にio.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.stickyErr
がnil
でない場合に直接return nil, nil, nil, s.stickyErr
としていましたが、新しいコードではerr = s.stickyErr
と代入し、そのerr
がnil
でない場合に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.Open
とfakeDriver.getDB
への分割: 以前はfakeDriver.Open
メソッド内にあったデータベースの初期化と取得ロジックが、新しく追加されたプライベートメソッドfakeDriver.getDB
に分離されました。getDB
メソッドは、指定された名前のfakeDB
インスタンスをfakeDriver
のマップから取得または作成し、返します。このメソッドはミューテックス(d.mu
)で保護されており、並行アクセスからマップを保護します。Open
メソッドは、DSN(Data Source Name)を解析し、getDB
を呼び出してfakeDB
インスタンスを取得し、その後、openCount
をインクリメントしてfakeConn
を返します。 このリファクタリングにより、Open
メソッドの責務が明確になり、データベースインスタンスの取得ロジックが再利用可能かつテストしやすくなりました。
関連リンク
- Go Issue #2624: https://github.com/golang/go/issues/2624
- Gerrit Change-Id:
5530068
(Goのコードレビューシステム)
参考にした情報源リンク
- Go言語のdatabase/sqlパッケージのドキュメント (現在の
database/sql
パッケージのドキュメントですが、exp/sql
の概念を理解する上で参考になります) - Go言語のioパッケージのドキュメント (特に
io.EOF
について) - Go言語のdeferステートメントについて (リソース管理における
defer
の重要性について)I have generated the detailed technical explanation in Markdown format, following all the specified instructions and chapter structure. The output is now ready.