Skip to content
Development
Skill

/go-defensive

Use when hardening Go code at API boundaries: copy slices/maps on entry and return, defer cleanup, verify interface compliance at compile time, model time with time.Time/time.Duration, design enum zero values, prefer crypto/rand, and inject clocks for testability. Apply

From plugin
gophers
826 skills4 agents
Install
$ npx -y skills add muratmirgun/gophers --skill go-defensive --agent claude-code

How it fires

How this skill gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.
  • Slash command/go-defensive

Context preview

The summary Claude sees to decide when to auto-load this skill.

Use when hardening Go code at API boundaries: copy slices/maps on entry and return, defer cleanup, verify interface compliance at compile time, model time with time.Time/time.Duration, design enum zero values, prefer crypto/rand, and inject clocks for testability. Apply

SKILL.md

go-defensive.SKILL.md
name: go-defensive
description: "Use when hardening Go code at API boundaries: copy slices/maps on entry and return, defer cleanup, verify interface compliance at compile time, model time with time.Time/time.Duration, design enum zero values, prefer crypto/rand, and inject clocks for testability. Apply proactively when reviewing for robustness. Error-handling strategy: see go-error-handling."
license: MIT
compatibility: "Designed for Claude Code or similar AI coding agents. `crypto/rand.Text` examples assume Go 1.24+."
allowed-tools: Read Edit Write Glob Grep Bash(go:*) Bash(golangci-lint:*)

Go Defensive Programming

Hardening Go code is not paranoia — it is the discipline of making your boundaries honest. Copy what crosses them, clean up what you opened, model time and randomness honestly, and never let a panic escape a package.

Core Rules

1. **Copy slices and maps at API boundaries.** They are reference types — leaking the backing array leaks mutation. 2. **`defer` the cleanup right after the acquire.** `f, err := os.Open(...); defer f.Close()`. 3. **Verify interface compliance at compile time:** `var _ I = (*T)(nil)`. 4. **Model time and durations with `time.Time` and `time.Duration`,** never raw ints. 5. **Inject `now func() time.Time`** instead of calling `time.Now()` directly in production code. 6. **Enums start at `iota + 1`** so the zero value is invalid. 7. **`crypto/rand` for secrets, never `math/rand`.** 8. **Panics never cross package boundaries.** Convert to errors at the edge. 9. **Avoid mutable package-level state.** Inject dependencies instead.

Boundary Hardening Checklist

When you touch an exported function or method, walk this list in order:

| # | Check | |---|---| | 1 | Return errors, don't panic across boundaries | | 2 | Copy slices/maps you'll retain | | 3 | Copy slices/maps you'll return if internal state aliases them | | 4 | `defer` Close / Unlock / cancel right after the acquire | | 5 | Compile-time interface satisfaction check | | 6 | `time.Time` / `time.Duration` types, injected clock | | 7 | Enum zero = invalid (`iota + 1`) | | 8 | `crypto/rand` for any secret material |

Copy at API Boundaries

// Receiving: copy a slice we'll retain
func (d *Driver) SetTrips(trips []Trip) {
    d.trips = make([]Trip, len(trips))
    copy(d.trips, trips)
}

// Returning: copy a map so callers can't mutate our state
func (s *Stats) Snapshot() map[string]int {
    out := make(map[string]int, len(s.counters))
    for k, v := range s.counters {
        out[k] = v
    }
    return out
}

> Read [references/boundary-copying.md](references/boundary-copying.md) when deciding which boundaries actually need copies (and when copying is wasted work).

Defer Cleanup

`defer` evaluates arguments at the `defer` statement and runs the call when the surrounding function returns (LIFO order):

f, err := os.Open(name)
if err != nil {
    return err
}
defer f.Close()

Place `defer` immediately after the acquire — the proximity makes pair-correctness reviewable at a glance.

For locks:

mu.Lock()
defer mu.Unlock()

Beware of `defer` inside loops — accumulated defers run only when the function returns, not when the iteration ends.

Verify Interface Compliance

var _ http.Handler = (*Handler)(nil)

If `(*Handler)` ever stops satisfying `http.Handler`, the build fails. The line costs nothing at runtime and gives you a free contract.

Time Modeling

// Bad — what unit is timeout?
type Config struct {
    Timeout int
}

// Good
type Config struct {
    Timeout time.Duration
}

For wall-clock work, inject the clock so tests can pin time:

type Signer struct {
    now func() time.Time
}

func NewSigner() *Signer {
    return &Signer{now: time.Now}
}

// In tests:
s := &Signer{now: func() time.Time { return fixedTime }}

> Read [references/time-and-enums.md](references/time-and-enums.md) for monotonic time, time zones, struct tags, and embedding tradeoffs.

Crypto Random

import "crypto/rand"

// Go 1.24+
func APIKey() string { return rand.Text() }

`math/rand` and `math/rand/v2` are predictable from a seed — never use them for keys, tokens, nonces, or any secret material.

Must Functions

`Must*` helpers panic on error. They are appropriate **only** at program initialization, where failure means the program cannot start:

var (
    validID = regexp.MustCompile(`^[a-z][a-z0-9-]{0,62}$`)
    tmpl    = template.Must(template.ParseFiles("index.html"))
)

Don't write `MustFoo` for runtime call sites — it shifts an error condition into a crash.

> Read [references/must-and-panic.md](references/must-and-panic.md) for writing custom `Must*`, recovering at goroutine boundaries, and distinguishing `panic` from `log.Fatal`.

Avoid Mutable Globals

// Bad — testing requires save/restore dance
var DB *sql.DB

// Good — pass the dependency
type Service struct {
    db *sql.DB
}

Constants and once-initialized lookup tables are fine. Mutable package-level vars are a code smell.

Anti-Patterns

| Anti-pattern | Why it hurts | Do this instead | |---|---|---| | Storing the caller's slice without copying | Mutation aliasing | `make` + `copy` | | Returning the internal map directly | External mutation of state | Return a snapshot | | `time.Now()` in business logic | Hostile to tests | Inject `now func() time.Time` | | `var Timeout = 5` read as seconds elsewhere | Ambiguous unit | `time.Duration` | | `math/rand` for keys | Predictable from seed | `crypto/rand` | | `panic` to signal a domain error | Crashes the caller | Return an error | | `defer` inside a tight loop | Defers stack until function return | Wrap loop body in a function |

Verification Checklist

  • [ ] Slices/maps stored from callers, or returned aliasing internal state, are copied
  • [ ] Every `Open`/`Lock` has a `defer Close`/`Unlock` next to it
  • [ ] Compile-time interface checks cover exported implementations
  • [ ] Duratio
Read more
Ships withgophers

26 production-grade Go skills for Claude Code, Gemini CLI, and opencode. Battle-tested patterns from the Go community — codified as triggerable AI skills.

Get the whole plugin

Other skills on gophers.