go-clean-architecture
Use when scaffolding or refactoring a Go service into a framework-agnostic clean (hexagonal) architecture: Domain, Usecase,…
Use when writing, wrapping, inspecting, or logging Go errors. Covers strategy choice (sentinel vs typed vs opaque), wrapping with %w/%v, errors.Is/As/Join, the log-or-return rule, error strings, and panic/recover boundaries. Apply proactively whenever a function returns or
Claude Code
$ npx -y skills add muratmirgun/gophers --skill go-error-handling --agent claude-code
Codex
$ npx skills add muratmirgun/gophers --skill go-error-handling -a codex -y
Cursor
$ npx skills add muratmirgun/gophers --skill go-error-handling -a cursor -y
Gemini CLI
$ npx skills add muratmirgun/gophers --skill go-error-handling -a gemini-cli -y
GitHub Copilot
$ npx skills add muratmirgun/gophers --skill go-error-handling -a github-copilot -y
OpenCode
$ npx skills add muratmirgun/gophers --skill go-error-handling -a opencode -y
Windsurf
$ npx skills add muratmirgun/gophers --skill go-error-handling -a windsurf -y
name: go-error-handling description: "Use when writing, wrapping, inspecting, or logging Go errors. Covers strategy choice (sentinel vs typed vs opaque), wrapping with %w/%v, errors.Is/As/Join, the log-or-return rule, error strings, and panic/recover boundaries. Apply proactively whenever a function returns or accepts an error, even if the user has not asked about error handling." license: MIT compatibility: "Designed for Claude Code or similar AI coding agents. Requires Go 1.20+ for errors.Join. Wrapping (%w, errors.Is/As) requires Go 1.13+." allowed-tools: Read Edit Write Glob Grep Bash(go:*) Bash(golangci-lint:*)
Errors in Go are values. Treat them as part of the API: choose a strategy per failure mode, propagate with wrapping, inspect with `errors.Is`/`As`, and handle each error **exactly once**.
1. **Errors are values, not exceptions.** Return them; do not panic across API boundaries. 2. **Handle each error exactly once.** *Either* log it *or* return it — never both. 3. **The caller decides what is exceptional.** Library code returns; binaries (or top-level handlers) decide whether to log, retry, or exit. 4. **Wrap only when you add real context.** A wrap that just repeats the function name is noise. Use `%w` to preserve identity; `%v` to deliberately hide an unstable type.
Pick the simplest strategy that meets the caller's needs:
| Strategy | When to use | Example | |---|---|---| | **Opaque error** (default) | Caller only needs to know *something* failed | `errors.New("invalid input")` | | **Sentinel error** | Caller needs to test for a specific named condition | `io.EOF`, `sql.ErrNoRows` | | **Typed error** | Caller needs structured fields (path, code, retry-after) | `*os.PathError`, `*url.Error` | | **Joined errors** | A single operation produced several independent failures | `errors.Join(errA, errB)` |
> Read [references/strategy-decision.md](references/strategy-decision.md) when the caller's needs are unclear or when migrating between strategies without breaking callers.
// Opaque — the caller only checks != nil
return errors.New("invalid character in token")
// Sentinel — exported, package-level, named ErrXxx
var ErrNotFound = errors.New("user: not found")
// Typed — when callers need structured fields
type ValidationError struct {
Field string
Rule string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation: %s violates %s", e.Field, e.Rule)
}if err := db.Get(id); err != nil {
return fmt.Errorf("loading user %d: %w", id, err)
}if errors.Is(err, sql.ErrNoRows) { /* handled */ }
var ve *ValidationError
if errors.As(err, &ve) {
return reply.BadRequest(ve.Field)
}**Never** compare error strings (`err.Error() == "..."`) — strings are not stable API.
errs := errors.Join(
validate(name),
validate(email),
validate(password),
)
if errs != nil {
return errs // errors.Is/As walks both branches
}> Read [references/wrapping-vs-shadowing.md](references/wrapping-vs-shadowing.md) when deciding between `%w` (expose) and `%v` (hide), or when wrapping would leak an implementation detail.
// Bad: caller will log it again, producing duplicate lines
if err := svc.Do(ctx); err != nil {
slog.ErrorContext(ctx, "svc.Do failed", "err", err)
return err
}
// Good: log only at the boundary that decides the request is done
if err := svc.Do(ctx); err != nil {
return fmt.Errorf("doing svc work: %w", err)
}The HTTP handler / job runner / `main` is the only layer that logs.
// Bad
if err == nil {
if x, ok := f(); ok {
return x, nil
}
}
return zero, err
// Good
if err != nil {
return zero, err
}
x, ok := f()
if !ok {
return zero, errSomething
}
return x, nil`panic` is for **programmer errors** (impossible states) and **package initialization**. It is never the right way to return a normal failure.
// Acceptable: invariants the type guarantees
func (q *Queue) MustEnqueue(v T) { if err := q.Enqueue(v); err != nil { panic(err) } }
// Acceptable: recover at the goroutine boundary so one bad request cannot kill the server
defer func() {
if r := recover(); r != nil {
log.Error("panic recovered", "value", r, "stack", debug.Stack())
http.Error(w, "internal error", 500)
}
}()Do **not** use `recover` to convert panics into errors as normal control flow.
For custom error types, implement `Unwrap() error` (or `Unwrap() []error` in Go 1.20+) so `errors.Is`/`As` can reach the cause. See [references/wrapping-vs-shadowing.md](references/wrapping-vs-shadowing.md#custom-unwrap).
| Anti-pattern | Why it hurts | Do this instead | |---|---|---| | `return errors.New(err.Error())` | Drops identity; `errors.Is` breaks | `return fmt.Errorf("ctx: %w", err)` | | `if err.Error() == "EOF"` | String matching against unstable text | `errors.Is(err, io.EOF)` | | `_ = doThing()` | Silently swallows failures | Handle, log at boundary, or document why | | Returning `*MyError` (concrete pointer) | Typed-nil trap; non-nil interface | Return `error` (see [references/typed-nil-trap.md](references/typed-nil-trap.md)) | | Logging then returning the same error | Duplicate log lines, no single source of truth | Log only at the
26 production-grade Go skills for Claude Code, Gemini CLI, and opencode. Battle-tested patterns from the Go community — codified as triggerable AI skills.
Use when scaffolding or refactoring a Go service into a framework-agnostic clean (hexagonal) architecture: Domain, Usecase,…
Invoke this skill to systematically review a Go change against community style standards before merging. Walks the diff topic by…
Use when writing or reviewing Go code for clarity, formatting, control flow, variable declarations, switch usage, and function…
Use when writing or reviewing concurrent Go code — goroutines, channels, select, mutexes, atomics, errgroup, singleflight, worker…
Use when designing, propagating, or debugging context.Context flow in Go — first-parameter placement, deadlines and cancellation,…
Use when writing conditionals, loops, switches, type switches, or blank-identifier patterns in Go. Covers if-with-initialization,…
You MUST use this before any creative work - creating features, building components, adding functionality, modifying behavior, or…
Ask which skill or flow fits your situation. A router over the skills in this repo.