Prepared Statements and Transactions

Purpose and Scope

This page explains how prepared statements and transactions fit into Go's database/sql programming model. A prepared statement is SQL text that has been parsed and retained by a database system or driver so that it can be executed repeatedly with different parameter values. A transaction is a scoped unit of work that groups several database operations so they either commit together or roll back together. In Go, application code normally works with sql.Stmt for prepared statements and sql.Tx for transactions, while the database/sql package coordinates connection use, argument conversion, context cancellation, and driver capabilities underneath.

The reader-facing workflow is intentionally simple: prepare repeated SQL with DB.Prepare or transaction-scoped preparation, execute it through Exec, Query, or QueryRow, and close it when the prepared statement is no longer needed. For transactions, call DB.Begin or DB.BeginTx, perform all required operations through the returned Tx, then end with exactly one of Commit or Rollback. The repository code shown here explains the less visible guarantees behind that workflow: how arguments become driver values, how context-aware driver methods are preferred, how unsupported transaction options are rejected, and how close synchronization prevents ordinary use from racing with teardown.

Sources: src/database/sql/ctxutil.go, src/database/sql/convert.go, src/database/sql/driver/driver.go, src/database/sql/driver/types.go, src/database/sql/closemu.go

Relevant Source Files

  • src/database/sql/closemu.go - Defines closingMutex, an internal read/write-style synchronization primitive used by the sql package to coordinate normal operations with close paths while allowing reentrant read-side use.
  • src/database/sql/convert.go - Contains the argument conversion path used by statement execution and querying, including named value validation, driver NamedValueChecker use, column conversion, and default parameter conversion.
  • src/database/sql/ctxutil.go - Implements context-aware adapters for prepare, exec, query, statement exec/query, and transaction begin operations at the driver boundary.
  • src/database/sql/driver/driver.go - Documents and declares the driver interfaces that database/sql expects drivers to implement, including context-aware prepare, query, exec, and begin support.
  • src/database/sql/driver/types.go - Defines driver value conversion contracts such as ValueConverter and Valuer, which are involved when application values are passed into prepared statements or scanned from results.
  • src/database/sql/internal/sql.go - Provides the internal ScanContext bridge shared between database/sql and database/sql/driver without exposing that detail as public API.

Core Primitives

The main application-level primitives are DB, Tx, and Stmt. A DB is a long-lived handle that manages a pool of driver connections. A Tx represents a transaction bound to a single underlying connection for its lifetime. A Stmt represents prepared SQL; depending on how it is created, it can be associated with the DB for pooled use or with a Tx for transaction-scoped use. The important rule for developers is that transaction work should be performed through the Tx, not by mixing in direct DB calls that might use different pooled connections.

The driver boundary uses a different set of primitives. The driver package defines the values and interfaces that concrete database drivers implement. Its package documentation states that most code should use database/sql, while drivers implement interfaces such as Connector, DriverContext, ConnPrepareContext, ConnBeginTx, ExecerContext, and QueryerContext as their capabilities evolve. That split lets application code use stable database/sql methods while the package selects the most capable driver path available at runtime.

Sources: src/database/sql/driver/driver.go

Parameters passed to Stmt.Exec, Stmt.Query, and related transaction methods are represented internally as driver.NamedValue values. A named value carries a Name, an ordinal position starting at one, and a driver-compatible value. This matters for prepared statements because placeholder syntax is driver-specific, but the sql package still needs a uniform way to validate and pass the actual argument values. The conversion layer preserves ordinal information, supports named arguments when the driver can accept them, and reports an error when named parameters reach a path that only supports positional driver.Value arguments.

Sources: src/database/sql/convert.go, src/database/sql/driver/driver.go

Prepared Statement Lifecycle

A prepared statement lifecycle starts with preparing SQL text and ends with closing the statement. The official Go documentation recommends preparing SQL when the same operation is expected to run repeatedly, because the database management system can parse and save the statement once and execute it later with new parameter values. The source-level counterpart is ctxDriverPrepare, which first checks whether the driver connection implements driver.ConnPrepareContext. If so, the context-aware prepare method is used directly. Otherwise the package falls back to Conn.Prepare and then checks whether the context was canceled before returning the statement.

This fallback behavior is important because it gives older drivers a compatible path while still respecting cancellation as much as possible. If a non-context prepare succeeds but the context is already done before control returns to the caller, ctxDriverPrepare closes the newly created driver statement and returns the context error. That prevents a canceled prepare operation from leaking a driver-side statement. It also shows the general shape of database/sql: prefer modern context-aware driver interfaces, but wrap legacy interfaces carefully so callers still see context-sensitive behavior where feasible.

