Go style guide#
Pigweed’s Go style guide extends the Google Go Style Guide (Style Decisions and Best Practices). Where the two conflict, this guide wins.
All Pigweed Go code is formatted with gofmt via ./pw format and targets
the Go version declared in the root go.mod (Go 1.24+).
Standard library generics and modern Go#
Use the standard library’s generic packages (slices, maps, cmp) and
modern language features instead of writing open-coded loops or boilerplate
helpers:
Slice and map operations: Use
slices.Contains,slices.IndexFunc,slices.SortFunc,slices.Compact,slices.Clone, andmaps.Clone/maps.Keysinstead of hand-written search, deduplication, or copy loops.Comparisons and defaults: Use
cmp.Or(a, b, fallback)for fallback chains andcmp.Compare(a, b)insideslices.SortFunccomparators.Built-in min/max and integer ranges: Use the built-in
minandmaxfunctions andfor i := range ninteger loops.Generic helpers: When a data transformation or set operation (such as order-preserving deduplication) applies to multiple types, write a small generic function constrained by
comparableorcmp.Orderedrather than duplicating type-specific helpers.
Yes: Standard library slices and cmp
slices.SortFunc(items, func(a, b Item) int {
return cmp.Or(
cmp.Compare(b.Priority, a.Priority),
cmp.Compare(a.Name, b.Name),
)
})
No: Open-coded sort.Slice or nested if/else comparators
sort.Slice(items, func(i, j int) bool {
if items[i].Priority != items[j].Priority {
return items[i].Priority > items[j].Priority
}
return items[i].Name < items[j].Name
})
Error handling and no silent failure#
Pigweed’s No silent failures rule applies strictly to Go code:
Never swallow errors: Do not write
if err != nil { return nil },_ = fn(), or empty error branches that hide failed RPCs, config parse errors, or subprocess failures.Explicit best-effort contracts: Returning
nilon an error is allowed only when the function’s documented contract is explicitly best-effort (namedTry*orBestEffort*), and the branch includes both a// OK: <reason>comment and a debug log (slog.Debug("...", "err", err)) or structured warning.Wrap errors with
%w: Always wrap underlying errors withfmt.Errorf("context: %w", err)(orerrors.Join) so callers can inspect the cause programmatically.Inspect errors structurally: Use
errors.Isanderrors.As(or HTTPresp.StatusCode) to classify errors. Do not match raw error strings withstrings.Contains(err.Error(), ...)when a typed error or status code is available.
Yes: Propagate and wrap errors with %w
cfg, err := LoadConfig(path)
if err != nil {
return fmt.Errorf("loading config: %w", err)
}
No: Silently discarding an error and returning nil
cfg, err := LoadConfig(path)
if err != nil {
return nil
}
CLI command architecture and state isolation#
Go CLI tools in Pigweed must remain hermetic and safe to test in parallel without process-wide state leaks:
Always use
RunE: Every Cobra command that performs an action must setRunE: func(cmd *cobra.Command, args []string) errorand never voidRun.No
os.Exitoutsidemain:os.Exitis permitted only incmd/main.go. Library and subcommand functions must returnerror(using typed exit-code errors such asExitErrorwhen a specific exit status is required) sodeferstatements run and unit tests can execute in-process.Injected I/O streams: Write all standard output to
cmd.OutOrStdout()and all diagnostics/warnings tocmd.ErrOrStderr(). Never callfmt.Print,fmt.Println, orfmt.Printf, or write directly toos.Stdout/os.Stderrin non-mainpackages.Avoid package-level mutable flag globals: Bind flags to a per-command options struct inside a constructor function (
newFooCommand()) rather than declaring package-level mutablevar fooFlag stringglobals that leak across test cases.
Context propagation and bounded resources#
Plumb
context.Contextfirst: Every function that performs network RPCs, spawns subprocesses (exec.CommandContext), or sleeps must acceptctx context.Contextas its first parameter (derived fromcmd.Context()). Never introducecontext.Background()mid-call-stack.Bound all response reads: Wrap every
io.ReadAll(resp.Body)on external HTTP or subprocess streams inio.LimitReader(resp.Body, maxBytes)with a named constant so a large log or diff cannot exhaust host memory.Bound concurrent fan-out: Any goroutine fan-out over changes, files, worktrees, or builds must bound concurrency with a semaphore channel or worker pool (typically 4–8 concurrent operations) rather than spawning one unbounded goroutine per item.
Testing#
Modern
testing.Thelpers: Uset.Context()for test contexts,t.TempDir()for temporary directories,t.Setenv()for environment variables, andt.Cleanup()to restore any mutated hooks.Table-driven tests and
cmp.Diff: Prefer table-driven subtests (t.Run) and compare structured values withcmp.Diff(want, got)fromgithub.com/google/go-cmp/cmp.Realistic mocks: When mocking external services (such as Gerrit or Buildbucket), verify that the request includes the required query options or parameters in addition to returning canned response payloads.