Go Writing
Keep Go code boring, explicit, version-aware, and easy to verify. Start from repository conventions, then use official Go guidance and version-specific modernization references to break ties or fill gaps.
Start With Local Context
- Read
go.mod,go.work,mise.toml,.mise.toml, or.tool-versionswhen present, plus the changed package, adjacent tests, and build or CI scripts before proposing structure. - Treat the module or workspace
godirective as the minimum language and compatibility target. - Check
toolchaindirectives,miseconfig, CI or container toolchains, and deployment/runtime constraints before suggesting tool invocations or standard-library APIs that may depend on the actual Go toolchain in use. - Follow repository conventions before generic advice.
- Prefer small, local changes. Add abstractions only after the concrete shape is clear.
- Use the repository's existing version manager. Prefer
miseonly for new or deliberately modernized repositories with no established alternative. - Open references/official-go-guidance.md when the repository is ambiguous or the design tradeoff is language-level rather than project-specific.
- Open references/go-1.26-modernization.md when the user asks for the latest Go patterns, when the repo already targets Go 1.26+, or when planning a toolchain upgrade.
- Open ../go-cli-writing/SKILL.md when the work is primarily about CLI structure, subcommands, flag UX, or command output.
Work In This Order
- Inspect the package boundary, dependencies, tests, and module version.
- Decide whether the problem needs a new type, function, interface, goroutine, or dependency at all.
- Write the smallest clear implementation that matches the surrounding package.
- Add or update tests and benchmarks before optimizing.
- Run the narrowest useful verification loop, then the repo's broader checks.
Shape The API
- Choose package names that are short, lower-case, and non-stuttering.
- Keep exported APIs small and documented. Add doc comments for exported identifiers, and add a package comment when creating a new package.
- Start with concrete types and functions. Introduce an interface only at the consuming boundary or when multiple implementations already exist.
- Accept small interfaces when they model the exact behavior needed. Return concrete types unless hiding the concrete type is a deliberate API choice.
- Make the zero value useful when practical. Add constructors only for invariants, required dependencies, or resource ownership.
- Keep data ownership obvious. Pass small values by value; use pointers for mutation, large structs, or shared identity.
- Use pointer receivers for mutation, large structs, or structs containing locks. Use value receivers for small immutable values.
- Keep receiver style consistent on a type. Do not mix pointer and value receivers without a deliberate method-set reason.
- Keep
context.Contextas the first parameter on request-scoped work, blocking operations, and cancellable I/O. Do not store it in a struct. - Avoid package names such as
util,common,misc,types, andapiunless the repository already normalized them.
Organize Files For Cohesion
- Optimize file layout for cohesion and reader navigation, not an arbitrary line-count limit.
- Split long files only when the new file creates a real boundary such as a distinct type family, protocol, platform, generated code path, or test fixture area.
- Keep a type and its methods close together. Do not split methods with the same receiver across files just to shorten files unless there is a strong boundary such as build tags or generated code.
Write The Implementation
- Prefer straight-line control flow with early returns over deep nesting.
- Return errors instead of logging or exiting inside library code.
- Do not use
panicfor normal error handling. - Use
errors.Newfor fixed values andfmt.Errorf("...: %w", err)when preserving a cause matters. - Use
%vinstead of%wat API or trust boundaries when callers should not depend on the underlying implementation detail. - Handle each error once. Either return it with context or log it while degrading gracefully.
- Do not discard errors with
_unless the ignore is deliberate, local, and obvious to the reader. - Keep error strings lower-case and without trailing punctuation.
- Keep wrapping context short. Prefer
open config: %wover repeatedfailed to ...chains. - Use
errors.Is,errors.As, anderrors.Joinwhen the caller needs machine-readable error behavior. - Keep helpers small, cohesive, and near their callers unless reuse is already proven.
- Avoid speculative generalization, reflection-heavy code, and hidden magic.
- Avoid new dependencies for trivial helpers when the standard library already covers the job.
- Prefer standard-library helpers such as
cmp,maps, andslicesbefore adding custom glue code.
Shape Data Carefully
- Prefer nil slices over empty slices unless an external contract requires
[]instead ofnull. - Preallocate slices when the final size is known or tightly bounded.
- Copy slices or maps at ownership boundaries when retaining mutable caller-provided data.
- Use field names in struct literals outside tightly local tests.
- Avoid embedding locks or other implementation details in exported structs.
Use Concurrency Deliberately
- Add goroutines only when they improve latency, throughput, or responsiveness in a clear way.
- Thread context cancellation through concurrent work.
- Use
errgroupfromgolang.org/x/sync/errgroupfor sibling tasks that fail or cancel together when the repository already usesx/syncor the dependency is justified. Otherwise use a clear standard-library coordination pattern. Usesync.WaitGrouponly for simple coordination. - Prefer synchronous function signatures; let callers opt into concurrency.
- Document how each goroutine exits. Treat goroutine lifetime as part of the design.
- Let the sending side close channels and keep channel ownership explicit.
- Default channel sizes to
0or1. Larger buffers need a reason tied to load, backpressure, or batching. - Protect shared mutable state or remove the sharing.
- Use typed atomics from
sync/atomicwhen atomics are truly the right tool.
Test Behavior First
- Start with a failing test when changing behavior.
- Prefer table-driven tests for input or state matrices, and use subtests to name cases.
- Use
t.Helper()in helpers andt.Parallel()only when the test and its fixtures are concurrency-safe. - Test observable behavior and error semantics, not private implementation details.
- Prefer standard-library assertions unless the repository already standardizes on another test helper library.
- Check wrapped errors with
errors.Isanderrors.As. - Add fuzz tests for parsers, decoders, and input-validation code when the risk justifies it.
- Add benchmarks before claiming a performance win. Use
pprofonly after measuring a real bottleneck.
Verify Before Finishing
- Run the narrowest useful package test loop while iterating, then run the repository's broader verification path.
- Use repository tooling if it exists. Otherwise default to
gofmt -w,go test ./..., andgo vet ./...; addgo test -race ./...for concurrency changes andgo mod tidywhen dependencies change. - After a Go toolchain upgrade or modernization pass, consider
go fix ./...before manual cleanup. - Regenerate generated files instead of hand-editing them unless the repository explicitly treats them as maintained sources.
- Keep imports and module metadata consistent with the repository workflow.
- If the repository lacks linting, recommend an appropriate lint path rather than introducing tooling as part of unrelated work.
Output Expectations
- Explain any non-obvious Go tradeoff in one or two sentences.
- Call out repository-specific deviations from generic Go advice when they matter.
- State when an idea needs a higher module or toolchain version than the repository currently declares.
- Prefer examples that match the package already being edited instead of inventing a new style.