Why should one *sql.DB be shared across goroutines instead of opened per request?
answer
- the handle is not a connection
- one per database, built at startup
- documented safe for concurrent use
- opening per request discards warm connections
- closing is a whole-program decision
basics
~20 s*sql.DB is a connection-pool handle, not a connection: it is safe for concurrent use and reuses idle connections. Create one per database at startup, share it everywhere, and close it only when the program is done with it.
solid answer
~50 sA `*sql.DB` is a pool, not a connection. It is explicitly safe for concurrent use by multiple goroutines, does its own locking, and hands each in-flight query its own underlying connection, opening new ones or reusing idle ones as demand moves. So the normal shape is: build one `*sql.DB` per database at startup, inject it into whatever needs to query, and let every goroutine use it directly. Calling `sql.Open` per request instead creates a brand-new pool each time, which throws away every warm connection and pays a fresh TCP plus authentication handshake for one statement — and if you forget `db.Close()` on that per-request handle, its connections leak until the server or the operating system reclaims them. `db.Close()` is a whole-program operation: a long-running server may never call it, while a short-lived process should close before exit so connections are torn down cleanly.
code
go · 18 linestype Store struct {
db *sql.DB // safe to use from many goroutines at once
}
func NewStore(db *sql.DB) *Store {
return &Store{db: db}
}
// Anti-pattern: a fresh pool per call, so every call pays a new handshake
// and a forgotten Close leaks its connections.
func badLookup(dsn string) error {
db, err := sql.Open("mydriver", dsn)
if err != nil {
return err
}
defer db.Close()
return db.Ping()
}go deeper
Know the rule and the reason: create the *sql.DB once, hand it around, and never open one per request. Be ready to say that the type is safe to use from many goroutines at once.
Explain the mechanics: each statement borrows an underlying connection for its duration, the handle's own locking makes that safe, and a per-request handle means a per-request pool with a fresh handshake and leaked connections.
Show the lifetime judgment: where the handle is constructed, how it is injected, when Close belongs in a graceful shutdown, and why a short-lived scheduled process should still keep one pool for its whole run.
Own the boundary question: whether teams get a shared handle behind a data-access package or construct their own, and what that decides about connection budgets and consistent configuration across services hitting one database.
## `*sql.DB` is a pool handle The single most useful sentence about `database/sql` is that `*sql.DB` is **not** a database connection. It is a handle to a managed pool of connections. Behind it sit zero or more real connections to the server, some idle, some in use, which the package opens on demand and retires according to its configuration. The type is documented as safe for concurrent use by multiple goroutines, and it maintains its own internal synchronisation, so nothing you do at the call site needs a mutex. When you run a statement, `database/sql` acquires a free connection from the pool for the duration of that operation and returns it afterwards. Two goroutines running two statements at the same time therefore use two different underlying connections — sharing the handle does not serialise them. ## Consequences for how you wire a program Because the handle is the pool, it should be created once and shared: ```go db, err := sql.Open("mydriver", os.Getenv("DATABASE_DSN")) if err != nil { return err } store := NewStore(db) // and everything that queries takes *Store or *sql.DB ``` Pass it as a dependency — a constructor parameter or a field on the type that owns your queries. A package-level variable works but makes tests and multiple databases awkward; the pattern that scales is explicit injection. The anti-pattern is calling `sql.Open` where you need a query — inside an HTTP handler, inside a loop, inside a helper function. That looks harmless because `sql.Open` is cheap and returns instantly, which is exactly the trap: it is cheap because it does nothing yet. Each call produces a *separate pool*, so: - The first query on each new handle pays a full TCP connect, TLS negotiation and authentication round trip. Under load this dominates your latency and hammers the server's connection-accept path. - Nothing is reused. Warm connections built by earlier requests belong to pools you have already dropped. - Every server has a hard limit on concurrent connections, and a per-request pool is the fastest way to reach it. - If the per-request handle is not closed, its connections stay open. The handle becoming garbage does not close them promptly, so you leak sockets on both sides until the server's idle timeout or the operating system intervenes. ## When `db.Close()` is right `Close` closes the handle and releases all of its connections, and it is deliberately rare. It is a statement about the whole program's relationship with that database, not about one query. Two shapes: - **A long-running server.** The pool lives as long as the process. Many services never call `Close` at all, because process exit tears the sockets down anyway. Calling it during graceful shutdown, after the HTTP server has stopped accepting and in-flight work has drained, is tidy and harmless — closing while queries are still running is not, since those queries will fail. - **A short-lived process** — a scheduled job that starts, does a few minutes' work and exits, perhaps every minute. Here it is worth deferring `Close` in `main`, so the connections are shut down in an orderly way rather than being severed by process exit. It also documents the lifetime. What you must *not* do is treat that process's pool as disposable per unit of work: even a job that runs for two seconds benefits from one pool for its whole run. One related mistake: `Close` is not how you return a connection after a query. Result sets do that — a `*sql.Rows` holds its connection until it is exhausted or closed. Closing the `*sql.DB` to "free the connection" destroys the pool for everyone. ## What sharing does not give you Sharing one handle does not give you a stable *session*. Successive statements may land on different underlying connections, so anything a database keeps per session — temporary tables, session variables, advisory locks, `SET`-style settings — is not reliably visible to the next statement. When you genuinely need several statements on the same physical connection, `database/sql` gives you `db.Conn(ctx)`, which reserves one and returns it to the pool when you close it. Finally, sharing is per database, not per package: two different databases mean two `*sql.DB` values, each with its own pool and its own lifetime.
- Does sharing one *sql.DB force queries from different goroutines to run one at a time?No. Each in-flight statement borrows its own underlying connection, so concurrent goroutines run concurrently. The handle's locking protects the pool's bookkeeping, not your queries. Concurrency is bounded by how many connections the pool is allowed to hold, not by the fact that everyone shares one handle.
- If several statements must run on the same physical connection, what do you use?db.Conn(ctx) reserves a single connection from the pool and returns a *sql.Conn you use for those statements, releasing it when you close it. That is what you need for session-scoped state such as temporary tables, session settings or advisory locks, which are not reliably visible across different pooled connections.
- Does letting a *sql.DB go out of scope close its connections?Not promptly or predictably. Until Close is called the pool keeps its connections open, and relying on garbage collection to tidy up leaves sockets held on both sides. That is exactly why a per-request sql.Open without a matching Close shows up as a growing connection count on the server.
saying these in an interview costs you the question
- Calls sql.Open inside every request handler
- Thinks *sql.DB is a single connection needing a mutex
- Calls db.Close after each query to free the connection
- Gives every goroutine its own *sql.DB
- Assumes session state survives between statements