skip to content

Relational Access

database/sql as it actually behaves: sql.Open connects to nothing, Rows must be closed and checked, a Tx pins one connection, and the pool limits decide your tail latency.

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

explore

questions

page 1 of 2

Why does scanning a SQL NULL into a Go string variable fail in database/sql, and what do you scan into instead?

level: juniorimportance: must knowfreq 70%

answer

  1. the zero value is not absence
  2. the driver reports NULL as nil
  3. one destination carries a bool alongside
  4. the other destination can simply be nil
  5. sql.NullString.Valid, or a *string target

basics

~20 s

A Go string has no value that means absent, so Scan returns an error saying converting NULL to string is unsupported. Declare the variable as sql.NullString or as *string, then check Valid or check for nil before using it.

solid answer

~50 s

SQL NULL means "no value", and a Go `string` cannot represent that: `""` is an ordinary value a column can also hold. When the driver reports NULL, `database/sql` hands `Scan` an untyped `nil`, and converting that into a `*string` destination fails with an error reading `converting NULL to string is unsupported` — no panic and no silent zero value. The two standard destinations are `sql.NullString`, a struct of `String string` plus `Valid bool` that implements `sql.Scanner`, and a pointer destination such as `var email *string` passed as `&email`, which `Scan` sets to `nil` for NULL and to a freshly allocated string otherwise. Both work in the other direction too: an invalid `sql.NullString` or a nil `*string` is written back as NULL. If the caller genuinely does not need to tell NULL from the empty string, `COALESCE` the column in the query instead and keep the plain Go type.

code

go · 16 lines
go
var (
	status sql.NullString // Valid is false when the column was NULL
	note   *string        // nil when the column was NULL
)

// row is a *sql.Row for one legacy customer.
if err := row.Scan(&status, &note); err != nil {
	return err
}

if status.Valid {
	fmt.Println("status:", status.String)
}
if note != nil {
	fmt.Println("note:", *note)
}

go deeper

for a junior

Be ready to say plainly that a Go string cannot hold "no value", that Scan returns an error rather than storing "", and to name both fixes: sql.NullString with its Valid field, or a *string that comes back nil.

for a middle

Explain the mechanics: the driver reports NULL as an untyped nil, and only Scanner implementations, pointer-to-pointer, *any, *[]byte and *sql.RawBytes destinations accept it. Show that the same types write NULL back.

for a senior

Show how you stop it reaching production: derive nullability from the schema or ColumnType.Nullable rather than from observed rows, and decide per column whether NULL is modelled in Go or collapsed in SQL.

for a principal

Own the convention. Decide whether nullable columns are represented once in a shared row-mapping layer or ad hoc per query, and how much of a legacy schema's nullability you are willing to carry into every downstream type.

