One Row, or None
Fetching a single row is the most common query a service makes, and database/sql shapes it in a way that surprises people once:
row := db.QueryRow(`SELECT title FROM tasks WHERE id = ?`, id)
row is never nil and there is no error to check here. QueryRow defers everything — the query may not even have run yet. The error surfaces when you Scan:
var title string
err := row.Scan(&title)
That design exists so the common case chains: db.QueryRow(...).Scan(&title) is one statement with one error, and it is the idiomatic form.
sql.ErrNoRows is not a failure
If the query matched nothing, Scan returns sql.ErrNoRows. This is the single most important error in the package to handle deliberately, because it is not a malfunction — it is an answer. The row isn't there.
switch {
case errors.Is(err, sql.ErrNoRows):
return Task{}, ErrNotFound
case err != nil:
return Task{}, fmt.Errorf("get task %d: %w", id, err)
}
Note what that does: it translates. sql.ErrNoRows is a storage detail, and letting it escape your store means every caller — HTTP handlers, CLI commands, other services — has to import database/sql to ask "was it missing?". Translate at the boundary and callers depend on your vocabulary, not your driver's.
Scan writes through pointers
Scan takes destinations, so every argument is an address: &title, not title. Pass a value and you get a runtime error about an unsupported type, not a compile error, because the signature is ...any. It is the most common typo in this package, and the compiler cannot help you.
The count must match the columns too — SELECT id, title needs exactly two destinations, in that order.
Placeholders, never string concatenation
The ? is a placeholder the driver binds separately from the SQL text. It is not string formatting, and the difference is the difference between a parameter and an injection:
// Never. A title of "'; DROP TABLE tasks; --" is now your problem.
db.QueryRow("SELECT id FROM tasks WHERE title = '" + title + "'")
Different databases spell the placeholder differently — SQLite and MySQL use ?, PostgreSQL uses $1 — but the rule never changes: values go through placeholders.
Try it
Implement a GetTask that returns one task, and reports a missing row in your own vocabulary rather than the driver's.
Practice
1. Fetch One Task
IntermediateImplement
GetTask(db *sql.DB, id int64) (Task, error).- On success, return the task with its
id,titleanddonepopulated. - When no row has that id, return the zero
TaskandErrNotFound— the sentinel already declared for you — notsql.ErrNoRows. Callers should not have to importdatabase/sqlto ask whether something was missing. - On any other failure, return the zero
Taskand the error, wrapped with%wso the cause survives.
Use a placeholder (
?) for the id. The hidden tests create the schema and seed rows themselves; you only write the query.package main import ( "database/sql" "errors" "fmt" _ "modernc.org/sqlite" ) // ErrNotFound is this package's vocabulary for "no such task". Callers test for // this, so they never need to know a database was involved. var ErrNotFound = errors.New("task not found") type Task struct { ID int64 Title string Done bool } // GetTask returns the task with the given id, or ErrNotFound. func GetTask(db *sql.DB, id int64) (Task, error) { // TODO: implement return Task{}, fmt.Errorf("not implemented") } func main() {}Sign in to write and run your solution in the browser.
Sign in for hints and the solution.
- On success, return the task with its