Notes — Module 14: Databases and Persistence¶
These are your personal study notes. Write freely and honestly. Incomplete notes are fine — they show where your understanding still needs work. Return to this file to add insights as they develop over time.
Module: [[go/14. Databases and Persistence]] Topic: [[go]] Date started: YYYY-MM-DD Status: In progress
Concept Map¶
Sketch how the concepts in this module relate to each other. Fill in the Mermaid diagram.
mindmap
root((Databases & Persistence))
database/sql architecture
driver interface
blank-import registration
sql.Open is lazy
db.PingContext verifies connectivity
sql.DB is a connection pool
long-lived single instance
safe for concurrent use
SetMaxOpenConns / SetMaxIdleConns
SetConnMaxLifetime / SetConnMaxIdleTime
querying
QueryContext — multiple rows
QueryRowContext — single row
ExecContext — INSERT/UPDATE/DELETE
always use context-aware variants
rows iteration protocol
rows.Next loop
rows.Scan into struct fields
rows.Err after loop
defer rows.Close after err check
NULL handling
sql.NullString / sql.NullInt64
pointer types (*string)
prepared statements
placeholder ? vs $1
db.PrepareContext for loops
transactions
BeginTx with TxOptions
defer tx.Rollback safety
Commit
isolation levels
SQL injection
never concatenate user input
parameterized queries always
migrations
versioned SQL scripts
golang-migrate / goose
repository pattern
UserStore interface
SQLUserStore concrete impl
FakeStore for tests
inject via interface — testable
ecosystem
sqlx struct scanning
sqlc code generation
GORM ORM
Alternative: draw this on paper, photo it, and link the image here.
Key Insights¶
The "aha moments" — the things that, once understood, made the rest clear. Be specific: "I finally understood X because Y" is more useful than "X makes sense".
sql.DBis a pool, not a connection: I finally understood this when I realized thatdb.QueryContextanddb.ExecContextcalled from different goroutines simultaneously each get their own connection from the pool — there's no serialization. This is why it's safe for concurrent use and why you must NOT create a newsql.DBper request.defer tx.Rollback()after Commit is a no-op: The safety idiom clicked when I realized: aftertx.Commit()succeeds, callingtx.Rollback()does nothing — the database ignores it. So the defer is free to always be there. It only fires on the "something went wrong" paths.- Add insights as you discover them
My Understanding¶
Explain the core concepts in your own words, as if teaching them to someone else. If you can't explain it simply, you don't understand it well enough yet.
The database/sql driver model¶
Your explanation here
What I'm still unsure about: (e.g., what exactly happens inside sql.Register — is there a global map?)
sql.DB as a connection pool¶
Your explanation here
What I'm still unsure about: (e.g., when does the pool actually create a new connection vs. reuse an idle one?)
The rows iteration protocol¶
Your explanation here
What I'm still unsure about: (e.g., what exactly does rows.Close() release — just the row cursor, or the connection back to the pool?)
Transactions¶
Your explanation here
What I'm still unsure about: (e.g., what does LevelSerializable actually prevent that LevelReadCommitted does not?)
The repository pattern¶
Your explanation here
Connections to Other Topics¶
How does this module connect to things you already know?
| This module's concept | Connects to | How |
|---|---|---|
Repository interface (UserStore) |
[[go/6. Methods and Interfaces]] | The interface is defined with methods; SQLUserStore satisfies it structurally — same mechanism as any Go interface |
Error wrapping (fmt.Errorf("get user: %w", err)) |
[[go/8. Error Handling]] | Every DB call wraps errors with context; callers use errors.Is(err, ErrNotFound) to check sentinel errors through the chain |
Passing r.Context() to DB calls |
[[go/13. Web Services and APIs]] | The HTTP request context carries the deadline/cancellation signal; passing it to QueryContext propagates cancellation to the database query |
| Connection pool concurrency | [[go/9. Concurrency]] | Multiple goroutines sharing one sql.DB is safe because the pool serializes connection checkout; understanding goroutines helps reason about what happens under load |
Questions That Arose¶
Log questions as they appear. Don't stop to answer them now — just capture them. Then move the serious ones to QUESTIONS.md.
- What happens to a transaction if the context is cancelled while a query inside the transaction is running? → added to QUESTIONS.md as Q001
- Does
defer rows.Close()return the underlying connection to the pool, or just close the cursor? → added to QUESTIONS.md as Q002 - In PostgreSQL, does
LastInsertId()work? (I remember reading it doesn't — you useRETURNING idinstead.) → needs verification
Code Snippets Worth Remembering¶
Patterns, idioms, or examples that captured something important.
The full rows iteration protocol¶
rows, err := db.QueryContext(ctx, `SELECT id, name FROM users`)
if err != nil {
return nil, fmt.Errorf("query: %w", err)
}
defer rows.Close()
var users []User
for rows.Next() {
var u User
if err := rows.Scan(&u.ID, &u.Name); err != nil {
return nil, fmt.Errorf("scan: %w", err)
}
users = append(users, u)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("rows: %w", err)
}
return users, nil
Why I'm saving this: Four steps — check initial err, defer Close, scan in loop, check rows.Err after. Skipping any one of them causes subtle bugs.
The transaction safety pattern¶
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return fmt.Errorf("begin: %w", err)
}
defer tx.Rollback() // no-op after Commit; safety net for all other paths
// ... do work with tx.ExecContext / tx.QueryContext ...
return tx.Commit()
Why I'm saving this: defer tx.Rollback() placed immediately after BeginTx is the canonical Go transaction pattern. Rollback after Commit is a no-op — you never need to check if you already committed.
Parameterized query (SQL injection prevention)¶
// NEVER:
query := "SELECT * FROM users WHERE name = '" + name + "'"
// ALWAYS:
rows, err := db.QueryContext(ctx, `SELECT * FROM users WHERE name = ?`, name)
Why I'm saving this: The wrong version is visually tempting (shorter, looks like standard string formatting). The habit of always using ? placeholders must be automatic.
Checking sql.ErrNoRows¶
err := db.QueryRowContext(ctx, `SELECT id FROM users WHERE email = ?`, email).Scan(&id)
if errors.Is(err, sql.ErrNoRows) {
// user doesn't exist — not an error in the program sense, just "not found"
return 0, ErrNotFound
}
if err != nil {
return 0, fmt.Errorf("get by email: %w", err)
}
Why I'm saving this: sql.ErrNoRows is a normal condition (no matching row), not a bug. Must be checked before the generic error check — otherwise you'd return a "not found" as an internal error.
What Tripped Me Up¶
Mistakes I made, misconceptions I had, things that confused me more than they should have.
sql.Opensucceeds even when the DB is unreachable — I initially thought a successfulsql.Openmeant the connection was established. It doesn't.PingContextis the actual connectivity check.- Forgetting
rows.Err()after the loop — I initially thought the loop body's error check was sufficient. But if the query fails mid-iteration (e.g., network drop),rows.Next()silently returnsfalseand I'd return a partial result. - NULL scanning panic — I initially scanned a nullable
TEXTcolumn into astringfield. It compiles fine but panics at runtime when the value is actually NULL. The fix is*stringorsql.NullString.
Summary in My Own Words¶
Write a 3–5 sentence summary of this entire module without looking at any notes. If you can't do this, you need more study time.
Write your summary here after completing the module.
Last updated: YYYY-MM-DD