This is a utility project for Golang. Each module lives in its own subdirectory with an independent go.mod.
-
Create a subdirectory named after your module:
fiddle/ └── yourmodule/ ├── go.mod ├── go.sum ├── yourmodule.go # public API / interfaces ├── impl.go # implementation(s) ├── yourmodule_test.go # tests └── Makefile # at minimum: a `test` target; include ../Makefile.common for lint/vuln -
go.mod— use a simple module name matching the directory:module yourmodule go 1.27.2 -
Public API file (
yourmodule.go) — define sentinel errors, interfaces, and config structs. Keep the public surface small; consumers should only need to import this package. -
Implementation file — implement the interfaces declared in the API file. Unexported types are fine; expose only constructors (e.g.
NewXxxClient). -
Makefile— include at least atesttarget, and the sharedlintandvulntargets:.PHONY: test include ../Makefile.common test: go test -count=1 -timeout 120s ./...
Add the module to
MODULESin the rootMakefileand to the matrix in.github/workflows/ci.yml.
The db/ module is the canonical reference implementation. It provides a PostgreSQL client with connection-pool configuration, automatic query timeouts, transaction helpers, and error classification utilities.
| File | Purpose |
|---|---|
db/client.go |
Sentinel errors, DBTX, SQLExecutor, DB interfaces, ConnectionConfig |
db/postgres.go |
NewPostgresClient constructor and postgresClient implementation (pgx through database/sql) |
db/Makefile |
test target, plus the shared lint and vuln |
DBTX — raw query methods (compatible with sqlc)
└── SQLExecutor — DBTX + WithTransaction
└── DB — SQLExecutor + Close + PingContext
Pass DBTX or SQLExecutor to repositories so they can't accidentally close the shared pool. Only the dependency-injection root should hold DB.
import "db"
// ctx cancels the startup ping, which is also bounded by PingTimeout (default 5s).
client, err := db.NewPostgresClient(ctx, dsn, db.ConnectionConfig{
MaxOpenConns: 10,
MaxIdleConns: 5,
ConnMaxLifetime: 30 * time.Minute,
QueryTimeout: 5 * time.Second, // per query
TxTimeout: 30 * time.Second, // per WithTransaction call, begin to commit
PingTimeout: 3 * time.Second,
})
if err != nil {
log.Fatal(err)
}
defer client.Close()
// Single query
var name string
err = client.QueryRowContext(ctx, `SELECT name FROM users WHERE id = $1`, id).Scan(&name)
// Transaction
err = client.WithTransaction(ctx, sql.LevelReadCommitted, func(ctx context.Context, tx db.DBTX) error {
_, err := tx.ExecContext(ctx, `INSERT INTO users (email) VALUES ($1)`, email)
return err
})
// Error classification
if db.IsDuplicateKeyError(err) { ... }
if db.IsTimeoutError(err) { ... }The driver is pgx through database/sql.
- PgBouncer (transaction pooling) and similar proxies. pgx prepares and caches statements per connection by default, which such a proxy cannot serve. Add
default_query_exec_mode=exec(orcache_describe,simple_protocol) to the DSN. Thedbintegration tests check that these modes create no named prepared statements. - Column types.
json,jsonb,uuid,numeric(scale kept, e.g."12.50"),timestamptz,bytea,boolandbigintscan into[]byte/string,string,time.Time,[]byte,boolandint64as usual (seeTestScan_CommonColumnTypes). Array columns are not covered: use a driver-specific helper for them.
Run from the repo root to cover every module, or make <target> inside one module:
make test # all tests
make lint # golangci-lint, configured by .golangci.yml
make vuln # govulncheck
make check # tidy, vet, race tests, lint and vuln: what CI checksCI (.github/workflows/ci.yml) runs the same targets per module.
see proven implementation from big companies, do we need abstraction? is it general implementation? do the implementation handle specific usecase only or general? is it configurable or not? document it in architecture decision record in adr.md in the directory if not exist create one.