## What NULL is by the time it reaches Go In SQL, a column value may be NULL — a marker meaning "there is no value here". Go's basic types have no such state. A `string` is always some string, and the closest candidate, `""`, is a perfectly ordinary value that the column can also legitimately hold. An `int64` is always some integer, and `0` is likewise a real value. So the moment a nullable column meets a plain Go variable, something has to give. ## What Scan actually does `Scan` takes `...any`, and each argument must be a **pointer to** the destination variable. Internally it inspects the value the driver produced for that column and the concrete type of the destination, and converts one into the other. Drivers report a NULL column as an untyped `nil`. For a `nil` source, `database/sql` accepts only a small set of destinations: - anything implementing `sql.Scanner` (which is how the `sql.Null*` types work), - a pointer-to-pointer such as `**string`, which it sets to `nil`, - `*any`, `*[]byte` and `*sql.RawBytes`, each of which becomes `nil`. Everything else — `*string`, `*int64`, `*bool`, `*time.Time` — produces an error whose core text is `converting NULL to string is unsupported`, wrapped with the column index and name. Note what does **not** happen: it does not panic, and it does not quietly store the zero value. It returns an error, and code that ignores `Scan`'s error turns this into a wrong-looking row instead of a loud one. ## Fix one: the sql.Null* family `sql.NullString` is a two-field struct: - `String string` — the value, meaningful only when `Valid` is true; - `Valid bool` — false exactly when the column was NULL. When `Valid` is false, `String` is `""`, so you must branch on `Valid`, never on the value. The same shape exists for the other column types: `sql.NullInt64`, `sql.NullInt32`, `sql.NullInt16`, `sql.NullByte`, `sql.NullFloat64`, `sql.NullBool` and `sql.NullTime`, and recent Go adds a generic `sql.Null[T]` with fields `V T` and `Valid bool` so you are not limited to the pre-declared set. Each of these implements `sql.Scanner`, so it is a legal `Scan` destination, and `driver.Valuer`, so passing one as a query argument writes NULL when `Valid` is false. ## Fix two: a pointer destination Declare the variable as a pointer and pass its address: ```go var email *string err := row.Scan(&email) ``` For NULL, `Scan` zeroes the pointer, so `email == nil`. For a non-NULL value it allocates a fresh `string` and points at it. The check is the idiomatic `if email != nil`, there is no extra struct in your model, and a nil pointer passed as an argument is written back as NULL. The costs are an allocation per non-NULL row and the ordinary risk of dereferencing a nil pointer somewhere downstream. ## Fix three: remove the NULL in SQL If the caller truly cannot act on the difference — a display-only description, say — `SELECT COALESCE(note, '') FROM ...` lets you keep a plain `string` destination. This is a deliberate loss of information: after it, no Go code can tell a NULL from an empty string. Make that choice consciously rather than by accident. ## Where this bites in practice The classic failure is an internal back-office page over a long-lived table whose columns were declared nullable years ago. Every row anyone has looked at has a value, the code scans into plain `string` fields, and it works for a year — until the one legacy row that predates the column is opened and the handler returns an error. Nothing changed in the code; the data finally exercised the schema. The defence is to treat nullability as a property of the **column**, not of the rows you happen to have seen. Read the DDL, or ask the result set: `rows.ColumnTypes()` returns `[]*sql.ColumnType`, and `ColumnType.Nullable()` returns `(nullable, ok bool)` — where `ok` is false when the driver cannot report it, which is itself worth knowing before you trust the answer. Anywhere the schema says nullable, the destination must be able to hold "no value". ## Writing NULL back The symmetry matters: `sql.NullString{}` (with `Valid` false) and a nil `*string` both become NULL as query arguments, whereas `sql.NullString{String: "", Valid: true}` and a pointer to `""` both insert an empty string. Using the same type in both directions is what keeps a round trip faithful.

  • How do you write a NULL back into that column from Go?
    Pass a destination that can express absence. An `sql.NullString` with `Valid` false, or a nil `*string`, both become NULL as a query argument, because both types implement `driver.Valuer` and return `nil` for the missing case. A `sql.NullString{String: "", Valid: true}` or a pointer to `""` inserts an empty string instead — the same distinction as on the read side.
  • How do you find out which columns of an existing table can actually be NULL?
    Read the schema — the DDL is the authority. At runtime you can also ask the result set: `rows.ColumnTypes()` gives `[]*sql.ColumnType`, and `ColumnType.Nullable()` reports `(nullable, ok bool)`. Treat `ok == false` as "the driver does not know" rather than "not nullable". Never infer nullability from the rows you happen to have seen; one legacy row is enough to break the assumption.
  • If Scan returns that error, why is it still dangerous to ignore it?
    Because the destination is left holding whatever it held before — typically the previous row's value in a loop, or the zero value. Ignoring the error turns a loud, specific failure into a plausible-looking wrong row that will be believed. `Scan`'s error is the only signal that the conversion did not happen.

saying these in an interview costs you the question

  • Claims a NULL column just scans as the empty string
  • Says Scan panics on NULL rather than returning an error
  • Checks NullString.String == "" instead of Valid
  • Assumes a column is not nullable because no NULL has appeared yet
  • Ignores Scan's error and reads the destination anyway
open as a page

In Go's database/sql, why pass values as arguments after the query string instead of fmt.Sprintf-ing them into the SQL?

level: juniorimportance: must knowfreq 82%

basics

~20 s

database/sql never splices arguments into the SQL text. The statement and the values reach the driver separately, so the database parses the query before it sees any value. Anything you fmt.Sprintf in becomes SQL syntax rather than data.

open as a page

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

level: juniorimportance: must knowfreq 72%

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.

open as a page

In Go's database/sql, what does db.PrepareContext return and why must you Close it?

level: juniorimportance: must knowfreq 52%

basics

~20 s

