skip to content

sql.DB as a Pool

sql.Open validates the data source name and opens no connection; the first query does that. One *sql.DB is a concurrency-safe pool you create once and keep for the process lifetime.

part ofGo (Golang)overview, primer and where to startread it →
on this pageshow

questions

4

Does sql.Open actually connect to the database, and how do you prove connectivity?

level: juniorimportance: must knowfreq 72%

answer

  1. lazy by design
  2. you get a handle, not a connection
  3. a stopped server still returns nil error
  4. something else has to force the first dial
  5. PingContext, with a deadline

basics

~10 s

No. sql.Open only validates its arguments and hands back a connection-pool handle; it usually dials nothing. Call db.PingContext with a timeout to force a real connection and prove the database is reachable.

solid answer

~40 s

`sql.Open` is lazy. It looks the driver name up in `database/sql`'s registry, may let the driver parse the DSN, and returns a `*sql.DB` — a pool handle that has not yet opened a single connection. So a stopped server, a wrong host, a bad password and a missing database all produce `err == nil` here. Practically the only error `sql.Open` returns is an unknown driver name, which means the driver package was never imported for its registration side effect. To prove connectivity you have to make the pool dial: `db.PingContext(ctx)` with a `context.WithTimeout` deadline, so an unreachable host fails fast instead of hanging startup. A successful ping proves one connection worked at one moment — it is a startup and readiness check, not a guarantee for later queries.

code

go · 12 lines
go
db, err := sql.Open("mydriver", os.Getenv("DATABASE_DSN"))
if err != nil {
	// almost always an unregistered driver name, not a connectivity problem
	return err
}

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := db.PingContext(ctx); err != nil {
	// a dead host, a bad password or a missing database shows up here
	return err
}

go deeper

for a junior

Remember the one-liner: sql.Open gives you a pool handle and does not connect. Be ready to say that PingContext is what actually proves the database is reachable, and to show a timeout on it.

for a middle

Explain the mechanics: the driver name is looked up in a registry populated at init, the DSN may be parsed into a connector, and the first real dial happens when the pool needs a connection. Name what sql.Open's error really means.

for a senior

Show where the check belongs in a real service: bounded ping at startup, a readiness probe that touches the database, and no per-query pinging. Explain what a ping does and does not prove about later queries.

for a principal

Frame it as a failure-visibility choice: which dependencies a process must prove at boot versus degrade into, and whether an unreachable database should block startup or only mark the instance not-ready.

