Many Rows, and the Error You Forget
Query returns a *sql.Rows you iterate. The shape is always the same, and every line of it earns its place:
rows, err := db.Query(`SELECT id, title FROM tasks ORDER BY id`)
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
var t Task
if err := rows.Scan(&t.ID, &t.Title); err != nil {
return nil, err
}
out = append(out, t)
}
return out, rows.Err()
Four obligations: check the Query error, defer Close, check each Scan, and — the one nearly everyone forgets — check rows.Err() at the end.
Why rows.Err() is not optional
rows.Next() returns a bool. It returns false when the rows are exhausted, and it also returns false when iteration failed — a dropped connection, a decoding error partway through. Those two outcomes are indistinguishable from the loop alone.
Skip rows.Err() and a query that died halfway through returns a short list with a nil error. Your service reports three tasks instead of ten and calls it success. That is worse than an error, because nothing anywhere will tell you it happened.
Close is about connections, not tidiness
Until the rows are exhausted or closed, they hold a connection from the pool. Return early from that loop without defer rows.Close() and the connection is never released. Do it under load and the pool empties, every subsequent query blocks waiting for a connection that is never coming back, and the service hangs — with no error and no crash, just silence.
defer rows.Close() immediately after the error check is the habit that prevents it. Closing twice is harmless, so the defer is safe even when the loop finished normally.
Query is for many, QueryRow is for one
They are not interchangeable. QueryRow reads at most one row and releases the connection for you; Query hands you the cursor and the responsibility. Reaching for Query when you want a single row means opting into all four obligations for no reason.
Try it
Return every task in id order — and make sure a failure partway through the result set reaches the caller instead of silently truncating it.
Practice
1. List Every Task
IntermediateImplement
ListTasks(db *sql.DB) ([]Task, error).- Return every task ordered by
id. - Return an empty, non-nil slice when there are none, so a JSON caller sees
[]rather thannull. - Release the connection when you are done with the rows.
- If iteration itself fails partway through, the caller must get an error — not a short list with a nil error.
The hidden tests create the schema and seed rows; you only write the query.
package main import ( "database/sql" "fmt" _ "modernc.org/sqlite" ) type Task struct { ID int64 Title string Done bool } // ListTasks returns every task, ordered by id. func ListTasks(db *sql.DB) ([]Task, error) { // TODO: implement return nil, fmt.Errorf("not implemented") } func main() {}Sign in to write and run your solution in the browser.
Sign in for hints and the solution.
- Return every task ordered by