skip to content

Query, Scan and Rows

Query hands back Rows you must both Close and check Err on, QueryRow hides its error until Scan, and a leaked Rows keeps a pooled connection checked out until the process ends.

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

questions

5

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

level: juniorimportance: must knowfreq 80%

answer

  1. four calls, always the same four
  2. the result set is holding a connection
  3. defer it right after the error check
  4. Scan wants addresses, not values
  5. false from Next does not mean success

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.

solid answer

~40 s

The read path is four calls. `db.QueryContext(ctx, query, args...)` returns a `*sql.Rows` plus an error for the query itself; if that error is nil you immediately `defer rows.Close()`, because the `*sql.Rows` is holding a connection checked out of the pool. Then `for rows.Next() { ... }` advances the cursor, and inside the loop `rows.Scan(&a, &b)` copies the current row's columns into the pointers you pass — one destination per selected column, in select-list order. `Next` returning `false` is ambiguous: it means either the result set is exhausted or iteration failed part-way, so after the loop you must check `rows.Err()` and return it. Skipping that check is how a truncated result set is mistaken for a complete one.

code

go · 17 lines
go
func listEmails(ctx context.Context, db *sql.DB, orgID int64) ([]string, error) {
	rows, err := db.QueryContext(ctx, "SELECT email FROM users WHERE org_id = ?", orgID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	var emails []string
	for rows.Next() {
		var email string
		if err := rows.Scan(&email); err != nil {
			return nil, err
		}
		emails = append(emails, email)
	}
	return emails, rows.Err()
}

go deeper

for a junior

Be ready to write the loop from memory: QueryContext, error check, defer rows.Close, for rows.Next, Scan into pointers, rows.Err after the loop. Say out loud that Scan needs addresses and one destination per selected column.

for a middle

Explain the mechanics: the result set holds a pooled connection until it is exhausted or closed, Next returns false for both end-of-data and failure, and Close is idempotent so the unconditional defer is safe.

for a senior

Show the production consequence. Say which failure each missing piece produces — a leaked connection, a runtime Scan error, or a silently short result — and describe how you would catch the last one in review or in an integration test against a real driver.

for a principal

Own the convention. Decide whether reads go through a small shared helper that encapsulates the loop so no team can forget rows.Err, and be able to argue that cost against handwritten loops that are easier to read but easy to get subtly wrong.

## The shape of a read in `database/sql` Go's standard library has no ORM and no row-mapping magic. Reading many rows is always the same four calls, and an interviewer asking this wants to see all four plus the reason each exists. ``` rows, err := db.QueryContext(ctx, "SELECT id, email FROM users WHERE org_id = ?", orgID) if err != nil { return nil, err } defer rows.Close() for rows.Next() { var id int64 var email string if err := rows.Scan(&id, &email); err != nil { return nil, err } // use id, email } return out, rows.Err() ``` ### `QueryContext` — the query, and a checked-out connection `(*sql.DB).QueryContext(ctx, query, args ...any) (*sql.Rows, error)` takes a connection from the pool, sends the statement, and hands you a cursor over the result. The returned error covers only *starting* the query: a syntax error, a dead connection, a cancelled context. Once it returns non-nil rows, a connection is **out of the pool and stays out** until the result set is finished or closed. That single fact explains most of the rules below. `ctx` matters: it is the query's lifetime. If the context is cancelled — a client disconnecting from an HTTP handler that passed `r.Context()`, say — the driver is told to cancel, the rows are closed, and iteration stops. ### `defer rows.Close()` — release the connection on every exit `(*sql.Rows).Close()` releases the connection back to the pool. It is idempotent and safe to call after the result set is already exhausted, which is precisely why the idiomatic form is a `defer` placed immediately after the error check. `Next` does auto-close the rows when it returns `false` at a clean end of data, so on the happy path the deferred `Close` is a no-op — but the happy path is not the one you are defending against. An early `return` from inside the loop, a `break`, or a panic all leave the cursor open, and without the `defer` that connection never goes back to the pool. Enough of those and every subsequent query blocks waiting for a free connection. Note the ordering trap: `defer rows.Close()` must come **after** `if err != nil { return }`. On an error `rows` may be nil, and deferring a method call on it before the check panics. ### `rows.Next()` — advance, and hide errors `(*sql.Rows).Next() bool` advances to the next row, fetching more from the server as needed, and returns `false` when there is nothing more to read. The word "nothing more" is doing dangerous work: `false` is returned both at the natural end of the result set **and** when iteration failed — a broken connection, a cancelled context, a driver-level error mid-stream. `Next` itself returns no error. A loop that treats `false` as "done" and returns whatever it accumulated will happily return a short answer with a nil error. ### `rows.Scan(dest ...any)` — pointers, in order `Scan` copies the current row's column values into the destinations you pass. Each destination must be a **pointer** — `&id`, not `id` — because `Scan` writes through it; passing a value gives you an error about an unsupported destination type. You must pass exactly as many destinations as the query selected columns, or `Scan` returns an error naming the mismatch. This is the reason to write out an explicit select list rather than `SELECT *`: with `*`, someone adding a column to the table breaks every `Scan` at runtime. `Scan` also converts: a driver's `int64` will land in an `*int`, numeric text will parse into a numeric destination, and so on, returning an error when the conversion is impossible. Declare the destination variables **inside** the loop body; reusing one variable declared outside is a classic source of stale values when a `Scan` error is skipped. ### `rows.Err()` — the check that makes the loop honest `(*sql.Rows).Err() error` reports the error that ended iteration, and `nil` if the loop ended because the data ran out. It is the only way to distinguish those two cases, and it must be checked after every `for rows.Next()` loop. `return out, rows.Err()` at the end of a helper is the compact idiom. ### The whole loop, judged A reviewer reads this code looking for: the error check before the defer; the defer itself; pointers in `Scan`; and the `rows.Err()` after the loop. Missing the first is a panic, missing the second is a leaked connection, missing the third is a compile-clean runtime error, and missing the fourth is a silent wrong answer — the worst of the four, because nothing ever tells you.

  • What happens if you break out of the loop early and never call rows.Close?
    The `*sql.Rows` keeps its connection checked out of the pool indefinitely — it is not returned when the function exits. Under load the pool fills with cursors nobody is reading and later queries block acquiring a connection until their context deadline fires. That is why `defer rows.Close()` goes in immediately after the error check rather than at the bottom of the happy path.
  • Is it a problem that the deferred rows.Close runs after Next has already closed the result set?
    No. `(*sql.Rows).Close` is idempotent: calling it on an already-finished result set is a no-op and does not change what `rows.Err()` reports. That is exactly what makes the unconditional `defer` safe, so you never have to reason about which exit path finished the cursor.
  • Why must every argument to rows.Scan be a pointer?
    `Scan` writes the row's values into the caller's variables, so it needs their addresses; the parameter type is `...any`, so passing a value compiles fine and fails at runtime with an unsupported-destination error. It also requires exactly one destination per selected column, which is a good argument for spelling out the select list instead of `SELECT *`.

saying these in an interview costs you the question

  • Says rows.Close is optional because Next closes the rows
  • Puts defer rows.Close() before checking QueryContext's error
  • Treats Next returning false as proof every row was read
  • Passes values rather than pointers to Scan
  • Never calls rows.Err() after the loop
  • Assumes the connection returns to the pool when QueryContext returns
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

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

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

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