It returns a *sql.Stmt, a handle to a statement the database server has already parsed and is holding open. Stmt.Close discards it on every pooled connection it was prepared on; skip that Close and the server-side state stays.

open as a page

In Go's database/sql, how do you run a multi-row query and read every row?

level: juniorimportance: must knowfreq 80%

basics

~10 s

Call db.QueryContext, defer rows.Close(), then loop while rows.Next() calling rows.Scan with a pointer per selected column, and check rows.Err() after the loop. Next returning false means end of data or a failure.

open as a page

What does db.SetMaxOpenConns(25) do to a *sql.DB, and what happens when all 25 connections are busy?

level: juniorimportance: must knowfreq 60%

basics

~20 s

SetMaxOpenConns caps the total connections a *sql.DB may have open at once, in-use and idle together. Once the cap is reached the next query blocks until a connection is returned rather than opening another. The default is unlimited.

open as a page

Why is a deferred tx.Rollback() safe right after db.BeginTx even when the transaction commits?

level: juniorimportance: must knowfreq 68%

basics

~10 s

Rollback on an already-committed *sql.Tx does nothing: it returns sql.ErrTxDone and leaves the commit intact. The deferred call exists as a safety net for early returns and panics, so its error is deliberately ignored.

open as a page

Why does sql.DB.QueryRowContext return no error, and where does a failed query surface?

level: middleimportance: must knowfreq 70%

basics

~20 s

QueryRowContext returns only a *sql.Row so the call can be chained straight into Scan; the query's error is stored and surfaces from Row.Scan. Scan returns sql.ErrNoRows when nothing matched, which callers must handle separately from a real failure.

open as a page

How do SetMaxIdleConns and SetMaxOpenConns differ on a *sql.DB, and what does a low idle limit cost?

level: middleimportance: must knowfreq 52%

basics

~20 s

SetMaxOpenConns caps how many connections may exist at once; SetMaxIdleConns caps how many the pool keeps when they fall idle. With the idle limit far below the open limit, connections are closed and reopened constantly under load, paying a fresh handshake each time.

open as a page

When do you scan a nullable column into sql.NullString, into a *string, or COALESCE it away in SQL?

level: middleimportance: should knowfreq 48%

basics

~20 s

Choose by whether the caller must tell NULL from the zero value. If it must, use sql.NullString for an explicit Valid flag or a *string for a nil check. If not, COALESCE in the query and keep a plain string.

open as a page

How do you implement sql.Scanner and driver.Valuer so a custom type round-trips a nullable column?

level: middleimportance: should knowfreq 42%

basics

~20 s

Implement Scan(src any) error on a pointer receiver, type-switching over the driver's values and treating a nil src as NULL. Implement Value() (driver.Value, error) on a value receiver, returning nil for NULL or one of driver.Value's types otherwise.

open as a page

Why is the placeholder syntax in database/sql driver-specific — ? for some databases and $1 for others?

level: middleimportance: should knowfreq 58%

basics

~20 s

database/sql passes the query text to the driver unchanged and has no SQL parser, so the placeholder token is the database's own syntax: ? for MySQL and SQLite, $1 for PostgreSQL, :name or @name elsewhere.

open as a page

How do you bind a variable-length list of ids to a SQL IN clause with database/sql?

level: middleimportance: should knowfreq 52%

basics

~20 s

Generate one placeholder token per element, join them into the IN list, and pass the ids as a []any spread with args.... The text holds only generated tokens, never values, and an empty list needs its own branch.

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

In database/sql, what happens when a *sql.Stmt is executed on a connection it was not prepared on?

level: middleimportance: should knowfreq 40%

basics

~20 s

database/sql prepares it again on that connection. A *sql.Stmt from a *sql.DB is not bound to one connection: it tracks which connections it has been prepared on and quietly adds each new one it is handed.

open as a page

Why does a *sql.Stmt prepared on *sql.DB not run inside your *sql.Tx, and what does tx.Stmt do?

level: middleimportance: should knowfreq 34%

basics

~20 s

A *sql.Tx owns one connection for its whole life, while a statement prepared on the *sql.DB runs on whatever connection the pool hands it. So its work happens outside the transaction. tx.Stmt returns a copy bound to the transaction's connection.

open as a page

After an UPDATE via sql.DB.ExecContext, how do you tell the caller no row matched?

level: middleimportance: should knowfreq 52%