Sources: src/database/sql/ctxutil.go, src/database/sql/driver/driver.go

Once a statement exists, execution follows the statement-specific context helpers. ctxDriverStmtExec prefers driver.StmtExecContext when the driver statement implements it; otherwise it converts named arguments to ordinary driver.Value arguments, checks the context before the call, and invokes the older Stmt.Exec method. ctxDriverStmtQuery follows the same pattern for queries, preferring driver.StmtQueryContext and falling back to Stmt.Query. These helpers explain why Stmt.ExecContext and Stmt.QueryContext are the preferred public forms in new code: they give capable drivers the context directly rather than only checking cancellation before a legacy call begins.

Sources: src/database/sql/ctxutil.go

Closing a prepared statement is not just a memory-management convention. The sql package must coordinate close operations with concurrent users so that a statement, result, or related object is not torn down while an operation is still using it. The closingMutex type in the supplied source is designed for this style of synchronization. Its comments describe it as an RWMutex for synchronizing close, with read-side locking prioritized over close and with reentrant-safe reads. That tradeoff favors active database operations while still allowing a closing path to wait until readers drain.

Sources: src/database/sql/closemu.go

Transaction Execution Flow

A transaction begins with DB.Begin or DB.BeginTx, then all operations that participate in the transaction should be invoked on the returned Tx. The official workflow is begin, perform operations, commit if all operations succeed, or roll back when an error occurs. The implementation evidence in ctxutil.go shows the driver-facing begin step. ctxDriverBegin checks whether the driver connection implements driver.ConnBeginTx; when it does, database/sql converts public transaction options into driver.TxOptions and calls BeginTx with the caller's context.

If a driver lacks ConnBeginTx, the fallback path is deliberately conservative. When non-default isolation is requested and the driver cannot accept BeginTx, ctxDriverBegin returns an error saying the driver does not support a non-default isolation level. When read-only mode is requested through options and the driver cannot accept it, the helper returns an error saying read-only transactions are unsupported. These checks protect callers from believing that transaction options were applied when the selected driver interface has no way to receive them.

Sources: src/database/sql/ctxutil.go, src/database/sql/driver/driver.go

For default transactions on older drivers, ctxDriverBegin can call Conn.Begin. If the context has no done channel, it simply begins the transaction. If the context can be canceled, the helper begins the transaction and then checks whether cancellation happened before returning. On cancellation after a successful legacy begin, it calls Rollback on the driver transaction and returns the context error. This mirrors the prepare fallback: when a driver lacks a context-aware operation, the sql package still avoids handing the caller a live resource for an operation that has already been canceled.

Sources: src/database/sql/ctxutil.go

Prepared statements inside transactions require a special mental model. A transaction is tied to one connection, so any statement prepared through the transaction is also scoped to that transactional connection and should be closed when no longer needed. Public methods such as Tx.Prepare, Tx.PrepareContext, Tx.Stmt, and Tx.StmtContext support the documented pattern of predefining statements for use inside a transaction. The source snippets do not show these public method bodies, but the driver helpers show the shared lower-level mechanics: prepare chooses context-aware driver support when possible, and statement execution chooses context-aware driver statement methods when available.

Sources: src/database/sql/ctxutil.go

Argument and Value Conversion

Prepared statements are only useful if argument values are converted predictably. driverArgsConnLocked is the conversion path from caller-supplied []any arguments to []driver.NamedValue values. It determines the expected number of placeholders from the driver statement's NumInput when a statement is available, while using -1 when the driver cannot provide that count. It then selects a checking path: first a statement-level driver.NamedValueChecker, then a connection-level checker, then a column converter if present, and finally the default parameter converter.

Sources: src/database/sql/convert.go, src/database/sql/driver/driver.go

This layered conversion contract gives drivers room to handle custom types without making every application depend on driver internals. The driver package documentation recommends NamedValueChecker for custom data types and notes that it can also accept per-query options by returning ErrRemoveArgument. When a value implements driver.Valuer, conversion can call its Value method so the type can turn itself into a supported driver value. types.go defines Valuer and says errors returned by Value are wrapped by database/sql, which lets callers use errors.Is after operations such as query or exec.

Sources: src/database/sql/convert.go, src/database/sql/driver/types.go

