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
  • 3 views
  • 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…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.