Lesson 22 · PostgreSQL in Go
The database layer: Ext, entities, ExecInTx
The thin abstraction every repository in your services is built on.
Your win: read our internal/golibs/database layer — the
interfaces, the entity pattern, and the transaction closure — recognising the Go-course
ideas underneath it.
The interfaces: Ext and QueryExecer
Repos don't take a concrete pool — they take a small interface, so the same method works with the pool or a transaction (your Go course, Lesson 6):
internal/golibs/database/db.go:17-36type QueryExecer interface { Query(...); QueryRow(...); Exec(...); SendBatch(...) }
type Ext interface { QueryExecer; Begin(ctx) (pgx.Tx, error) } // can start a tx
// repos: func (r *EmailRepo) UpsertEmail(ctx, db database.QueryExecer, e *model.Email) error
The entity pattern
A Go struct maps to a table via FieldMap() (columns ↔ field pointers) and
TableName(); pgtype fields carry NULL via their status
(Lesson 2). database.Select(...).ScanAll(&ents) reads rows using the
slice's Add():
type Email struct { EmailID pgtype.Text; Content pgtype.JSONB; ... }
func (*Email) TableName() string { return "emails" }
func (e *Email) FieldMap() ([]string, []interface{}) { ... }
database.Select(ctx, db, query, args...).ScanAll(&emails)
Writes use raw SQL with $N params + helpers like
database.GeneratePlaceholders and UpsertExcept (ON CONFLICT,
Lesson 4). IDs are ULID text (idutil.ULIDNow()).
Transactions: the ExecInTx closure
To run several writes atomically (Lesson 4), wrap them in a closure — the same defer/closure pattern from your Go course, Lesson 13:
internal/golibs/database/db.go:41database.ExecInTx(ctx, db, func(ctx, tx pgx.Tx) error {
repoA.Insert(ctx, tx, a) // tx satisfies Ext → threads into each call
repoB.Insert(ctx, tx, b)
return nil // nil → COMMIT; error → ROLLBACK
})
Ext/QueryExecer — Go L6) that make repos mockable;
pgtype zero-value/NULL handling (Go L2); the ExecInTx closure +
deferred commit/rollback (Go L13). And it's how the Postgres concepts you learned reach
real SQL — UpsertExcept is Lesson 4's ON CONFLICT,
ExecInTxWithRetry is Lesson 20's 40001.
pgx (queries, transactions, batch) + the repo layer
How pgx Query/Exec/Begin/SendBatch
work under our wrappers. Ground truth: repo-postgres-map.md.
Check yourself (from memory)
Q1. A repo method takes db database.Ext so it can accept…
pgx.Tx satisfies Ext, so the
same repo call runs inside or outside a transaction. (Small interface — Go L6.)
Q2. The entity's FieldMap() exists to…
&field
pointers, so ScanAll can read rows into the struct.
Q3. ExecInTx commits when the closure…
nil → COMMIT; return an error →
ROLLBACK. The whole group is atomic (Lesson 4).
(ctx, db database.Ext|QueryExecer, ...)
— small interfaces (Go L6) accepting the pool or a tx. Entities implement
FieldMap()/TableName() with pgtype fields (NULL via
.Set(nil)/status); Select(...).ScanAll(&ents) reads; writes
use $N params + helpers (UpsertExcept = ON CONFLICT). For
several writes in one tx: ExecInTx(ctx, db, func(ctx, tx){ repoA.Insert(ctx, tx,…);
repoB.Insert(ctx, tx,…); return nil }) — COMMIT on nil else ROLLBACK (Go L13);
tx satisfies Ext so it threads into each call.SendBatch (pipelining) or ScanAll works under
the hood? Ask me.
1. Repo: repo-postgres-map.md; pgx/v4 docs.