Go Engineering
Guidance for producing Go that reads like it belongs in the standard library: clear, small, boring, and easy to delete.
Scope
Three kinds of correctness, and this skill owns one and a half of them.
| Owned by | This skill | |
|---|---|---|
| Mechanical — formatting, unchecked errors, deprecated APIs, format verbs | gofmt, go vet, golangci-lint |
Defers to the tools; helps read and triage what they report |
Structural — data races, goroutine leaks, unreleased resources, stored ctx, aliasing, API shape, package boundaries |
Judgment | Yes. This is the core of it. |
| Semantic — does the code do what the requirements say | Tests, and a reviewer who knows the domain | No. |
That last row is a hard boundary, not a weakness to apologize for. Code can be flawless by every rule in this skill and still compute the wrong answer. When reviewing, say plainly that you have checked structure and idiom, and that whether the behavior is correct is a separate question the tests must answer.
Two corollaries:
- Don't claim a clean review means the code works. It means the code is well-formed.
- Don't stretch for semantic findings to seem thorough. If the logic looks wrong, say so as an ordinary observation, not as a finding from this skill.
How to use this skill
- Run the tooling first. Most style questions are already answered by
gofmt,go vet, and the project's owngolangci-lintconfig. Don't hand-argue what a linter can decide. Seereferences/linting.md. - Match the surrounding code. Local consistency beats a marginal improvement that makes a file internally inconsistent.
- Load the reference file for the decision you're actually making (routing table below). Don't read all of them.
- Apply the tiers honestly (next section). Don't escalate taste into law.
Normativity tiers
This skill labels guidance so you can tell settled practice from opinion. When you give advice or review code, carry the tier with you.
| Tier | Meaning | How to act on it |
|---|---|---|
| [Rule] | Canonical across Effective Go, Go Code Review Comments, and the Google Go Style Guide, or enforced by the toolchain. | Follow it. A deviation needs a comment explaining why. Flag violations in review. |
| [Convention] | Normative in the sources and consistent across the ecosystem, but with legitimate exceptions. | Follow it by default. Deviating locally is fine with a reason. Raise in review as a suggestion, not a blocker. |
| [Judgment] | A real engineering trade-off with no single right answer. | Present the axes and pick one. Never report a judgment call as a rule violation in review. |
Three failure modes to avoid:
- Inventing rules. If none of the sources say it and it isn't tool-enforced, it's [Judgment]. Say so.
- Applying Google-internal conventions to public code. Several rules in the Google guide exist for Google's monorepo and internal libraries and do not generalize. They are marked Google-internal throughout and catalogued in
references/sources.md. - Citing stale advice. Effective Go and Go Code Review Comments predate generics,
errors.Is/As,log/slog, and Go 1.22 loop semantics. Seereferences/modern-go.mdbefore repeating pre-1.18 advice.
Style priorities
When two good options conflict, resolve in this order (Google Go Style Guide):
- Clarity — the code's purpose and rationale are clear to the reader.
- Simplicity — it accomplishes its goal the simplest way.
- Concision — high signal-to-noise ratio.
- Maintainability — it can be changed easily and correctly.
- Consistency — it looks like the rest of the codebase.
This is a tiebreaker order, not a scoring function. Concision never justifies obscurity; consistency never justifies propagating a bug.
Routing
| Decision you're making | Read |
|---|---|
| Naming a package, type, func, var, receiver, or constant | references/naming.md |
| Designing a function signature, interface, or exported API; generics; pointers vs values; zero values; options | references/api-design.md |
| Returning, wrapping, inspecting, logging, or defining errors; panic vs error | references/errors.md |
Goroutines, channels, mutexes, context, cancellation, races, lifetimes |
references/concurrency.md |
Package boundaries, file layout, imports, dependency direction, internal/ |
references/packages.md |
| Writing tests, table tests, fakes vs mocks, benchmarks, fuzzing, assertions | references/testing.md |
| Doc comments, package docs, examples, what deserves a comment | references/documentation.md |
| Reviewing a diff or PR; what to block on vs mention | references/code-review.md |
| Running golangci-lint against the project's config | references/linting.md |
| Version-gated features; which old advice is obsolete | references/modern-go.md |
| Which source says what, and where sources conflict | references/sources.md |
The short list
The rules that come up constantly. Everything here is [Rule] unless marked.
Formatting and naming
- All code is
gofmt-clean. No exceptions, including generated code. MixedCaps/mixedCaps, neverunder_scoresorALL_CAPS— constants included.- Initialisms keep uniform case:
URL,ID,HTTP,userID,parseURL. NeverUrl,Id. - Package names are short, lowercase, no underscores, and not
util,common,base,helpers, ormisc. - Don't stutter:
http.Server, nothttp.HTTPServer. Callers already type the package name. - No
Getprefix on getters:u.Name(), notu.GetName(). - Receiver names are one or two letters, consistent across every method on the type. Never
this,self, orme. - Variable name length scales with scope:
iin a three-line loop,remainingRetriesat package level.
Errors
erroris the last return value.nilmeans success. [Rule]- Error strings are lowercase and have no trailing punctuation — they get embedded in longer messages. Exception: strings starting with an exported name, proper noun, or acronym.
- Never silently discard an error with
_. Handle it, return it, or explain in a comment why dropping it is correct. - Don't use
panicfor ordinary failure. Errors are values; return them. - Keep the happy path at minimal indentation — handle the error and return early, no
elseblock. - No in-band error values (
-1,"",nilmeaning "not found"). Return an extraerrororbool.
Types and APIs
- Interfaces belong in the consumer package, not the implementation's. Don't define an interface before there's a real second use or a real boundary.
- Interfaces should be small. One or two methods is normal.
- Don't pass pointers just to avoid copying a few bytes. Pass a pointer when you need mutation, identity, nilability, or the value is genuinely large.
- Don't mix value and pointer receivers on one type. [Convention]
- Design types whose zero value is useful (
sync.Mutex,bytes.Buffer). - Use
any, notinterface{}, in new code.
Concurrency
context.Contextis the first parameter, namedctx. Never store one in a struct field. Never define a custom context type or a context-like interface. [Rule — the Google guide states this one admits no exceptions]- When you start a goroutine, it must be obvious when and how it exits. Unbounded or undocumented goroutine lifetimes are a defect.
- Prefer synchronous functions. Let the caller add concurrency; they can't remove yours.
- Use
crypto/rand, nevermath/rand, for keys, tokens, nonces, or session IDs.
Testing
- Failure messages say what was called, with what input, what you got, and what you wanted —
gotbeforewant. - Tests keep going: prefer
t.Errorovert.Fatalunless the test genuinely cannot continue. - Compare structs with
cmp.Diff, not hand-written field-by-field checks. [Convention]
Documentation
- Every exported name has a doc comment starting with the name itself, written as a complete sentence.
- The package comment sits immediately above
package x, with no blank line.
Workflows
Writing new Go code
- Check
go.modfor the language version — it gates loop semantics, generics, and stdlib availability (references/modern-go.md). - Read a neighboring file first. Adopt its naming, error style, and test style.
- Write the smallest thing that works. Concrete types over interfaces; functions over types; a struct over a framework.
- Design the zero value to be usable, or provide exactly one constructor.
- Handle every error at the point you get it. Add context that the caller doesn't already have.
gofmt→go build ./...→go vet ./...→go test ./...→ project'sgolangci-lint.
Designing an exported API
Load references/api-design.md. The checklist:
- Can a caller misuse this without the compiler stopping them? Tighten the types.
- Is every exported symbol necessary? Unexport what you can — you can always export later, never the reverse.
- Does it take an interface it doesn't need? Take the narrowest thing that works.
- Does it return an interface where a concrete type would tell the caller more? Usually return the concrete type.
- Is the zero value meaningful, or does it panic?
- Does it accept
ctxfirst, returnerrorlast? - Is it safe for concurrent use, and does the doc say so either way?
Reviewing Go
Load references/code-review.md. In order of what matters:
- Correctness — races, ignored errors, nil derefs, goroutine leaks, resource leaks, loop/slice aliasing.
- API surface — is the exported shape something you can live with for years?
- Clarity — will a new reader follow this?
- Tests — do they test behavior, and do they fail usefully?
- Style — last, and only what the linter didn't catch.
Say the tier out loud: "blocking: this leaks a goroutine" vs "nit, non-blocking: I'd name this n." Don't dress preferences as rules.
Linting
Always prefer the project's own configuration; never substitute your own.
golangci-lint config path # what config is actually in effect
golangci-lint run ./... # full run
golangci-lint run --new-from-rev=HEAD~ # only what this change introduced
golangci-lint fmt # apply configured formatters
If the project has no config, run the default linter set and report findings — do not add a .golangci.yml unless asked. Details and failure modes: references/linting.md.
What not to do
- Don't create a
.golangci.yml, change lint settings, or add//nolintdirectives to make a run pass. Fix the code or report the finding. - Don't refactor beyond the task to satisfy a style preference.
- Don't add an interface, a factory, or a layer of indirection "for testability" before there is a second implementation or a real external dependency.
- Don't reach for a framework where a
structand a function will do. - Don't quote this skill as authority for a [Judgment] call.