Skip to content
Release

Introducing gopackx: a small collection of Go packages I actually use in production

Why I built gopackx, what's inside, and what's coming next.

Andrian Prasetya
4 min read 825 words

For the last few years, every backend project I started in Go ran into the same four problems before it ever shipped a feature: how do I migrate this schema, how do I audit who did what, how do I send a notification without standing up a service for it, and how do I document this API without lying to my clients.

Each time, I went looking. Each time, I came back with the same impression: the Go ecosystem is full of libraries that are either too big to read or too dead to depend on. So eventually I stopped searching and started extracting.

That’s gopackx. Four packages, each pulled out of a real production service, each small enough that you can read the source in one sitting and decide for yourself whether to trust it.

Why another collection of packages?

Most of the libraries I’ve adopted over the years have failed in one of two ways. The first failure mode is the framework: you reach for a small thing, import it, and discover it has opinions about your DI container, your logger, your context propagation, and the shape of your main(). By the time you’ve satisfied all of them, your codebase has a new dependency graph.

The second failure mode is the orphan: a perfectly-scoped package, exactly what you need, last commit three years ago, four open issues describing data loss bugs. You can fork it. You probably should. But forking means owning, and once you own it you may as well have written it yourself.

The best Go libraries are boring. They do one thing, the API hasn’t changed in two years, and you forget you’re using them. That’s the bar.

gopackx is my attempt at that bar — for the four problems I keep solving.

What’s inside

go-migration

Schema migrations for SQL databases. Versioned files, transactional by default, runs the same way locally and in CI. No DSL, no embedded ORM, no autogeneration of migrations from struct tags. You write the SQL, the package runs it in the right order, and it tells you exactly what it did.

// internal/db/migrate.go
package db

import (
	"database/sql"

	"github.com/andrianprasetya/go-migration"
)

func MigrateUp(db *sql.DB) error {
	m, err := migration.New(db, migration.WithDir("migrations"))
	if err != nil {
		return err
	}
	return m.Up()
}

The integration test suite runs against PostgreSQL, MySQL, and SQLite — not mocks — because a migration that “works” against a mock has never once told me the truth about production.

go-audit

Structured audit logging. Append-only by design, queryable with a real index, and pluggable so the sink can be Postgres today and Kafka tomorrow without rewriting the call sites. The point is not to ship a logger; the point is to let your service answer “who did what, to what, when” without you reaching for a tracing tool.

go-notification

A unified dispatcher across email, SMS, push, and webhooks. One interface, multiple drivers, pluggable transports. The opinion this package holds is that delivery is asynchronous and at-least-once, full stop. If you need synchronous send-and-fail, you don’t need this package.

open-swaggo

A drop-in replacement for the original swag tool. Generates OpenAPI 3.x specs from Go annotations, keeps the docs honest by failing the build when they drift from the handler signatures, and stays out of your imports. It exists because I was tired of doc that lied about my API.

How every package is built

There are four principles I’ve tried to hold across all of them, and they appear in the README of each repo verbatim:

1. Zero magic

No reflection-heavy abstractions, no global registries, no init-time goroutines. If you read the source, you can predict what runs.

2. Tested where it matters

Integration tests against real engines gate the release. Unit tests are useful; they’re not what tells me a release is safe.

3. Stable APIs

Once exported, breaking it requires a major version bump and a written migration path. Refactor freely, break carefully.

What’s next

The roadmap is small on purpose. v1.0 cuts for each of the four packages once their public APIs have been stable for two consecutive minor versions. A matrix of integration tests covering the latest two Go releases. And, maybe, one new package focused on background job orchestration — only if I keep needing it across services. No expansion for the sake of expansion.

If you’d like to follow along, the GitHub org is where the work happens. Issues and discussions are open. Bug reports with a minimal repro are the best gift you can give a maintainer.

Try one of the packages this week. If it breaks, file a bug. If it doesn’t, let me know that too — silence is the worst feedback.