Memories
One-paragraph idiomatic tips. bd prime auto-injects all of these into agent sessions; this page is the human-browsable mirror.
atomic-rename-samedir
For atomic file writes, the temp file MUST live in the same directory
as the target — rename(2) is atomic only within a single filesystem,
so a rename from /tmp/foo to ~/.config/foo is a non-atomic
cross-filesystem copy. Use os.CreateTemp(filepath.Dir(target), ".tmp-*"), write, sync, rename. Wrap in a WriteFileAtomic helper.
blank-ident-assert
Declare var _ Iface = (*Concrete)(nil) at package scope wherever a
concrete type implements an interface you care about. The assignment
is type-checked at compile time, so any drift (renamed method, wrong
signature, new interface method) fails the build at that line instead
of at a distant runtime call site. Zero runtime cost.
context-first-param
ctx context.Context is always the first parameter of any function
that takes one, conventionally named ctx, never stored as a struct
field. Storing ctx on a struct ties every method's cancellation
lifetime to whoever populated the field, defeats
errgroup.WithContext's derived-ctx pattern, and tempts closures to
capture a stale ctx. The byob-lifecycle.1 decision threads ctx
through every runFunc — this is the underlying rule.
decisions-lookup
byob ships its library under a custom byob issue type — reference
material for architectural choices you make now (error patterns,
command shapes, config loaders, interface seams). Browse the ~20
category roots with bd list --type=byob --no-parent; drill in
with bd list --type=byob -l errors (or any other category label);
then bd show <id> for the full Problem / Idea / Tradeoffs /
Sketch. Use bd ready --exclude-type=byob to keep the library out
of the ready-work list — your own decisions and epics still
surface because they use the built-in types.
byob beads are preferences, not contracts. Apply them when writing new code. Existing code that diverges might or might not be a bug — assess case-by-case; don't reflexively migrate just because something doesn't follow a byob idiom. File a task bead if the gap is worth tracking. And don't build anything — tests, lints, CI gates, hooks, runtime asserts — that fails when a byob decision is violated. They're idioms for new code, not invariants to enforce.
defer-unlock
After acquiring a mutex, defer the release on the very next line:
mu.Lock(); defer mu.Unlock(). Same rule for RUnlock, channel
closes, file Close, and any "must release" pair — adjacent placement
makes the unlock visible at the lock site and impossible to forget on
early returns or panics. The tx.Rollback exception: defer the
rollback then call tx.Commit() before return; rollback after a
successful commit is a documented no-op in database/sql, so the
defer is safe.
doc-comment-shape
Doc comments on exported declarations start with the identifier name
and form a complete sentence: // Store persists items to a backing SQLite file. not // This struct holds the store. godoc and IDE
hover tooltips render the comment verbatim, so a comment that doesn't
name what it documents loses its anchor when read out of context.
Same shape for package comments: // Package store persists items to disk.
errors-is-as
Compare wrapped errors with errors.Is(err, sentinel) for sentinel
checks and errors.As(err, &target) to extract a typed error — never
==, never raw type assertion. Once any layer wraps with %w (per
errors-wrap-w), err == ErrFoo returns false even when
errors.Is(err, ErrFoo) returns true; the type-assertion form
e, ok := err.(*MyErr) has the same failure mode. errors.As walks
the chain — pair with errors.Is once %w-wrapping is in the
codebase.
errors-message-style
Error messages start with a lowercase letter, have no trailing
punctuation, and contain no newlines or tabs. Errors get wrapped into
larger sentences — fmt.Errorf("loading config: %w", err) composes
into "loading config: reading foo.toml: no such file or directory",
clean without mid-sentence capitals or stray periods. Multi-line
remediation text belongs in an ErrHint wrapper or on ErrOut, not
inside the error value.
errors-wrap-w
Always wrap errors with %w, never %v or %s:
fmt.Errorf("reading %s: %w", path, err). %w preserves the chain
so errors.Is/errors.As can find the underlying error; %v
stringifies and silently severs it. Printed output is identical — the
only difference is machine-readable introspection. Use %v only in
log messages you're building but not returning.
goroutine-exit-path
Every go func() has a documented exit path: ctx cancellation via
select { case <-ctx.Done(): return }, a sync.WaitGroup the parent
calls Wait on, a channel close that breaks the inner loop, or natural
completion of a bounded loop. No fire-and-forget. A goroutine blocked
on an unbuffered send or receive is invisible to the GC and lives
forever. byob's default fanout is errgroup.Group (byob-lifecycle.3);
the rule applies to any goroutine.
got-want-order
Test failure messages put got before want:
t.Errorf("Foo(%v) = %v, want %v", in, got, want). The testing
package's diff output, IDEs, and reader habit all assume that order;
reversing to "expected vs actual" works locally but breaks tooling.
Mnemonic: "got is what you got, want is what you wanted." Note that
go-cmp inverts at the call site — cmp.Diff(want, got) — because
the diff renders as -want +got; the wrapping message still reads
got-then-want.
initialism-casing
Initialisms (URL, HTTP, ID, JSON, API) keep consistent case in
identifiers: appID not appId, serveHTTP not serveHttp,
URLParser not UrlParser. Matches the stdlib (http.ServeHTTP,
url.URL). The trap is copy-pasting from JS/Java/C# corpora where
userId and urlParser read as natural — Go's stdlib has trained
readers to expect the all-or-nothing form, and inconsistency stands
out.
no-blank-error-discard
Don't write _ = err or _, _ = io.Copy(dst, src) to silence an
error. Either handle it (return, log, fall back), or annotate the
discard with a comment explaining why the error is provably safe to
ignore: _ = f.Close() // best-effort close on read-only file.
Silent discards are how production bugs hide in plain sight — every
one is either a missed return path or worth one line of justification.
no-get-prefix
Field accessors omit the Get prefix: func (u *User) Name() string,
not GetName(). Setters keep Set because func SetName(s string)
has no idiomatic alternative. Action verbs are not accessors and keep
their natural name: s3.GetObject is correct because it performs an
RPC, not a field read. Mental model: Counts() is a noun (the
count); GetCounts() is a Java-flavored method call.
pass-by-value-default
Don't pass pointers to function arguments just to save a copy. Small
types — int, string, time.Time, structs of a few words, slices,
maps — go by value. Passing *string adds an indirection, can force
the value to escape to the heap, and signals "this function might
mutate me" that you don't mean. Reach for a pointer when the function
genuinely mutates the receiver, when the struct is large enough that
copying matters (proto messages, big configs), or when nil encodes
"absent."
principles-dry-kiss-solid
Duplicate twice before abstracting. Three similar lines is better than a premature interface or helper — the right abstraction is usually visible only after the second or third instance lands. The byob decisions already encode the abstractions worth pulling forward (Factory DI, narrow Options, small interfaces, semantic error types), so reach for them when the shape matches, not because a named principle (KISS, YAGNI, DRY, SOLID) demands it. Treat those names as tiebreakers during refactor, not constraints during design — invoking them up front tends to push toward more structure rather than less.
prompter-tty-check
Every Prompter method must check IO.IsStdinTTY() first. If stdin
is not a terminal (CI, piped input, nohup'd background process),
return the sentinel ErrNotTTY instead of prompting. Callers surface
a clear error ("pass --yes to run non-interactively") or branch on
errors.Is(err, prompt.ErrNotTTY). Failure mode to avoid: a prompt
library reads EOF, interprets it as "no," and silently skips a
destructive action. Loud failure beats silent wrong answer.
quote-strings-in-errors
Format user-supplied strings in errors and log messages with %q not
%s: fmt.Errorf("unknown subcommand %q", name). %q wraps the
value in Go-syntax quotes and escapes non-printables — empty strings
render as "" instead of vanishing into mid-sentence whitespace,
trailing newlines surface as \n, and a quoted "--help" doesn't
get visually merged with surrounding text.
receiver-name
Method receiver names are a 1–2 letter abbreviation of the type, used
consistently across every method on that type — func (s *Store) ...
everywhere, never mixing in func (st *Store) or generic
this/self/me. Short receivers read like math; consistent
receivers let you scan a type's methods without re-parsing the
binding name.
sync-oncevalue
Use sync.OnceValue[T] (Go 1.21+) to wrap an expensive lazy factory
instead of hand-rolling sync.Once + captured vars. First call runs
the underlying function and caches the return; subsequent calls
return the cache. sync.OnceValues[A, B] handles the two-return
form, which is what factory closures usually want:
func() (Store, error). Errors are sticky — a failed open won't
magically succeed on retry, almost always what you want for a
factory.
test-cleanup
Use t.Cleanup(func()) instead of bare defer for test teardown. It
runs LIFO even on panic, composes cleanly with t.Run subtests, and
lives next to the resource it's tearing down rather than at the top
of the test. Cleanups registered inside a helper stay attached to the
test even after the helper returns — defer can't do that. Subtests
inherit t and can register their own cleanups independently.
test-helper
Call t.Helper() as the first line of every test helper function.
The testing framework skips helper frames when reporting failure
locations, so a failed assertion points at the test that called the
helper instead of at the helper's internals. Skip it only when
debugging the helper itself.
test-tempdir
Use t.TempDir() for any test that touches the filesystem: it
creates a per-test isolated directory and auto-cleans after the test
ends. Write fixtures inline with os.WriteFile rather than
maintaining a committed testdata/ tree — tests stay hermetic,
parallel-safe, and the fixture and assertion read on the same screen.
Reserve testdata/ for truly static reference files (golden outputs,
sample binary inputs).
validate-at-boundaries
Validate user input once, at the boundary — Options.Validate() per
byob-input-validation.5, or the config-parse step per
byob-input-validation.2. Internal callers can then trust what they
receive; don't re-check args in every helper. Defensive validation deep
in internal code rots into noise, hides real bugs by making the
validation graph hard to read, and quietly assumes the rest of the
codebase is untrusted when it isn't.