database/sql Connection Pool — Specification¶
Table of Contents¶
- Introduction
- Go database/sql Package Documentation
- database/sql/driver Interface Contracts
- The Single-Thread Guarantee
- Connection Lifecycle
- Context Cancellation Contract
- Transaction Contracts
- Prepared Statement Contracts
- Go Memory Model Implications
- SQL Standard Transaction Isolation
- Cross-Reference Table
- References
Introduction¶
This file collects normative excerpts from database/sql and database/sql/driver package docs, the relevant Go memory model statements, and the SQL standard isolation levels referenced by sql.IsolationLevel. It is the reference you consult when a behavior in the junior/middle/senior files needs an authoritative source.
Citations are taken from Go 1.22 source comments and the SQL:2016 standard. Paraphrased only for compactness.
Go database/sql Package Documentation¶
*sql.DB is a pool, not a connection¶
From database/sql/sql.go package doc:
DB is a database handle representing a pool of zero or more underlying connections. It's safe for concurrent use by multiple goroutines. The sql package creates and frees connections automatically; it also maintains a free pool of idle connections. If the database has a concept of per-connection state, such state can be reliably observed within a transaction (Tx) or connection (Conn). Once DB.Begin is called, the returned Tx is bound to a single connection. Once Commit or Rollback is called on the transaction, that transaction's connection is returned to DB's idle connection pool.
Pool limits¶
From database/sql/sql.go doc comments on the setters:
SetMaxOpenConns sets the maximum number of open connections to the database. If MaxIdleConns is greater than 0 and the new MaxOpenConns is less than MaxIdleConns, then MaxIdleConns will be reduced to match the new MaxOpenConns limit. If n <= 0, then there is no limit on the number of open connections. The default is 0 (unlimited).
SetMaxIdleConns sets the maximum number of connections in the idle connection pool. If MaxOpenConns is greater than 0 but less than the new MaxIdleConns, then the new MaxIdleConns will be reduced to match the MaxOpenConns limit. If n <= 0, no idle connections are retained. The default max idle connections is currently 2. This may change in a future release.
SetConnMaxLifetime sets the maximum amount of time a connection may be reused. Expired connections may be closed lazily before reuse. If d <= 0, connections are not closed due to a connection's age.
SetConnMaxIdleTime sets the maximum amount of time a connection may be idle. Expired connections may be closed lazily before reuse. If d <= 0, connections are not closed due to a connection's idle time.
DB.Conn for single-connection scope¶
Conn returns a single connection by either opening a new connection or returning an existing connection from the connection pool. Conn will block until either a connection is returned or ctx is canceled. Queries run on the same Conn will be run in the same database session. Every Conn must be returned to the database pool after use by calling Conn.Close.
DB.PingContext opens a connection if necessary¶
PingContext verifies a connection to the database is still alive, establishing a connection if necessary.
database/sql/driver Interface Contracts¶
driver.Driver¶
Open returns a new connection to the database. The name is a string in a driver-specific format. Open may return a cached connection (one previously closed), but doing so is unnecessary; the sql package maintains a pool of idle connections for efficient re-use.
driver.Connector (preferred since Go 1.10)¶
Connect returns a connection to the database. Connect may return a cached connection (one previously closed), but doing so is unnecessary; the sql package maintains a pool of idle connections for efficient re-use. The provided context.Context is for dialing only and should not be retained for use after Connect returns.
driver.Conn¶
Extended interfaces a real driver should also implement:
ConnPrepareContext:PrepareContext(ctx context.Context, query string) (Stmt, error)ConnBeginTx:BeginTx(ctx context.Context, opts TxOptions) (Tx, error)Pinger:Ping(ctx context.Context) errorValidator:IsValid() bool(Go 1.15+, used bysqlto drop bad conns before reuse)SessionResetter:ResetSession(ctx context.Context) error(called before reuse)NamedValueChecker,ExecerContext,QueryerContext
Driver Stmt¶
type Stmt interface {
Close() error
NumInput() int
Exec(args []Value) (Result, error) // deprecated; use StmtExecContext
Query(args []Value) (Rows, error) // deprecated; use StmtQueryContext
}
Extended: - StmtExecContext - StmtQueryContext
Driver Rows¶
Extended: - RowsColumnTypeDatabaseTypeName, RowsColumnTypeLength, RowsColumnTypeNullable, RowsColumnTypePrecisionScale, RowsColumnTypeScanType - RowsNextResultSet
The Single-Thread Guarantee¶
From database/sql/driver/driver.go package doc:
All Conn implementations should implement the following interfaces: Pinger, SessionResetter, and Validator. If named parameters or context are supported, the driver's Conn should implement: ExecerContext, QueryerContext, ConnPrepareContext, and ConnBeginTx.
And critically:
Drivers are now encouraged to implement Validator, IsValid(). The sql package will call IsValid before reusing a connection. If IsValid returns false, the connection will be removed from the pool.
The single-thread guarantee (paraphrased from package commentary and the (*DB).conn lock structure in sql.go:1330):
A driver's Conn and any objects derived from it (Stmt, Tx, Rows) MUST NOT be used concurrently by multiple goroutines. The sql package ensures this by holding
driverConn.Lock()for the duration of every operation that touches a particular*driverConn.
This means a driver author writing (*pgConn).ExecContext may freely assume: - No other goroutine is calling any method on this Conn at the same time. - No Stmt derived from this conn is being used concurrently. - The conn's network socket has a single reader and a single writer.
The driver may still launch internal goroutines (e.g., for reading the wire) but must serialize external API calls itself if it does so.
Connection Lifecycle¶
A driverConn (the sql-package wrapper around driver.Conn) progresses through these states:
| State | Field/Condition | Meaning |
|---|---|---|
| Opening | counted in numOpen, not yet in freeConn | connectionOpener is dialing |
| Idle (free) | in freeConn slice, inUse == false | Available for next conn() call |
| Checked out | inUse == true, removed from freeConn | A goroutine owns it for query or tx |
| Closing | closed == true, removed from freeConn, in numOpen | Either age/idle-time expired or driver returned an error |
| Closed | numOpen decremented, dc.finalClose() called | Returned to driver Close() |
See database/sql/sql.go:467 for *driverConn struct definition. The fields dc.inUse, dc.closed, dc.dbmuClosed, and dc.createdAt are protected by dc.Lock() (a per-conn sync.Mutex), while dc.db.freeConn and dc.db.numOpen are protected by dc.db.mu.
Context Cancellation Contract¶
From database/sql/sql.go doc on DB.QueryContext:
QueryContext executes a query that returns rows, typically a SELECT. The args are for any placeholder parameters in the query.
The ctx contract has three points:
- Before checkout. If
ctxis already canceled before(*DB).connis reached,conn()returnsctx.Err()without allocating a conn. - While waiting in
connRequests. Ifctxis canceled while the goroutine is parked on its request channel,(*DB).connreturnsctx.Err()and arranges that any future fulfilment of the request is put back into the pool. Seesql.go:1390-1430. - During driver work. Once the conn is in the driver's hands, the driver's
*Contextmethod is responsible for honoring cancellation. Thesqlpackage spawns a small "ctx-watcher" goroutine ((*DB).withLock→tx.awaitDone-style) that calls back into the driver to abort.
Spec text on cancellation¶
From database/sql/driver/driver.go:
If a Conn does not implement QueryerContext, the sql package's DB.Query will fall back to Queryer. If a Conn implements neither, DB.Query will use Prepare followed by Stmt.Query. Cancellation is best-effort; the driver may not be able to interrupt an in-flight query.
Transaction Contracts¶
sql.IsolationLevel¶
type IsolationLevel int
const (
LevelDefault IsolationLevel = iota
LevelReadUncommitted
LevelReadCommitted
LevelWriteCommitted
LevelRepeatableRead
LevelSnapshot
LevelSerializable
LevelLinearizable
)
From the SQL:2016 standard, §4.35.4 (paraphrased):
| Level | Phenomena prevented |
|---|---|
| READ UNCOMMITTED | None |
| READ COMMITTED | Dirty reads |
| REPEATABLE READ | Dirty reads, non-repeatable reads |
| SERIALIZABLE | Dirty reads, non-repeatable reads, phantom reads |
SNAPSHOT and LINEARIZABLE are not in SQL:2016 but are commonly supported by Postgres, MS SQL, and Spanner respectively.
Tx pinning¶
From the sql.Tx doc:
Tx is an in-progress database transaction. A transaction must end with a call to Commit or Rollback. After a call to Commit or Rollback, all operations on the transaction fail with ErrTxDone. The statements prepared for a transaction by calling the transaction's Prepare or Stmt methods are closed by the call to Commit or Rollback.
This is the formal source of the "transaction pins a connection" rule: every Tx holds an exclusive reference to a *driverConn from BeginTx until Commit or Rollback.
Prepared Statement Contracts¶
From the sql.Stmt doc:
A prepared statement is safe for concurrent use by multiple goroutines.
This is implemented in database/sql/sql.go:2700+ by caching, per *sql.Stmt, a map from *driverConn to driver.Stmt. When a goroutine calls stmt.QueryContext, the package:
- Acquires a
*driverConn(via(*DB).conn). - Looks up the driver
Stmtfor this conn in the cache; if absent, callsdc.ci.PrepareContext. - Executes the query.
- Returns the conn to the pool (driver stmt stays cached on the conn).
This is why a *sql.Stmt is goroutine-safe even though a driver.Stmt is not: each goroutine gets its own conn-bound driver stmt.
Go Memory Model Implications¶
From https://go.dev/ref/mem:
The closing of a channel happens before a receive that returns because the channel is closed.
For any sync.Mutex or sync.RWMutex variable l and n < m, call n of l.Unlock() happens before call m of l.Lock() returns.
Applied to database/sql:
dc.db.mu.Lock()in(*DB).connsynchronizes-withdc.db.mu.Unlock()in(*DB).putConn, so a conn returned by goroutine A is safely usable by goroutine B.- The
connRequestchannel send/receive inputConnDBLocked(sql.go:1480) establishes a happens-before edge from the releasing goroutine to the waiting goroutine. - The per-conn
dc.Lock()makes all driver-side state writes visible to any subsequent user of that conn.
The sql package does NOT use any sync/atomic field on driverConn for ordering — every cross-goroutine field is mutex-protected.
SQL Standard Transaction Isolation¶
SQL:2016 §4.35.4 defines four standard levels; database/sql maps them as follows:
sql.LevelXxx | SQL standard | Notes |
|---|---|---|
| LevelReadUncommitted | READ UNCOMMITTED | Allows dirty reads |
| LevelReadCommitted | READ COMMITTED | Default in Postgres, Oracle |
| LevelRepeatableRead | REPEATABLE READ | Default in MySQL InnoDB |
| LevelSerializable | SERIALIZABLE | Strongest standard level |
| LevelSnapshot | (not in std) | Postgres REPEATABLE READ is actually snapshot |
| LevelLinearizable | (not in std) | Spanner external consistency |
The driver translates sql.TxOptions.Isolation to a SET TRANSACTION ISOLATION LEVEL ... statement (or driver-specific protocol message) when the underlying driver.ConnBeginTx is called.
Cross-Reference Table¶
| Concept | Source location | Lock |
|---|---|---|
DB struct | database/sql/sql.go:430 | DB.mu |
driverConn struct | database/sql/sql.go:467 | driverConn.Lock and DB.mu |
(*DB).conn | database/sql/sql.go:1330 | DB.mu |
(*DB).putConn | database/sql/sql.go:1450 | DB.mu |
(*DB).putConnDBLocked | database/sql/sql.go:1475 | caller holds DB.mu |
(*DB).connectionOpener | database/sql/sql.go:1190 | takes DB.mu |
(*DB).openNewConnection | database/sql/sql.go:1210 | takes DB.mu |
(*DB).connectionCleaner | database/sql/sql.go:1080 | takes DB.mu |
connRequest channel | database/sql/sql.go:480 (map field) | per-request channel; DB.mu guards map |
freeConn slice | database/sql/sql.go:444 | DB.mu |
Tx.Commit / Rollback | database/sql/sql.go:2200+ | Tx.closemu, Tx.cancel, dc.Lock |
Stmt.QueryContext | database/sql/sql.go:2880+ | Stmt.css slice, per-conn dc.Lock |
References¶
- Go source:
src/database/sql/sql.go(Go 1.22+). - Go source:
src/database/sql/driver/driver.go. - Go memory model: https://go.dev/ref/mem.
- Go
database/sqlpackage doc: https://pkg.go.dev/database/sql. - Go
database/sql/driverpackage doc: https://pkg.go.dev/database/sql/driver. - SQL:2016 standard (ISO/IEC 9075-1:2016), §4.35 Transactions.
- PostgreSQL docs on isolation levels: https://www.postgresql.org/docs/current/transaction-iso.html.
- MySQL InnoDB locking and isolation: https://dev.mysql.com/doc/refman/8.0/en/innodb-transaction-isolation-levels.html.
- Go blog: "Go Database/SQL Tutorial" (still authoritative for pool semantics).
- Brad Fitzpatrick's original
database/sqldesign notes (golang-dev mailing list, 2011–2013).