| name | golang-expert |
|---|---|
| description | Idiomatic Go best practices for writing clean, performant, and maintainable Go code. Apply when writing, reviewing, or refactoring any Go code — including modules, packages, error handling, concurrency, CLI tools, file I/O, security, and tooling. |
Comprehensive guide to idiomatic Go, covering language best practices, design patterns, concurrency, CLI tool creation, file I/O, security, testing, and tooling. Apply these rules whenever writing or reviewing Go code to ensure correctness, clarity, and long-term maintainability.
Reference these guidelines when:
- Writing new Go packages, modules, or services
- Reviewing Go code for correctness and idiomatic style
- Refactoring existing Go code
- Designing interfaces and package APIs
- Writing concurrent code with goroutines or channels
- Building CLI tools or command-line interfaces
- Handling file I/O, streaming, or file transfer
- Implementing secure Go code
- Writing tests or benchmarks
- Setting up Go tooling in CI/CD pipelines
| Priority | Category | Impact | Prefix |
|---|---|---|---|
| 1 | Code Organisation | CRITICAL | org- |
| 2 | Error Handling | CRITICAL | error- |
| 3 | Security | CRITICAL | sec- |
| 4 | Interfaces & Composition | HIGH | iface- |
| 5 | Concurrency | HIGH | conc- |
| 6 | CLI Tool Creation | HIGH | cli- |
| 7 | File I/O & Strings | HIGH | io- |
| 8 | Testing | MEDIUM-HIGH | test- |
| 9 | Performance | MEDIUM | perf- |
| 10 | Tooling & Linting | MEDIUM | tool- |
org-module-structure— one module per repository;go.modat the rootorg-package-naming— short, lowercase, singular nouns; no underscores or mixedCase (user, notuserService)org-package-cohesion— organise packages by domain concept, not by layer (user/, notmodels/,controllers/,services/)org-internal— useinternal/to prevent external packages from importing unexported APIsorg-cmd— keepmainpackages thin incmd/<name>/main.go; all logic lives in importable packages
error-explicit— never ignore an error; assign to_only when intentional and documentederror-wrapping— add context withfmt.Errorf("doing X: %w", err); preserve the original error forerrors.Is/errors.Aserror-sentinel— define sentinel errors withvar ErrFoo = errors.New("foo")for values callers compare againsterror-types— use custom error types (implementingerror) when callers need to inspect structured error dataerror-no-panic— do not usepanicfor recoverable errors; reserve it for truly unrecoverable programmer mistakes
sec-no-hardcoded-secrets— never hardcode secrets, tokens, or passwords; read from environment variables or a secrets managersec-crypto-rand— usecrypto/randfor all security-sensitive random values; never usemath/randfor tokens or noncessec-constant-time— compare secrets and tokens withsubtle.ConstantTimeCompareto prevent timing attackssec-tls-config— always usetls.ConfigwithMinVersion: tls.VersionTLS12; never setInsecureSkipVerify: truein productionsec-exec-no-shell— useexec.Command("bin", arg1, arg2)with explicit args; never interpolate user input into a shell stringsec-sql-parameterised— always use parameterised queries (db.QueryContext(ctx, "SELECT ... WHERE id=?", id)); never concatenate SQL stringssec-input-validation— validate and sanitise all external input at the boundary; fail fast with a clear error
iface-small— prefer single-method interfaces (io.Reader,io.Writer); the smaller the interface, the more implementations it acceptsiface-accept-return— accept interfaces, return concrete types; this maximises flexibility for callersiface-define-at-use— define interfaces in the package that uses them, not the package that implements themiface-composition— compose larger interfaces from smaller ones rather than defining monolithic interfaces
conc-context— acceptcontext.Contextas the first parameter of any function that does I/O or can be cancelled; never store context in a structconc-goroutine-cleanup— every goroutine must have a clear owner and a defined exit path; usesync.WaitGrouporerrgroupto wait for completionconc-channel-ownership— the goroutine that creates a channel is responsible for closing it; never close a channel from the receiver sideconc-mutex-vs-channel— use channels for transferring ownership or signalling; usesync.Mutexfor protecting shared stateconc-race-detector— always run tests with-race; enable it in CI (go test -race ./...)
cli-cobra-structure— usecobrafor multi-command CLIs; one*cobra.Commandper file undercmd/; root command incmd/root.gocli-flag-validation— validate all flags inPersistentPreRunEorRunE; return an error rather than callingos.Exitdirectlycli-exit-codes— exit 0 on success, 1 on user error, 2 on internal/unexpected error; useos.Exitonly inmaincli-stderr-stdout— write human-readable output tostdout; write errors, warnings, and progress tostderrcli-embed-config— use//go:embedto bundle default config templates, completion scripts, or static assets into the binarycli-heredoc-templates— use raw string literals (backticks) withtext/templatefor multi-line output templates; avoid hand-built string concatenationcli-shell-completion— generate shell completion scripts viacobra completion; add acompletionsubcommand
io-stream-not-buffer— useio.Copyto stream file content; never read an entire file into memory withos.ReadFileunless it is provably smallio-buffered-rw— wrap file reads/writes inbufio.NewReader/bufio.NewWriter; flush writers explicitly withdefer w.Flush()io-atomic-write— write to a temp file in the same directory, then rename; this prevents partial writes corrupting the destinationio-close-defer— alwaysdefer f.Close()immediately after a successfulos.Open; check the error onClosefor writersio-filepath-not-path— usepath/filepath(notpath) for OS file paths; usepathonly for URL path segmentsio-heredoc-raw-strings— use raw string literals (`) for multi-line strings, SQL, JSON templates, and scripts; avoid escape-heavy interpreted stringsio-embed-assets— use//go:embedto include static files, templates, and schemas in the binary at compile time
test-table-driven— use table-driven tests ([]struct{ name, input, want }) for all non-trivial logictest-subtests— uset.Run(tc.name, func(t *testing.T) {...})inside table-driven loops for isolated, named failurestest-interface-mocking— mock dependencies via interfaces, not concrete types or monkey-patchingtest-golden-files— use golden files (testdata/*.golden) for complex expected outputs; update with-updateflagtest-benchmarks— writeBenchmarkXxxfunctions for performance-critical paths; run withgo test -bench=.
perf-avoid-allocations— profile withpprofbefore optimising; minimise heap allocations in hot paths usingsync.Poolor pre-allocated slicesperf-strings-builder— usestrings.Builder(orbytes.Buffer) for string concatenation in loops; never use+=in a loopperf-slice-capacity— pre-allocate slices withmake([]T, 0, n)when the final size is knownperf-struct-layout— order struct fields from largest to smallest alignment to minimise padding
tool-gofmt— always rungofmt -s(orgoimports); enforce in CI withgofmt -l .exiting non-zero on difftool-go-vet— rungo vet ./...as part of every CI build; it catches correctness issuesgofmtmissestool-golangci-lint— usegolangci-lint runin CI with a committed.golangci.yml; enable at minimumerrcheck,staticcheck,gosimple,unusedtool-go-generate— use//go:generatedirectives for code generation; commit generated files so the build does not require external tools at runtime
Apply rules by ID when reviewing or writing Go code. Read individual rule files in references/ for detailed explanations and before/after code examples:
references/error-handling-patterns.md
references/security-patterns.md
references/concurrency-patterns.md
references/cli-patterns.md
references/io-and-strings-patterns.md
references/interface-patterns.md
references/testing-patterns.md
references/code-organisation-patterns.md
See references/rule-index.md for the full list of all rules mapped to their local files.