The default driver value universe is intentionally small: nil, int64, float64, bool, []byte, string, and time.Time, plus driver-supported cursor values. ValueConverter implementations in the driver package provide common conversions and validation, such as converting to bool or an int32-sized integer while detecting overflow or invalid textual input. For prepared statements and transactions, this means conversion errors can occur before the database sees the SQL operation. Treat those errors the same way as driver execution errors: stop the transaction workflow, roll back if needed, and return useful context to the caller.

Sources: src/database/sql/driver/types.go

Context-Aware Behavior Reference

The context helpers are the compact implementation reference for cancellation at the driver boundary. They do not make a non-context driver operation interruptible once it has started, but they do select context-aware interfaces when available and check cancellation before or immediately after legacy operations where the package can still clean up safely. This distinction matters in production services: using PrepareContext, ExecContext, QueryContext, and BeginTx is necessary but driver support determines whether cancellation reaches the database while an operation is in progress.

Sources: src/database/sql/ctxutil.go, src/database/sql/driver/driver.go

HelperPreferred driver interfaceFallback behaviorImportant failure behavior
ctxDriverPreparedriver.ConnPrepareContextCalls Conn.PrepareCloses the statement and returns ctx.Err() if cancellation is observed after legacy prepare
ctxDriverExecdriver.ExecerContextConverts named values to values, checks context, calls Execer.ExecRejects named parameters when converting for legacy execution
ctxDriverQuerydriver.QueryerContextConverts named values to values, checks context, calls Queryer.QueryRejects named parameters when converting for legacy query
ctxDriverStmtExecdriver.StmtExecContextConverts arguments, checks context, calls Stmt.ExecReturns context error before legacy execution if already canceled
ctxDriverStmtQuerydriver.StmtQueryContextConverts arguments, checks context, calls Stmt.QueryReturns context error before legacy query if already canceled
ctxDriverBegindriver.ConnBeginTxCalls Conn.Begin for supported default optionsRejects unsupported isolation/read-only options; rolls back if cancellation is observed after legacy begin

Practical Usage Patterns

In application code, a repeated query normally looks like this shape: prepare once near the code that owns the operation, defer or otherwise arrange Close, and pass only argument values when calling the statement. Placeholder spelling is still owned by the database driver, so ?, $1, and named syntaxes are not interchangeable across drivers. The sql package's conversion layer can represent named values, but the fallback namedValueToValue path returns an error when named parameters reach a driver interface that does not support them. That is why driver documentation and tests should be consulted when choosing placeholder style.

Sources: src/database/sql/ctxutil.go, src/database/sql/convert.go

For transactions, structure the function so rollback is the default cleanup until commit succeeds. Begin the transaction with BeginTx when a request context, timeout, isolation level, or read-only mode matters. Execute statements through the Tx, not through the parent DB. If any prepare, exec, query, scan, or conversion step returns an error, roll back and return. If all operations succeed, call Commit and treat a commit error as meaning the transaction's results should not be trusted. This matches the official transaction guidance and the implementation's careful handling of begin-time cancellation and unsupported options.

A concise transaction pattern is: begin with a context, prepare transaction-scoped statements if they will be reused, execute all required operations, close rows and statements when finished, and commit only after every step has succeeded. If the context is canceled before a context-aware driver begins, executes, or queries, the driver can receive that cancellation directly. If the driver only supports legacy interfaces, the helpers still check the context before calling those legacy methods, or clean up immediately after successful legacy prepare or begin when cancellation is observed.

Sources: src/database/sql/ctxutil.go, src/database/sql/closemu.go

Implementation Details and Next Steps

The supplied files show that prepared statements and transactions are not isolated features; they sit on top of shared driver adaptation and conversion machinery. ctxutil.go controls which driver method is called for context-sensitive operations. convert.go controls how application arguments become driver arguments. driver.go and types.go define the capability interfaces and value contracts drivers implement. closemu.go explains why close coordination deserves a specialized synchronization primitive. internal/sql.go shows the package boundary pattern used when database/sql and database/sql/driver must share an internal representation without making it public API.

Sources: src/database/sql/ctxutil.go, src/database/sql/convert.go, src/database/sql/driver/driver.go, src/database/sql/driver/types.go, src/database/sql/closemu.go, src/database/sql/internal/sql.go

For your next pass, read the broader database/sql overview before designing connection ownership, then read the querying and changing data page for row scanning and result handling. Pair this page with cancellation and SQL injection guidance when adding request-scoped database work to a server. If you maintain a driver, focus on implementing the context-aware interfaces named in driver.go, plus NamedValueChecker for custom parameter types, so callers get the most complete behavior from prepared statements and transactions.