go / must
I use small must helper functions to convert errors into
panics in code paths where errors indicate programmer
mistakes: startup, test setup, and internal invariants that
should never fail.
The pattern
A 4-line function, defined inline in whichever package needs it:
func must(err error) {
if err != nil {
panic(err)
}
}
Used at startup or in cleanup paths where there's no sensible fallback:
must(os.Setenv("GIT_CONFIG_GLOBAL", gitConfigGlobal))
must(os.RemoveAll(rootDir))
must(os.MkdirAll(wsDir, 0700))
must(command(ctx, os.Stdout, "git", "clone", repoURL, repoDir).Run())
If any of these fail, the process can't make progress. Better to crash with a stack trace than to limp along.
Test variant
In tests, take *testing.T so the failure stops the test
gracefully and the message points at the call site, not the
helper:
func must(t *testing.T, err error) {
t.Helper()
if err != nil {
t.Fatal(err)
}
}
t.Helper() makes the failure report the test function's
line, not the line inside must. Same idea as the
log helper registry, applied to test output.
Usage:
func TestSchema(t *testing.T) {
db = pqtest.Open(t, pqtest.SchemaFile("schema.sql"))
defer db.Close()
must(t, boxPing(ctx, BoxPingReq{ID: "box1"}))
must(t, upsertJobs(ctx, "commit1", "/", []string{"cmd1"}))
must(t, markDone(ctx, job, "error", "...", "", 0))
}
Inline or package?
I keep must inline in whichever package needs it. The
function is small enough that copy-paste costs less than
threading an import through a project, and each variant
(production vs. test) has its own signature anyway.
For widely-needed cases the standard library already
provides Must* wrappers:
template.Must(template.Parse(...))regexp.MustCompile(pattern)
Reach for those first.
Returning a value
If you do need a generic version that returns a value (e.g.
for var x = mustGet(somefunc()) at package level), keep
it inline too:
func mustGet[V any](v V, err error) V {
if err != nil {
panic(err)
}
return v
}
var configRe = mustGet(regexp.Compile(`^[a-z]+$`))
In practice the stdlib Must* variants cover this case for
regex, templates, and a few others, so I rarely need it.
When to use
- Package-level
varinitialization where failure is a bug - Test setup that should never fail and shouldn't clutter every line with error checks
- Internal invariants that callers rely on
When not to use
- Code paths that handle untrusted input
- Library code that other packages call (return the error)
- Recoverable errors