skip to content

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

level: middleimportance: nice to knowfreq 25%

answer

  1. one takes a string, one takes an object
  2. no registry lookup, no DSN parsing
  3. notice the missing error result
  4. called again for every new connection
  5. credentials that expire every few minutes

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.

solid answer

~50 s

`sql.Open` is the string-based door: it looks a driver name up in `database/sql`'s global registry and hands the driver an opaque DSN to parse. `sql.OpenDB(connector)` is the typed door: it takes a `driver.Connector` — an interface with `Connect(context.Context) (driver.Conn, error)` and `Driver() driver.Driver` — and returns a `*sql.DB` with no error at all, because there is nothing to look up and nothing to parse. Three reasons to prefer it. First, configuration becomes typed values instead of a stringly-typed DSN you assemble and escape. Second, no global registration is required, so you avoid the blank import and any name collision. Third and most useful, the pool calls `Connect` for every new connection it opens, so a connector can fetch a short-lived credential each time rather than embedding a static password — and you can wrap a driver's own connector to add logging or instrumentation around connection creation.

code

go · 8 lines
go
// driver.Connector, the interface sql.OpenDB accepts:
//	Connect(context.Context) (driver.Conn, error)
//	Driver() driver.Driver

// Connect is called for each new pooled connection, so a connector can
// mint a fresh short-lived credential instead of embedding a password.
db := sql.OpenDB(connector)
defer db.Close()

go deeper

for a junior

Know that there are two ways to build a *sql.DB: a driver name plus a DSN string, or a connector object. Being able to say sql.OpenDB exists and takes a connector is enough at this level.

for a middle

Explain the interface: Connect plus Driver, no registry lookup, no parsing, and therefore no error result. Say when Connect is called and why that timing is the interesting part.

for a senior

Make the operational case: rotating short-lived credentials, keeping secrets out of long-lived strings, wrapping connection creation for instrumentation, and why most services are still fine with a DSN.

for a principal

Own the standard: whether services in your estate authenticate to databases with static DSNs or with connectors that fetch credentials per connection, and what that costs in driver portability and shared tooling.

## Two doors into the same pool `database/sql` gives you two constructors for a `*sql.DB`: - `sql.Open(driverName, dataSourceName string) (*sql.DB, error)` - `sql.OpenDB(c driver.Connector) *sql.DB` They produce the same kind of pool. The difference is entirely in how the pool learns to make a connection. `sql.Open` is *name plus string*. The name is a key into a package-level registry that drivers populate at init time by calling `sql.Register(name, driver)` — which is what the blank import of a driver package exists to trigger. The string is opaque to `database/sql`; only the driver understands it. That is why `sql.Open` returns an error: the name may not be registered, and the driver may reject the string. `sql.OpenDB` is *object*. You hand it something that already knows how to dial: ```go type Connector interface { Connect(context.Context) (driver.Conn, error) Driver() driver.Driver } ``` There is no registry lookup and no parsing, so there is no error to return — the signature has one result. Under the hood the two paths converge: when a driver implements `driver.DriverContext`, `sql.Open` asks it for a connector via `OpenConnector(dsn)` and then builds the pool from it, exactly as `OpenDB` would. ## Why the connector form is worth reaching for **Credentials that rotate.** This is the strongest reason. `Connect` is called by the pool every time it opens a *new* connection — at startup, when demand grows, and after old connections are retired. A DSN, by contrast, is fixed at open time. If your database authenticates with a short-lived token that expires every few minutes, a static DSN gives you a pool that works until the token dies and then fails on every new connection. A connector can obtain a fresh token inside `Connect`, so every new connection is authenticated with a currently valid credential and the rotation is invisible to callers. It also keeps the secret out of a long-lived string that tends to end up in logs and crash dumps. **Typed configuration.** A DSN is a mini-language: host, port, timeouts, TLS mode and pool hints all crammed into a string with its own escaping rules, validated only at runtime by the driver. A connector is usually built from a driver-specific config struct, so mistakes are compile-time or at least clearly located, and composing configuration from environment variables stops being string concatenation. **No global registration.** `sql.Register` panics if the same name is registered twice, and the registry is process-global — a real annoyance if you want two builds of the same driver, or a wrapped version, side by side. `OpenDB` sidesteps the registry entirely; nothing needs a name. **Wrapping.** Because `Connector` is a small interface, you can implement it yourself around a driver's connector to add behaviour at connection-creation time: emit a metric, apply your own dial deadline, log which replica was chosen, or run a fixed setup statement on each new connection before handing it over. ## When the DSN form is still right Most of the time. `sql.Open` is one line, every driver supports it, the DSN travels naturally in configuration and secret stores, and the connector interface is only exposed by drivers that choose to export a connector type or a constructor for one. If your credentials are static and your configuration is a URL from the environment, the string form is the simpler, more portable choice — and the pool you get is identical. ## The parts that do not change Whichever door you use, the result is a `*sql.DB`: a pool handle, safe for concurrent use, that dials lazily. `OpenDB` does not connect either — like `sql.Open`, it returns before any network work happens, so you still prove reachability with a bounded `PingContext`. And you still call `Close` on the handle only when the program is finished with that database.

  • What does the blank import of a driver package have to do with sql.Open, and does sql.OpenDB need it?
    The blank import runs the driver package's init, which calls sql.Register to put its name into database/sql's global registry. sql.Open resolves its first argument there, and without the import you get an unknown-driver error. sql.OpenDB is handed the connector directly, so no registration and no blank import is required.
  • Why does sql.OpenDB return no error when sql.Open does?
    sql.Open can fail in two ways that OpenDB cannot: the driver name may not be registered, and the driver may reject the DSN it is asked to parse. OpenDB receives an already-constructed connector, so there is nothing left to resolve or validate. Failures move to Connect, which the pool calls when it actually dials.
  • Does sql.OpenDB connect any earlier than sql.Open does?
    No. Both return a pool handle that has opened nothing. The connector's Connect is called the first time the pool needs a connection, not when OpenDB returns, so you still need a bounded PingContext or a real query to prove the database is reachable.

saying these in an interview costs you the question

  • Thinks sql.OpenDB opens a connection immediately
  • Expects sql.OpenDB to return an error like sql.Open
  • Believes a connector is consulted only once
  • Bakes an expiring auth token into a static DSN
  • Thinks OpenDB still requires the driver to be registered