Files
skills-public/code-languages/go.md
T
BluscreamandClaude Opus 5 32b47b075b fix(coding-dev): prefer a test over the build script for size limits
A shell check only runs where that script runs. A test travels with the repo,
needs no extra tooling, and fails with a message naming the offending files.
Enforcement is now a preference order: native linter rule > test > build
script as fallback, with a worked example and a note on why a per-compile
build hook usually is not worth the incremental-build cost.

Propagated per language: rust (clippy has no file-length lint -> workspace
root test), js-typescript (ESLint max-lines covers both natively, no test
needed), go/python/csharp (linter for functions, test for files), bash (don't
reimplement in the gate what a test can assert).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 12:52:37 +02:00

5.5 KiB

name, description
name description
go Use this skill when writing, building, linting, testing, or releasing Go — including golangci-lint configuration, strict typing over map[string]interface{}, official SDK preference, CGO-free static builds, context handling, and the build gate.

Go — Rules & Preferences

Extends coding-dev.md. Everything there applies; this adds Go specifics.


1. The Gate

gofmt -l .            # must print nothing
go vet ./...
golangci-lint run
go test -race ./...
go build ./...

Wire this into a single entry point (make check, scripts/build.sh) so it cannot be partially run. A skipped test fails the gate. Add govulncheck for binary vulnerability scanning on anything that ships.


2. Linting — Strict Set

golangci-lint with a deliberately broad selection, not the defaults:

linters:
  enable:
    - staticcheck
    - errcheck      # every returned error is handled
    - gosec         # security findings
    - noctx         # no HTTP request without a context
    - revive
    - misspell
    - unconvert
    - bodyclose     # every response body is closed
    - goconst
    - ineffassign
    - nilerr

errcheck and bodyclose catch the two most common real Go bugs. As always: fix rather than suppress, and when suppressing use a specific //nolint:name // reason with the reason.


3. Typing — Concrete, Not Dynamic

  • Strongly typed structs everywhere. Avoid map[string]interface{} and reflection when a struct can be defined — including for JSON, config, and API payloads.
  • Typed constants/enums, not bare strings:
    type SyncStatus string
    
    const (
        StatusSynced   SyncStatus = "synced"
        StatusOutdated SyncStatus = "outdated"
        StatusDirty    SyncStatus = "dirty"
    )
    
  • Concrete error types with errors.Is / errors.As, not string comparison.
  • Keep types shared with a frontend structurally in sync with that frontend's types; when both exist, a change to one is incomplete without the other.

4. Errors

  • Every error is handled. errcheck enforces it; do not _ = an error away without a comment explaining why it cannot matter.
  • Wrap with context: fmt.Errorf("fetching %s: %w", name, err). Always %w, never %v, when the caller might need to inspect it.
  • Sentinel errors as package-level var ErrNotFound = errors.New(...); typed errors when the caller needs data from them.
  • panic only for genuinely unrecoverable programmer error, never for expected runtime conditions. A library never panics on input it was designed to receive.
  • defer the cleanup immediately after acquiring the resource, always.

5. Context

  • Every function that does I/O takes ctx context.Context as its first parameter.
  • Never store a context in a struct field.
  • Never pass nil — context.TODO() marks a genuine gap.
  • Honor cancellation: select on ctx.Done() in loops, and pass ctx down to every HTTP call (noctx enforces this).
  • Set timeouts on outbound requests. A default http.Client has no timeout at all.

6. Dependencies — Official SDKs First

Follow the global reuse-first ordering. In Go, strongly prefer official/first-party SDKs over third-party wrappers — they track API changes and handle auth and pagination correctly.

Choose pure-Go implementations over CGO ones whenever the trade is acceptable: a CGO dependency turns a static single-binary build into a toolchain and glibc problem, and rules out a FROM scratch/Alpine container image.

CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o bin/app ./cmd/app

go mod tidy before committing, and commit go.sum.


7. Concurrency

  • Share memory by communicating. Channels for handoff; sync.Mutex for guarding a struct's own fields. Don't mix both for the same state.
  • go test -race is not optional — it belongs in the gate, not in an occasional manual run.
  • Every goroutine has a defined exit path. errgroup.Group for a set of related goroutines so errors propagate and cancellation fans out.
  • Events over polling — subscribe to a watch/notify API rather than looping on a ticker. Where a ticker is genuinely necessary, make the interval configurable and document why.
  • Never leak a goroutine on a channel nobody reads.

8. Structure

  • cmd/<binary>/ for entry points; pkg/ or internal/ for the actual code.
  • internal/ for anything that must not become public API — it is compiler-enforced.
  • One package per concern; package names are lowercase, single-word, no underscores, and never util or common.
  • Keep the exported surface small. Unexported by default.
  • Embed frontend/static assets with //go:embed for single-binary distribution — the frontend itself follows js-typescript.md.
  • File and function size limits from the general rules apply — funlen in golangci-lint covers functions; put the file limit in a test (see coding-dev.md §4).

9. Testing

  • Table-driven tests are the default idiom.
  • t.Parallel() where tests are genuinely independent.
  • t.TempDir() for filesystem work — it cleans itself up.
  • httptest for HTTP handlers and for faking upstreams; do not hit real services in unit tests.
  • Regression test every fixed bug.
  • Never run destructive operations against real accounts or user data during validation — temporary fixtures only.