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:
gosdk/ └── yourmodule/ ├── go.mod ├── go.sum ├── yourmodule.go # public API / interfaces ├── impl.go # implementation(s) ├── Makefile # at minimum: a `test` target └── example/ └── integration_test.go # integration tests using testcontainers or similar -
go.mod— use a simple module name matching the directory:module yourmodule go 1.26.1 -
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 an integration test target:test: go test -tags integration -v -count=1 -timeout 300s ./...
-
example/integration tests — useTestMainto spin up any required infrastructure (e.g. a Testcontainers container) once for the whole suite, then share the connection across tests to keep suite startup time flat:func TestMain(m *testing.M) { // start container / external dependency once // populate a package-level DSN / client var code := m.Run() // teardown os.Exit(code) }
Helpers like
newClient(t, cfg)should register cleanup viat.Cleanupso individual tests never callClosemanually.
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 |
db/Makefile |
test target running integration tests with -tags integration |
db/example/postgres_integration_test.go |
Full integration test suite using Testcontainers |
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"
client, err := db.NewPostgresClient(dsn, db.ConnectionConfig{
MaxOpenConns: 10,
MaxIdleConns: 5,
ConnMaxLifetime: 30 * time.Minute,
QueryTimeout: 5 * 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) { ... }