Inserting, and Getting the ID Back
Statements that don't produce rows — INSERT, UPDATE, DELETE, CREATE TABLE — go through Exec, which returns a sql.Result instead of a cursor:
res, err := db.Exec(`INSERT INTO tasks (title, done) VALUES (?, ?)`, title, false)
if err != nil {
return 0, fmt.Errorf("insert task: %w", err)
}
id, err := res.LastInsertId()
Using Query for an insert is a real mistake, not a style preference: it returns a *sql.Rows that holds a connection until you close it, and an insert produces no rows to make you close it.
sql.Result is a promise, not a value
Both methods return (int64, error), and that second return is not decoration. Whether the driver can answer depends on the database:
LastInsertIdworks on SQLite and MySQL. PostgreSQL does not support it at all — there you useINSERT ... RETURNING idwithQueryRowinstead.RowsAffectedis widely supported but still permitted to fail.
So id, _ := res.LastInsertId() is code that works until you change database and then silently returns zero. Check the error, and know which of these two your driver actually implements.
Let the database assign the id
id INTEGER PRIMARY KEY AUTOINCREMENT means the database owns that sequence. The temptation is to compute the next id yourself — SELECT max(id) + 1 — and it is wrong the moment two requests arrive together: both read the same maximum, both insert, one wins. The database's sequence is atomic precisely so you don't have to think about that.
This is the same reason the API layer decodes into a request struct rather than the domain type: the id is not the client's to choose, and it is not yours either.
Placeholders carry values, never identifiers
db.Exec(`INSERT INTO tasks (title) VALUES (?)`, title) // yes
db.Exec(`INSERT INTO ` + table + ` (title) VALUES (?)`, title) // the table name is not a value
Placeholders are bound as data. A table or column name cannot be one, which is exactly why building those by concatenation is where injection comes back — validate such names against a fixed allow-list, never against a regex.
Try it
Insert a task and return the id the database assigned it — checking the error the Result is entitled to give you.
Practice
1. Insert a Task
IntermediateImplement
CreateTask(db *sql.DB, title string) (int64, error).- Insert a row into
taskswith the given title anddonefalse. - Return the id the database assigned, not one you computed.
- Wrap any failure with
%w, including a failure to read the id back.
The schema is created by the hidden tests:
CREATE TABLE tasks ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, done BOOLEAN NOT NULL DEFAULT 0)package main import ( "database/sql" "fmt" _ "modernc.org/sqlite" ) // CreateTask inserts a task and returns the id the database assigned it. func CreateTask(db *sql.DB, title string) (int64, error) { // TODO: implement return 0, fmt.Errorf("not implemented") } func main() {}Sign in to write and run your solution in the browser.
Sign in for hints and the solution.
- Insert a row into