basics

~10 s

sql.DB.ExecContext returns a sql.Result; call Result.RowsAffected() and treat a count of zero as no matching row. It returns an error as well, because not every driver can report a count.

open as a page

Why must statements inside a *sql.Tx go through the Tx rather than the *sql.DB?

level: middleimportance: should knowfreq 55%

basics

~20 s

A *sql.Tx owns one dedicated connection, and the database transaction lives on that connection. A statement issued on the *sql.DB takes a different connection from the pool, so it runs outside the transaction and commits on its own.

open as a page

Why can't ORDER BY use a SQL placeholder for a user-chosen sort column, and what replaces it?

level: seniorimportance: should knowfreq 45%

basics

~20 s

Placeholders bind values, while an identifier decides what the statement means and must be fixed before parsing. Replace it with a lookup from a request key to a column fragment written in your own source.

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

An HTTP handler sums a column over rows from db.QueryContext and its total comes back short by a few rows on some requests, with no error logged. How do you find the cause?

level: seniorimportance: should knowfreq 45%

basics

~10 s

The loop almost certainly ignores rows.Err(). rows.Next() returns false both at the end of data and on a mid-iteration failure, so a truncated result set looks complete; a swallowed Scan error does the same.

open as a page

A Go worker pool's db.QueryContext calls return context deadline exceeded while the database logs no slow queries — how do you prove the connection pool is the bottleneck?

level: seniorimportance: should knowfreq 46%

basics

~20 s

Sample db.Stats over time. Rising WaitCount with WaitDuration divided by WaitCount approaching the deadline, InUse pinned at the open limit and Idle at zero, proves callers are timing out while queueing for a connection rather than while running a query.

open as a page

When a helper wraps work in a *sql.Tx, how do you avoid losing the Commit or Rollback error?

level: seniorimportance: should knowfreq 42%

basics

~20 s

Return tx.Commit()'s error to the caller rather than discarding it, and give the wrapper a named result so its deferred rollback can see the failure it is undoing and attach a genuine rollback error, ignoring only sql.ErrTxDone.

open as a page

How do you set SetMaxOpenConns across a service's replicas when the database's connection budget is shared with the rest of the fleet?

level: principalimportance: should knowfreq 36%

basics

~20 s

Treat the cap as a share of a granted budget, not a local tuning knob: the database sees your cap multiplied by your maximum replica count, plus jobs and migrations. Decide that total with whoever owns the database, then choose what callers experience when it is reached.

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

What do SetConnMaxLifetime and SetConnMaxIdleTime cap on a *sql.DB, and why set either one?

level: middleimportance: nice to knowfreq 38%

basics

~20 s

SetConnMaxLifetime caps a connection's total age since it was created; SetConnMaxIdleTime caps how long it may sit unused. Both default to no limit. Lifetime retires connections before something outside Go kills them; idle time lets the pool shrink after a burst.

open as a page

What happens to an open transaction when the context passed to db.BeginTx is cancelled?

level: middleimportance: nice to knowfreq 40%

basics

~20 s

database/sql watches that context for the life of the transaction and rolls it back automatically when the context is done. A later tx.Commit() then fails, returning the context's error or sql.ErrTxDone once the rollback has landed.

open as a page

What does scanning a column into sql.RawBytes give you, and what rules must you follow to use it safely?

level: seniorimportance: nice to knowfreq 25%

basics

~20 s

sql.RawBytes aliases memory owned by the driver, so Scan skips the copy a *[]byte or *string destination makes. The bytes stay valid only until the next Next, Scan or Close call, must not be mutated, and are nil for NULL.

open as a page

A Go reindexer makes the server's prepared-statement count climb all day — how do you find and fix it?

level: seniorimportance: nice to knowfreq 22%

basics

~20 s

Usually a *sql.Stmt prepared inside the row loop and never closed, leaving live state on a pooled connection each iteration. Group the server's prepared statements by session and text, then hoist the prepare out of the loop and close it.

open as a page

In database/sql, how do you Scan rows whose column list is only known at runtime?

level: seniorimportance: nice to knowfreq 24%

basics

~10 s

Call rows.Columns() for the names, allocate a []any of values plus a second []any holding a pointer to each value, and pass that second slice to rows.Scan with a spread. Type-switch on what lands.

open as a page

showing 1–30 of 31