## What `sql.Open` actually is `database/sql` is a thin, driver-agnostic layer. `sql.Open(driverName, dataSourceName)` returns `(*sql.DB, error)`, and the crucial thing to understand is what a `*sql.DB` is: **not a connection**, but a handle to a pool of connections that the package will open, reuse and retire on your behalf. Creating that handle requires no network activity at all, so `sql.Open` performs none. Concretely, `sql.Open` does roughly three things: 1. Looks `driverName` up in the package-level registry that drivers populate by calling `sql.Register(name, driver)` from their `init` function. If nothing registered that name you get an error along the lines of `sql: unknown driver "..."` — the classic symptom of a missing blank import of the driver package. 2. Gives the driver a chance to turn the DSN string into a reusable connector object, so the string is parsed once rather than per connection. Drivers that parse eagerly can report a malformed DSN here; many do not. 3. Allocates the pool bookkeeping and returns. No TCP dial, no TLS handshake, no authentication. The first connection is opened lazily, when something actually needs one. ## Why that surprises people Every other language's `connect()` you have used probably dialled immediately, so the instinct is to write: ```go db, err := sql.Open("mydriver", dsn) if err != nil { log.Fatal(err) // "the database is fine" } ``` and believe the database is fine. It is not: the server can be switched off, the credentials wrong, the DSN pointed at a database that does not exist, and this code prints nothing. The failure then surfaces much later and somewhere less convenient — inside the first request handler, as a query error that looks like an application bug. This is also why a health or readiness endpoint that only checks "did the process start" will happily report success against a dead database. The process genuinely did start; it just never touched the server. ## Proving connectivity The explicit way to force the pool to open a connection is `Ping`: ```go ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() if err := db.PingContext(ctx); err != nil { return fmt.Errorf("database unreachable: %w", err) } ``` `PingContext` takes a connection from the pool — opening one if there are no idle connections — and asks the driver to verify it is alive, returning the connection afterwards. Prefer it over the context-free `Ping()`: without a deadline, a host that black-holes packets can leave startup hanging until the operating system's TCP timeout, which may be minutes. What a successful ping proves is narrow but real: at this instant, one connection could be established and authenticated against that DSN. It does not prove the schema is what you expect, that the account has the privileges your queries need, or that the next connection will succeed. If you want stronger evidence, run a cheap real query instead — for example a `SELECT` of a constant, or one row from a table you actually depend on. ## Where to put the check - **At startup**, immediately after `sql.Open`, with a bounded deadline. For a short-lived process this is what turns "silently did nothing" into a visible, non-zero exit. - **In a readiness check**, so an orchestrator stops sending traffic while the dependency is down. Give each ping its own short deadline derived from the incoming request's context. - **Not on every query.** Pinging before each statement doubles your round trips and still races: the connection can die between the ping and the query. `database/sql` already retries a query on a fresh connection when a pooled one turns out to be dead, so defensive pinging buys you very little. ## Handling `sql.Open`'s error anyway Because it fires so rarely, `sql.Open`'s error is often ignored or, worse, discarded with `_`. Don't: it is exactly the signal that tells you the binary was built without the driver linked in, which is a build-time mistake worth failing loudly on. The two errors are complementary — `sql.Open`'s error means "this program cannot talk to this kind of database at all", and `PingContext`'s error means "this program cannot reach this particular database right now".

  • If sql.Open almost never fails, what errors can it actually return?
    Mainly an unregistered driver name — the driver package was not imported for its side effect, so nothing called sql.Register for it. Some drivers also parse the DSN eagerly and reject malformed syntax at this point, but that is driver-dependent and not something you can rely on.
  • Why prefer db.PingContext over db.Ping at startup?
    PingContext takes a context, so you can attach a deadline. Without one, a host that silently drops packets leaves the dial hanging until the operating system gives up, which can be minutes of a startup that looks frozen. With a five-second timeout you get a clear, fast failure you can log and act on.
  • Does a successful ping mean later queries will succeed?
    No. It proves that one connection could be opened and authenticated at that moment. The server can go away a second later, the account may lack privileges for your actual statements, and the schema may not match. Treat it as a reachability check, not a correctness or availability guarantee.

saying these in an interview costs you the question

  • Says sql.Open dials the server and errors if it is down
  • Discards sql.Open's error with the blank identifier
  • Calls Ping with no deadline and hangs startup
  • Pings before every query as a safety measure
  • Treats one successful ping as a lasting guarantee
open as a page

Why should one *sql.DB be shared across goroutines instead of opened per request?

level: middleimportance: should knowfreq 60%

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.

open as a page

Why does a database/sql service start up healthy when its database is down, and how do you fix readiness?

level: seniorimportance: should knowfreq 45%

basics

~20 s

Because sql.Open never contacts the server, a process starts and reports ready with an unreachable database. Make the readiness check call db.PingContext with a short deadline, and keep liveness separate so a database blip does not restart the process.

open as a page

When would you use sql.OpenDB with a driver.Connector rather than sql.Open with a DSN?

level: middleimportance: nice to knowfreq 25%

basics

~20 s

sql.OpenDB takes a driver.Connector, an object that dials on demand, so it needs no registered driver name and no DSN string. Reach for it when connection setup must be typed, wrapped, or must mint a fresh credential per connection.

open as a page