[インデックス 18019] ファイルの概要
このコミットは、Go言語の標準ライブラリ database/sql
パッケージにおける、プリペアドステートメント使用時の自動再接続の不具合と、それに伴う複数のコネクションリークを修正するものです。特に、データベース接続が切断された際に driver.ErrBadConn
エラーが発生した場合のハンドリングを改善し、堅牢性を高めています。
コミット
database/sql: プリペアドステートメントにおける自動再接続の修正
これにより、いくつかのコネクションリークも修正されます。 Fixes #5718
R=bradfitz, adg CC=alberto.garcia.hierro, golang-dev https://golang.org/cl/14920046
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/762a9d934eab267418595df7a220eec50919b77d
元コミット内容
commit 762a9d934eab267418595df7a220eec50919b77d
Author: Julien Schmidt <google@julienschmidt.com>
Date: Tue Dec 17 11:57:30 2013 -0800
database/sql: fix auto-reconnect in prepared statements
This also fixes several connection leaks.
Fixes #5718
R=bradfitz, adg
CC=alberto.garcia.hierro, golang-dev
https://golang.org/cl/14920046
変更の背景
このコミットの主な背景は、Goの database/sql
パッケージが、データベースとの接続が予期せず切断された際に、特にプリペアドステートメントを使用している場合に、適切に自動再接続を試みない、または再接続の試行中にコネクションリークが発生するという問題に対処するためです。
具体的には、Goの database/sql
パッケージは、データベースドライバが driver.ErrBadConn
を返した場合に、内部的に接続を再確立し、操作をリトライするメカニズムを持っています。しかし、このメカニズムがプリペアドステートメント (*sql.Stmt
) のコンテキストで十分に機能していなかったようです。プリペアドステートメントは、一度準備されると特定のデータベースコネクションに紐付けられることが多く、そのコネクションが切断された場合に、ステートメント自体が新しいコネクションで再準備される必要があります。この再準備のロジックに不備があったため、driver.ErrBadConn
が返された際に、無限ループに陥ったり、古いコネクションが適切にクローズされずにリークしたりする問題が発生していました。
GitHub Issue #5718 (golang.org/issue/5718) は、この問題の具体的な報告であり、このコミットはその問題を解決するために作成されました。コネクションリークは、アプリケーションのメモリ使用量が増加し、最終的にはデータベースへの接続が枯渇する原因となるため、非常に重要な修正です。
前提知識の解説
database/sql
パッケージ
database/sql
はGo言語の標準ライブラリで、SQLデータベースへの汎用的なインターフェースを提供します。このパッケージ自体は特定のデータベースドライバを含まず、データベース固有の操作は database/sql/driver
インターフェースを実装した外部ドライバに委譲します。
driver.ErrBadConn
database/sql/driver
パッケージで定義されているエラーで、データベースドライバが「コネクションが壊れている、または使用できない」と判断した場合に返されます。このエラーが返された場合、database/sql
パッケージは通常、内部的に新しいコネクションを取得し、失敗した操作をリトライしようとします。これは、一時的なネットワークの問題やデータベースサーバーの再起動などによってコネクションが切断された場合に、アプリケーションが自動的に回復できるようにするための重要なメカニズムです。
プリペアドステートメント (*sql.Stmt
)
プリペアドステートメントは、SQLクエリを事前にデータベースサーバーに送信して準備(コンパイル)しておくことで、繰り返し実行する際のパフォーマンスを向上させるための機能です。SQLインジェクション攻撃を防ぐためにも推奨されます。database/sql
パッケージでは、DB.Prepare()
メソッドや Tx.Prepare()
メソッドで *sql.Stmt
オブジェクトを作成します。この *sql.Stmt
オブジェクトは、通常、それを準備した特定のデータベースコネクションに紐付けられます。
コネクションプール
database/sql
パッケージは、データベースコネクションのプールを内部的に管理しています。これにより、アプリケーションがデータベースにアクセスするたびに新しいコネクションを確立するオーバーヘッドを避けることができます。コネクションは使用後にプールに戻され、再利用されます。しかし、コネクションが適切にクローズされずにプールに戻されない場合、コネクションリークが発生し、プール内の利用可能なコネクションが枯渇する可能性があります。
自動再接続メカニズム
database/sql
パッケージは、driver.ErrBadConn
が返された場合に、透過的に操作をリトライし、必要に応じて新しいコネクションを確立しようとします。これは、アプリケーション開発者が手動でエラーハンドリングとリトライロジックを記述する手間を省くためのものです。しかし、このメカニズムが正しく機能しないと、アプリケーションの信頼性やパフォーマンスに悪影響を及ぼします。
技術的詳細
このコミットの技術的詳細は、主に database/sql/sql.go
と database/sql/fakedb_test.go
の変更に集約されます。
sql.go
の変更点
-
maxBadConnRetries
定数の導入:const maxBadConnRetries = 10
が導入されました。これは、driver.ErrBadConn
が返された場合に操作をリトライする最大回数を定義します。以前はハードコードされた10
が使われていましたが、これを定数として明示することで、コードの意図が明確になり、将来的な調整が容易になります。 -
DB
メソッドのリトライロジックの改善:DB.Prepare
,DB.Exec
,DB.Query
,DB.Begin
の各メソッドにおいて、driver.ErrBadConn
が返された場合にmaxBadConnRetries
回までリトライするループが明示的に記述されています。これにより、一時的なコネクションの問題が発生した場合でも、これらの操作が自動的に回復を試みるようになります。 -
Stmt.Exec
およびStmt.Query
のリトライロジックの追加: これがこのコミットの最も重要な変更点の一つです。以前のStmt.Exec
とStmt.Query
は、s.connStmt()
でコネクションとステートメントを取得し、エラーが発生した場合はすぐに返していました。しかし、この変更では、DB
メソッドと同様にmaxBadConnRetries
回のリトライループが導入されました。s.connStmt()
がdriver.ErrBadConn
を返した場合、ループを継続して再試行します。resultFromStatement
またはrowsiFromStatement
の実行中にdriver.ErrBadConn
が返された場合も、releaseConn(err)
を呼び出してコネクションを適切に解放し、ループを継続して再試行します。- これにより、プリペアドステートメントが紐付けられているコネクションが切断された場合でも、新しいコネクションでステートメントを再準備し、操作をリトライできるようになります。
-
Stmt.connStmt()
の変更:Stmt.connStmt()
は、プリペアドステートメントが使用するコネクションと、そのコネクション上のドライバステートメント (driver.Stmt
) を取得する内部メソッドです。- 以前は、新しいコネクションが必要な場合に
for i := 0; ; i++
の無限ループでコネクションを取得し、driver.ErrBadConn
の場合にcontinue
していましたが、このループはStmt
のcss
(cached statements) に新しいconnStmt
を追加するロジックと混在しており、複雑でした。 - 変更後、
Stmt.connStmt()
はdriver.ErrBadConn
のリトライロジックを直接持たず、単一のコネクション取得と準備の試行を行います。エラーが発生した場合は、そのエラーを呼び出し元 (Stmt.Exec
やStmt.Query
) に返します。これにより、Stmt.Exec
やStmt.Query
のリトライループがdriver.ErrBadConn
を適切に処理できるようになります。 - また、
dc.prepareLocked
でエラーが発生した場合にs.db.putConn(dc, err)
を呼び出してコネクションをプールに戻すことで、コネクションリークを防いでいます。
- 以前は、新しいコネクションが必要な場合に
fakedb_test.go
の変更点
このファイルは、database/sql
パッケージのテストのために、データベースドライバの振る舞いをシミュレートする fakeConn
や fakeStmt
を定義しています。
hookPrepareBadConn
,hookExecBadConn
,hookQueryBadConn
の追加: これらのグローバル変数は、テスト中にfakeConn.Prepare
,fakeStmt.Exec
,fakeStmt.Query
メソッドが意図的にdriver.ErrBadConn
を返すようにするためのフックです。これにより、実際のデータベース接続が切断された状況をシミュレートし、database/sql
パッケージの再接続ロジックが正しく機能するかを検証できるようになります。
sql_test.go
の変更点
TestErrBadConnReconnect
テストケースの追加: この新しいテストケースは、fakedb_test.go
で追加されたフックを利用して、DB.Exec
,DB.Query
,DB.Prepare
,Stmt.Exec
,Stmt.Query
の各操作がdriver.ErrBadConn
を受け取った際に、適切に再接続とリトライが行われることを検証します。simulateBadConn
ヘルパー関数が定義されており、指定された操作が一度driver.ErrBadConn
を発生させ、その後成功することを確認します。- 特に重要なのは、
numOpen
をチェックしてコネクションリークが発生していないことを検証している点です。これは、このコミットがコネクションリークの修正も目的としているため、非常に重要なテストです。 stmt1.css
やstmt2.css
のdc.inUse = true
を設定することで、プリペアドステートメントがキャッシュされたコネクションを使用している状態をシミュレートし、そのコネクションが壊れた場合の挙動をテストしています。
これらの変更により、database/sql
パッケージは、データベース接続が不安定な環境下でも、より堅牢に動作し、コネクションリークのリスクを低減するようになります。
コアとなるコードの変更箇所
src/pkg/database/sql/sql.go
--- a/src/pkg/database/sql/sql.go
+++ b/src/pkg/database/sql/sql.go
@@ -798,13 +798,17 @@ func (db *DB) putConnDBLocked(dc *driverConn, err error) bool {
return false
}
+// maxBadConnRetries is the number of maximum retries if the driver returns
+// driver.ErrBadConn to signal a broken connection.
+const maxBadConnRetries = 10
+
// Prepare creates a prepared statement for later queries or executions.
// Multiple queries or executions may be run concurrently from the
// returned statement.
func (db *DB) Prepare(query string) (*Stmt, error) {
var stmt *Stmt
var err error
- for i := 0; i < 10; i++ {
+ for i := 0; i < maxBadConnRetries; i++ {
stmt, err = db.prepare(query)
if err != driver.ErrBadConn {
break
@@ -846,7 +850,7 @@ func (db *DB) prepare(query string) (*Stmt, error) {
func (db *DB) Exec(query string, args ...interface{}) (Result, error) {
var res Result
var err error
- for i := 0; i < 10; i++ {
+ for i := 0; i < maxBadConnRetries; i++ {
res, err = db.exec(query, args)
if err != driver.ErrBadConn {
break
@@ -895,7 +899,7 @@ func (db *DB) exec(query string, args []interface{}) (res Result, err error) {
func (db *DB) Query(query string, args ...interface{}) (*Rows, error) {
var rows *Rows
var err error
- for i := 0; i < 10; i++ {
+ for i := 0; i < maxBadConnRetries; i++ {
rows, err = db.query(query, args)
if err != driver.ErrBadConn {
break
@@ -983,7 +987,7 @@ func (db *DB) QueryRow(query string, args ...interface{}) *Row {
func (db *DB) Begin() (*Tx, error) {
var tx *Tx
var err error
- for i := 0; i < 10; i++ {
+ for i := 0; i < maxBadConnRetries; i++ {
tx, err = db.begin()
if err != driver.ErrBadConn {
break
@@ -1245,13 +1249,24 @@ type Stmt struct {
func (s *Stmt) Exec(args ...interface{}) (Result, error) {
s.closemu.RLock()
defer s.closemu.RUnlock()
-\tdc, releaseConn, si, err := s.connStmt()\n-\tif err != nil {\n-\t\treturn nil, err\n-\t}\n-\tdefer releaseConn(nil)\n \n-\treturn resultFromStatement(driverStmt{dc, si}, args...)\n+\tvar res Result\n+\tfor i := 0; i < maxBadConnRetries; i++ {\n+\t\tdc, releaseConn, si, err := s.connStmt()\n+\t\tif err != nil {\n+\t\t\tif err == driver.ErrBadConn {\n+\t\t\t\tcontinue\n+\t\t\t}\n+\t\t\treturn nil, err\n+\t\t}\n+\n+\t\tres, err = resultFromStatement(driverStmt{dc, si}, args...)\n+\t\treleaseConn(err)\n+\t\tif err != driver.ErrBadConn {\n+\t\t\treturn res, err\n+\t\t}\n+\t}\n+\treturn nil, driver.ErrBadConn
}
func resultFromStatement(ds driverStmt, args ...interface{}) (Result, error) {
@@ -1329,26 +1344,21 @@ func (s *Stmt) connStmt() (ci *driverConn, releaseConn func(error), si driver.St
// Make a new conn if all are busy.
// TODO(bradfitz): or wait for one? make configurable later?
if !match {
-\t\tfor i := 0; ; i++ {\n-\t\t\tdc, err := s.db.conn()\n-\t\t\tif err != nil {\n-\t\t\t\treturn nil, nil, nil, err\n-\t\t\t}\n-\t\t\tdc.Lock()\n-\t\t\tsi, err := dc.prepareLocked(s.query)\n-\t\t\tdc.Unlock()\n-\t\t\tif err == driver.ErrBadConn && i < 10 {\n-\t\t\t\tcontinue\n-\t\t\t}\n-\t\t\tif err != nil {\n-\t\t\t\treturn nil, nil, nil, err\n-\t\t\t}\n-\t\t\ts.mu.Lock()\n-\t\t\tcs = connStmt{dc, si}\n-\t\t\ts.css = append(s.css, cs)\n-\t\t\ts.mu.Unlock()\n-\t\t\tbreak\n+\t\tdc, err := s.db.conn()\n+\t\tif err != nil {\n+\t\t\treturn nil, nil, nil, err\n \t\t}\n+\t\tdc.Lock()\n+\t\tsi, err := dc.prepareLocked(s.query)\n+\t\tdc.Unlock()\n+\t\tif err != nil {\n+\t\t\ts.db.putConn(dc, err)\n+\t\t\treturn nil, nil, nil, err\n+\t\t}\n+\t\ts.mu.Lock()\n+\t\tcs = connStmt{dc, si}\n+\t\ts.css = append(s.css, cs)\n+\t\ts.mu.Unlock()\n }\
conn := cs.dc
@@ -1361,31 +1371,39 @@ func (s *Stmt) Query(args ...interface{}) (*Rows, error) {
s.closemu.RLock()
defer s.closemu.RUnlock()
-\tdc, releaseConn, si, err := s.connStmt()\n-\tif err != nil {\n-\t\treturn nil, err\n-\t}\n+\tvar rowsi driver.Rows\n+\tfor i := 0; i < maxBadConnRetries; i++ {\n+\t\tdc, releaseConn, si, err := s.connStmt()\n+\t\tif err != nil {\n+\t\t\tif err == driver.ErrBadConn {\n+\t\t\t\tcontinue\n+\t\t\t}\n+\t\t\treturn nil, err\n+\t\t}\n \n-\tds := driverStmt{dc, si}\n-\trowsi, err := rowsiFromStatement(ds, args...)\n-\tif err != nil {\n-\t\treleaseConn(err)\n-\t\treturn nil, err\n-\t}\n+\t\trowsi, err = rowsiFromStatement(driverStmt{dc, si}, args...)\n+\t\tif err == nil {\n+\t\t\t// Note: ownership of ci passes to the *Rows, to be freed\n+\t\t\t// with releaseConn.\n+\t\t\trows := &Rows{\n+\t\t\t\tdc: dc,\n+\t\t\t\trowsi: rowsi,\n+\t\t\t\t// releaseConn set below\n+\t\t\t}\n+\t\t\ts.db.addDep(s, rows)\n+\t\t\trows.releaseConn = func(err error) {\n+\t\t\t\treleaseConn(err)\n+\t\t\t\ts.db.removeDep(s, rows)\n+\t\t\t}\n+\t\t\treturn rows, nil\n+\t\t}\n \n-\t// Note: ownership of ci passes to the *Rows, to be freed\n-\t// with releaseConn.\n-\trows := &Rows{\n-\t\tdc: dc,\n-\t\trowsi: rowsi,\n-\t\t// releaseConn set below\n-\t}\n-\ts.db.addDep(s, rows)\n-\trows.releaseConn = func(err error) {\n \t\treleaseConn(err)\n-\t\ts.db.removeDep(s, rows)\n+\t\tif err != driver.ErrBadConn {\n+\t\t\treturn nil, err\n+\t\t}\n \t}\n-\treturn rows, nil\n+\treturn nil, driver.ErrBadConn
}
src/pkg/database/sql/fakedb_test.go
--- a/src/pkg/database/sql/fakedb_test.go
+++ b/src/pkg/database/sql/fakedb_test.go
@@ -433,11 +433,19 @@ func (c *fakeConn) prepareInsert(stmt *fakeStmt, parts []string) (driver.Stmt, e
return stmt, nil
}
+// hook to simulate broken connections
+var hookPrepareBadConn func() bool
+
func (c *fakeConn) Prepare(query string) (driver.Stmt, error) {
c.numPrepare++
if c.db == nil {
panic(\"nil c.db; conn = \" + fmt.Sprintf(\"%#v\", c))\
}\
+\n+\tif hookPrepareBadConn != nil && hookPrepareBadConn() {\n+\t\treturn nil, driver.ErrBadConn\n+\t}\n+\n parts := strings.Split(query, \"|\")
if len(parts) < 1 {
return nil, errf(\"empty query\")
@@ -489,10 +497,18 @@ func (s *fakeStmt) Close() error {
var errClosed = errors.New(\"fakedb: statement has been closed\")
+// hook to simulate broken connections
+var hookExecBadConn func() bool
+
func (s *fakeStmt) Exec(args []driver.Value) (driver.Result, error) {
if s.closed {
return nil, errClosed
}\
+\n+\tif hookExecBadConn != nil && hookExecBadConn() {\n+\t\treturn nil, driver.ErrBadConn\n+\t}\n+\n err := checkSubsetTypes(args)
if err != nil {
return nil, err
@@ -565,10 +581,18 @@ func (s *fakeStmt) execInsert(args []driver.Value, doInsert bool) (driver.Result
return driver.RowsAffected(1), nil
}
+// hook to simulate broken connections
+var hookQueryBadConn func() bool
+
func (s *fakeStmt) Query(args []driver.Value) (driver.Rows, error) {
if s.closed {
return nil, errClosed
}\
+\n+\tif hookQueryBadConn != nil && hookQueryBadConn() {\n+\t\treturn nil, driver.ErrBadConn\n+\t}\n+\n err := checkSubsetTypes(args)
if err != nil {
return nil, err
src/pkg/database/sql/sql_test.go
--- a/src/pkg/database/sql/sql_test.go
+++ b/src/pkg/database/sql/sql_test.go
@@ -1255,6 +1254,111 @@ func TestStmtCloseOrder(t *testing.T) {
}
}\
+// golang.org/issue/5781
+func TestErrBadConnReconnect(t *testing.T) {
+ db := newTestDB(t, \"foo\")
+ defer closeDB(t, db)
+ exec(t, db, \"CREATE|t1|name=string,age=int32,dead=bool\")
+
+ simulateBadConn := func(name string, hook *func() bool, op func() error) {
+ broken, retried := false, false
+ numOpen := db.numOpen
+
+ // simulate a broken connection on the first try
+ *hook = func() bool {
+ if !broken {
+ broken = true
+ return true
+ }
+ retried = true
+ return false
+ }
+
+ if err := op(); err != nil {
+ t.Errorf(name+\": %v\", err)
+ return
+ }
+
+ if !broken || !retried {
+ t.Error(name + \": Failed to simulate broken connection\")
+ }
+ *hook = nil
+
+ if numOpen != db.numOpen {
+ t.Errorf(name+\": leaked %d connection(s)!\", db.numOpen-numOpen)
+ numOpen = db.numOpen
+ }
+ }
+
+ // db.Exec
+ dbExec := func() error {
+ _, err := db.Exec(\"INSERT|t1|name=?,age=?,dead=?\", \"Gordon\", 3, true)
+ return err
+ }
+ simulateBadConn(\"db.Exec prepare\", &hookPrepareBadConn, dbExec)
+ simulateBadConn(\"db.Exec exec\", &hookExecBadConn, dbExec)
+
+ // db.Query
+ dbQuery := func() error {
+ rows, err := db.Query(\"SELECT|t1|age,name|\")
+ if err == nil {
+ err = rows.Close()
+ }
+ return err
+ }
+ simulateBadConn(\"db.Query prepare\", &hookPrepareBadConn, dbQuery)
+ simulateBadConn(\"db.Query query\", &hookQueryBadConn, dbQuery)
+
+ // db.Prepare
+ simulateBadConn(\"db.Prepare\", &hookPrepareBadConn, func() error {
+ stmt, err := db.Prepare(\"INSERT|t1|name=?,age=?,dead=?\")
+ if err != nil {
+ return err
+ }
+ stmt.Close()
+ return nil
+ })
+
+ // stmt.Exec
+ stmt1, err := db.Prepare(\"INSERT|t1|name=?,age=?,dead=?\")
+ if err != nil {
+ t.Fatalf(\"prepare: %v\", err)
+ }
+ defer stmt1.Close()
+ // make sure we must prepare the stmt first
+ for _, cs := range stmt1.css {
+ cs.dc.inUse = true
+ }
+
+ stmtExec := func() error {
+ _, err := stmt1.Exec(\"Gopher\", 3, false)
+ return err
+ }
+ simulateBadConn(\"stmt.Exec prepare\", &hookPrepareBadConn, stmtExec)
+ simulateBadConn(\"stmt.Exec exec\", &hookExecBadConn, stmtExec)
+
+ // stmt.Query
+ stmt2, err := db.Prepare(\"SELECT|t1|age,name|\")
+ if err != nil {
+ t.Fatalf(\"prepare: %v\", err)
+ }
+ defer stmt2.Close()
+ // make sure we must prepare the stmt first
+ for _, cs := range stmt2.css {
+ cs.dc.inUse = true
+ }
+
+ stmtQuery := func() error {
+ rows, err := stmt2.Query()
+ if err == nil {
+ err = rows.Close()
+ }
+ return err
+ }
+ simulateBadConn(\"stmt.Query prepare\", &hookPrepareBadConn, stmtQuery)
+ simulateBadConn(\"stmt.Query exec\", &hookQueryBadConn, stmtQuery)
+}
+
type concurrentTest interface {
init(t testing.TB, db *DB)
finish(t testing.TB)
コアとなるコードの解説
このコミットの核心は、database/sql
パッケージが driver.ErrBadConn
を受け取った際の再接続とリトライのロジックを、より堅牢かつ一貫性のあるものにすることです。
-
一貫したリトライメカニズム: 以前は、
DB
オブジェクトのメソッド(Prepare
,Exec
,Query
,Begin
)と、Stmt
オブジェクトのメソッド(Exec
,Query
)で、driver.ErrBadConn
のハンドリングに差異がありました。このコミットでは、すべての主要な操作に対してmaxBadConnRetries
(10回) のリトライループを導入することで、一貫した自動再接続の振る舞いを保証しています。これにより、一時的なネットワークの問題やデータベースの再起動などによってコネクションが切断された場合でも、アプリケーションコードを変更することなく、透過的に回復を試みることができます。 -
プリペアドステートメントの再準備とコネクションリークの修正: 特に重要だったのは、
*sql.Stmt
のExec
およびQuery
メソッドにおける修正です。プリペアドステートメントは、通常、特定のデータベースコネクションに紐付けられています。もしそのコネクションがdriver.ErrBadConn
によって無効になった場合、ステートメントは新しいコネクション上で再準備される必要があります。- 変更前は、
Stmt.connStmt()
がdriver.ErrBadConn
を返した場合、Stmt.Exec
やStmt.Query
はすぐにエラーを返していました。これにより、自動再接続が機能せず、アプリケーションがエラーを処理する必要がありました。 - 変更後、
Stmt.Exec
とStmt.Query
は、s.connStmt()
からdriver.ErrBadConn
が返された場合、またはその後の操作中にdriver.ErrBadConn
が返された場合に、リトライループ内で新しいコネクションを取得し、ステートメントを再準備しようとします。 - また、
Stmt.connStmt()
内でdc.prepareLocked
が失敗した場合にs.db.putConn(dc, err)
を呼び出すことで、コネクションが適切にプールに戻され、リークが防止されます。これにより、無効になったコネクションがプールに残り続け、利用可能なコネクションが枯渇する問題を解決しています。
- 変更前は、
-
テストカバレッジの向上:
fakedb_test.go
に追加されたフックと、sql_test.go
に追加されたTestErrBadConnReconnect
テストケースは、この修正の有効性を検証するために不可欠です。これらのテストは、driver.ErrBadConn
が発生する様々なシナリオ(Prepare
時、Exec
時、Query
時など)をシミュレートし、database/sql
パッケージが期待通りに再接続し、コネクションリークが発生しないことを確認します。これにより、将来的な回帰を防ぎ、パッケージの信頼性を高めています。
このコミットは、Goアプリケーションがデータベースと対話する際の堅牢性と信頼性を大幅に向上させるものであり、特に長期間稼働するサービスや、ネットワークの不安定な環境下での運用においてその恩恵は大きいです。
関連リンク
- https://github.com/golang/go/commit/762a9d934eab267418595df7a220eec50919b77d
- https://golang.org/cl/14920046
- https://golang.org/issue/5718
参考にした情報源リンク
- Go database/sql documentation
- Go database/sql/driver documentation
- Go's database/sql package: a deep dive (一般的な
database/sql
の動作理解のため) - Go Concurrency Patterns: Context (直接的な関連はないが、Goのコネクション管理やタイムアウトの文脈で関連する可能性のある概念として)
- Understanding Go's database/sql package (一般的な
database/sql
の動作理解のため)