Style guide
How to write Go that fits the rest of the codebase.
// Package channels owns the public/private channel namespace and the
// lifecycle manager. Every other Parsec surface defers to ParseName for
// validation.
package channels
Two reflexes summarize most of the rules: keep functions short, and let the type system carry the contract.
Files
- Short. Most files in the repo are under 200 lines. If a file is growing past 300, split it by responsibility.
- Package-doc on every package, on the file named for the package
(
channels/name.gocarries the// Package channels...doc). - One package per directory. No subpackages by hand.
Comments
- Comment what is not obvious. Do not echo the function signature in English.
- Comment every exported symbol with a sentence that starts with its
name, per
gofmtconvention. - Do not narrate the implementation inside a function. If you feel the need to, the function is probably too long.
A bad comment:
// Touch resets the LastActive timestamp on the channel.
func (m *Manager) Touch(name Name) error {
// ...
}
A good comment:
// Touch marks the channel as recently active so the tick loop will not
// expire it on the next sweep.
func (m *Manager) Touch(name Name) error {
// ...
}
The first one is the signature in English; the second carries the intent.
Errors
- All errors that cross a package boundary are
*errors.Errorfromgithub.com/frankbardon/parsec/errors. Nevererrors.New(...)from the stdlib at a boundary. - Codes are
PARSEC_DOMAIN_CATEGORYstrings. Pick the right one fromerrors/codes.go; if none fits, add one in that file in the same PR and update the RPC mapping inrpc/server.go:writeServiceError. - Wrap with
errors.Wrap(code, msg, cause)when you have an underlying error; otherwiseerrors.New(code, msg).
Internal helpers can use any error type — but the moment the error is returned from an exported function, it must be coded.
Channel names
There is exactly one validator: channels.ParseName. Every surface
calls it. Do not reimplement parsing logic anywhere else. If you need
a new visibility prefix or a new component rule, the change lands in
channels/name.go and gains tests in channels/name_test.go.
Library-first
parsec.Parsec is the deliverable. Everything else (CLI, RPC) is a
translator.
cmd/parsec/main.goparses flags. Nothing else.internal/cli/*builds command structures. The bodies dispatch toservice.Serviceor*parsec.Parsec. No business logic.- If a CLI subcommand grows logic that the library cannot also call, the CLI is wrong. Pull the logic into a library package and let the CLI re-use it.
Filesystem access
Library code uses afero.Fs (opts.FS). Never os.Open /
os.ReadFile inside library packages. The CLI is the one allowed
exception (e.g. publish --file).
Descriptor envelopes
Every JSON output (CLI --json, /manifest) wraps its payload in
descriptor.Envelope. The format version bumps on any wire-shape
change.
Tests
- Table-driven where it helps.
channels/name_test.gois the model. - Inject the clock for any time-dependent test.
Manager.SetClockis there for this; do not calltime.Sleep. - Tests live next to the code, in
_test.gofiles. The naming pattern isTestThing_subcase. AvoidTestXfollowed by 30 unrelated scenarios.
Imports
- Group: stdlib, third-party, project-local.
gofmtenforces. - No dot imports.
- No blank imports outside
mainpackages.
Naming
- Package names are lower-case, one word.
- Exported types are
TitleCase. Exported constants are alsoTitleCase(noMAX_FOOshouting). - Receivers are short — a single letter that matches the type
(
m *Manager,b *Broker,p *Parsec). The receiver name is consistent across every method on the type.
Anti-patterns to refuse
These show up in PRs and should be pushed back on:
- A new
internal/package that duplicatesservice/logic. - A CLI command that calls
broker.*orchannels.*directly. Go throughservice.Service. - A new channel-validation helper outside
channels/. - A change to a public surface (CLI, RPC, library) without an update to the corresponding docs page in the same PR.
See also
- Development setup.
- The
CLAUDE.mdat the repo root — the operational contract this guide derives from.