Skip to content
Back to skills

Go Conventions

ASecurity

Use when a ticket adds or changes Go code and it must follow the repo's Go conventions — idiomatic Go (Effective Go), explicit error handling and wrapping, small interfaces, correct pointer-receiver rules, goroutines with an owner and an exit, and table-driven tests run with the race detector; go vet / staticcheck / golangci-lint clean. Invoke for "add this in Go", "fix the go vet/build issues", "add the handler/service", "fix the goroutine leak", or as the language pack for any Go change or ...

  • 2 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added September 5, 2026
ai-agentsgotestingdatabasesecurity

Works with

  • cli
  • mcp

Security analysis

A100/100

Scanned September 29, 2026

npx -y skills add tmj-90/gaffer --skill go-conventions --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Go Conventions?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Go Conventions
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/tmj-90-go-conventions/badge)](https://www.skillsdirectory.com/skills/tmj-90-go-conventions)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: go-conventions
description: Use when a ticket adds or changes Go code and it must follow the repo's Go conventions — idiomatic Go (Effective Go), explicit error handling and wrapping, small interfaces, correct pointer-receiver rules, goroutines with an owner and an exit, and table-driven tests run with the race detector; go vet / staticcheck / golangci-lint clean. Invoke for "add this in Go", "fix the go vet/build issues", "add the handler/service", "fix the goroutine leak", or as the language pack for any Go change or Go review.
stack: [go]
area: language
---

# Write idiomatic Go

Go rewards plain code: every error checked and wrapped, small interfaces, goroutines
that always end. For the builder and the reviewer of a Go diff; the repo's config and existing code win over it.

## Procedure

1. **Discover the repo's conventions first.** Call `search_lore` for Go conventions.
   Read `go.mod` (the `go` directive gates features: per-iteration loop variables need
   ≥ 1.22, `wg.Go` and `testing/synctest` ≥ 1.25), `go.work`, `.golangci.yml` (v2 files
   start `version: "2"`), `staticcheck.conf`, the `Makefile`, and the CI workflow. Open
   the nearest sibling package and its `_test.go` and copy their layout, error style,
   logging (`log/slog` or the repo's logger) and test helpers. Never hand-edit a file
   headed `// Code generated … DO NOT EDIT.`; change its source and run `go generate`.
2. **Pin the exact commands** from CI or the `Makefile` (the `run-tests` and `run-lint`
   skills). Use the defaults below only when the repo defines none.
3. **Write the change with the idioms below**, then walk the concurrency section for
   every goroutine, shared value, file and outbound call you touched.
4. **Test each acceptance criterion's own behaviour.** One test (or `t.Run` case) per AC
   that fails without your change, plus its error path — table-driven with `t.Run`, and
   `t.Helper()` in assertion helpers. If the AC involves shared state
   or persistence, add a test that runs N goroutines against it concurrently under
   `-race` and asserts the invariant (no lost update, no error, exactly one winner).
5. **Verify, then stop.** Done when: `gofmt -l .` prints nothing, build/vet/lint are clean
   at the repo's configuration, `go test -race` is green, `go mod tidy -diff` is empty, and
   every AC has a test. Record the output with the `record-evidence` skill; the runner
   submits the work.

## Commands

- Format: `gofmt -l .` (or `goimports -l .`) must print nothing.
- Build and vet: `go build ./...` and `go vet ./...`.
- Lint: `golangci-lint run ./...` when configured, else `staticcheck ./...` when configured.
- Test: `go test -race -count=1 ./...`; one case: `go test -race -run 'TestX/case' ./pkg/x`.
- Dependencies: `go mod tidy -diff` (Go 1.23+) reports drift without rewriting
  `go.mod`/`go.sum`. Never `go get` or let `go mod tidy` add a module: a new or bumped
  dependency is a blocker (the `dependency-upgrade` skill). `govulncheck ./...` if
  installed.
- Do not run `go fix ./...` (Go 1.26 modernizers) across the tree unless the ticket asks;
  it rewrites unrelated code.

## Idioms that matter

- **Errors are values.** Check every one; return early; wrap with context of what you
  were doing: `fmt.Errorf("load config %s: %w", path, err)`. Match with
  `errors.Is`/`errors.As`, never `==` or string comparison. Handle an error once:
  log it or return it, not both. `panic` only for programmer errors.
- **Interfaces**: small, defined where they are consumed; accept interfaces, return
  concrete types; no interface with one implementation "for later".
- **`context.Context`** is the first parameter (`ctx`) of anything that blocks or does
  I/O, is never stored in a struct, and is passed down, not replaced with
  `context.Background()` mid-request.
- **Receivers**: pointer receivers when the method mutates or the type holds a mutex or
  is large; keep one kind across a type's method set.
- **Names**: initialisms upper-case (`ID`, `URL`), no stutter (`user.Service`, not
  `user.UserService`), no `util`/`common` packages.

## Concurrency and resource safety

- **Every goroutine has an owner and an exit.** Start it under `errgroup.WithContext` or a
  `sync.WaitGroup` (`wg.Go` on 1.25+, otherwise `wg.Add` *before* `go`, never inside the
  goroutine), and make it return on `ctx.Done()`. A send on an unbuffered channel whose
  receiver has timed out blocks forever: buffer it or `select` on `ctx.Done()`.
- **Maps are not safe for concurrent writes** (the runtime aborts the process). Guard shared
  maps and slices with a `sync.Mutex`; never copy a struct that contains one (`go vet`
  copylocks).
- **Read-modify-write under one critical section.** A `Load` then a separate `Store`, or
  two lock scopes around read and write, loses updates. Use one lock across the whole
  operation, `atomic.Int64`/CompareAndSwap, or a database transaction/version check.
- **Files other requests read**: `os.WriteFile` is not atomic. Write to
  `os.CreateTemp(filepath.Dir(dst), ".name-*")` (unique name, same filesystem), `Sync`,
  check the `Close` error, then `os.Rename`. Never a fixed temp name or one built from a
  timestamp or PID.
- **Cross-process locks** use an OS lock or the database. A lease that expires on
  wall-clock time alone lets a paused holder write after another took over; the write
  must check a fencing token or version.
- **Close what you open**: `defer resp.Body.Close()` after the error check, `rows.Close()`
  plus `rows.Err()`, `defer cancel()` for every `WithTimeout`/`WithCancel` (vet
  lostcancel), `ticker.Stop()`. A `defer` inside a loop runs only at function return:
  extract the loop body.
- **Outbound HTTP** needs a timeout (`http.Client{Timeout: …}` or a deadline on the
  request context); `http.DefaultClient` has none. Servers set `ReadHeaderTimeout`.
- **Aliasing**: `append` may write into a caller's backing array; `slices.Clone` before
  retaining or mutating a slice you were given.

## Review checklist — flag as defects

Walk this against the diff. An item is grounds for CHANGES only when, in changed code, it
causes a concrete failure (wrong result, crash, lost or corrupted data, security hole) or
leaves an AC's own behaviour untested: cite the line and that failure. Otherwise it is an
`(optional)` note. Formatting the tools would fix, and preferences the repo
does not enforce, are not findings. Do not patch the code under review.

- [ ] An `error` is ignored, assigned to `_` without a reason, or shadowed by `:=` in an
      inner scope so the outer function returns nil.
- [ ] An error is wrapped with `%v` (or compared with `==`) where callers use
      `errors.Is`/`errors.As`.
- [ ] A function returns a typed nil pointer as `error`, so `err != nil` is true.
- [ ] A goroutine has no exit path or owner, `wg.Add` runs inside the goroutine, or a
      fan-out is unbounded.
- [ ] A map, slice or struct is written from more than one goroutine without a lock, or a
      read-modify-write is split across lock scopes.
- [ ] A body, rows, file or ticker is not closed; a writable file's `Close` error is
      ignored; a cancel func is dropped; `defer` accumulates in a loop.
- [ ] `context.Background()` replaces the request context, or a context is stored in a struct.
- [ ] A shared file is overwritten in place or through a fixed temp name; a lock relies on
      a time-only lease with no fencing check.
- [ ] An outbound call has no timeout.
- [ ] A generated file was edited by hand, or `go.mod`/`go.sum` are not tidy.
- [ ] An AC has no test, the test does not reach the AC's code path, or a concurrency AC
      has no concurrent test run with `-race`.

## Capture lore

This skill is one of the places durable, reusable knowledge naturally surfaces:
**A Go convention this repo enforces beyond the obvious — an error-wrapping pattern, a package-boundary rule, a concurrency invariant, or a linter constraint.** That kind of fact is *lore*. Capture it via the **lore-capture
protocol in your brief** (`CLAUDE.factory.md`, step 11 "Memory contribution"):
call the Memory MCP `suggest_lore` once at the close of your work — reusable
conventions, gotchas, decisions, and boundaries only, never per-ticket trivia.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…