go-defensive-coding — Claude Code skill: setup and features
Stops panics, slice aliasing, and subtle runtime bugs in Go: typed-nil interfaces, numeric truncation, and loop defer hazards.
npx skills add eduardo-sl/go-agent-skills --skill go-defensive-coding -a claude-codeWhat go-defensive-coding does
The skill instructs coding agents on Go defensive programming practices. It flags edge cases that compile without warnings: non-nil interface values wrapping nil pointers, memory retention from sub-slices, and silent data corruption during narrowing numeric conversions.
It operates in two distinct modes. In Harden mode, the agent enforces bounds checks, defensive copies via slices.Clone, and safe map initialisation while writing code. In Review mode, it scans existing packages and flags issues by severity from fatal panics to minor style notes with exact file and line references.
Running automated rule enforcement requires the Go toolchain on your path. Linters like golangci-lint and gosec are optional companions for deeper static checks.
What it helps with
- Catch typed-nil interface returns before they trigger production panics
- Prevent slice aliasing bugs caused by append mutations
- Enforce range checks on narrowing integer conversions
- Detect deferred cleanups lingering inside loop bodies
Example prompts for the agent
- Review this package for nil-safety and slice aliasing bugs
- Harden this data transformer against integer overflow and nil map writes
- Check why my error interface returns non-nil on success
How to install go-defensive-coding
1Skills CLI (Vercel)
npx skills add eduardo-sl/go-agent-skills --skill go-defensive-coding -a claude-code2Manually: put the files in place
~/.claude/skills/go-defensive-coding/SKILL.mdCopy the whole skill folder; for one project use .claude/skills/go-defensive-coding/
1Skills CLI (Vercel)
npx skills add eduardo-sl/go-agent-skills --skill go-defensive-coding -a codex2Manually: put the files in place
.agents/skills/go-defensive-coding/SKILL.mdFor all projects use ~/.agents/skills/
1Skills CLI (Vercel)
npx skills add eduardo-sl/go-agent-skills --skill go-defensive-coding -a cursor2Manually: put the files in place
.cursor/skills/go-defensive-coding/SKILL.mdCursor also reads .agents/skills/ and .claude/skills/
1GitHub CLI
gh skill install eduardo-sl/go-agent-skills go-defensive-coding2Manually: put the files in place
.github/skills/go-defensive-coding/SKILL.md1Skills CLI (Vercel)
npx skills add eduardo-sl/go-agent-skills --skill go-defensive-coding -a gemini-cli2Platform CLI
gemini skills install https://github.com/eduardo-sl/go-agent-skills --path 'skills/(safety)/go-defensive-coding'Manually: put the files in place
.devin/skills/go-defensive-coding/SKILL.mdThe legacy .windsurf/skills/ path still works
Skills CLI (Vercel)
npx skills add eduardo-sl/go-agent-skills --skill go-defensive-coding -a opencodeClaude.ai
Zip the folder and upload it in Settings → Capabilities → Skills
Step by step, with commands for each platform: How to install skills in Claude Code: five ways
Questions about go-defensive-coding
How to install go-defensive-coding in Claude Code?
Run npx skills add eduardo-sl/go-agent-skills --skill go-defensive-coding -a claude-code in your terminal. It sets up ~/.claude/skills/go-defensive-coding/SKILL.md.
What does go-defensive-coding do?
It guides AI agents through reviewing and writing defensive Go code, focusing on boundary copying, nil handling, float comparisons, and resource cleanup.
Does this skill handle goroutine leaks or race conditions?
No. The skill deliberately excludes goroutine lifecycle management and data race checks, routing those audits to go-concurrency-review.
Source text
SKILL.md · MIT
---
name: go-defensive-coding
description: >
Prevent panics, silent corruption, and subtle runtime bugs in Go:
typed-nil interfaces, slice aliasing, integer overflow on conversion,
float comparison, defer in loops, defensive copying at API boundaries, and
zero-value design. Use when hardening code against crashes, reviewing for
nil-safety, converting between numeric types, or deciding what to copy at
a package boundary. Trigger examples: "nil pointer panic", "why is my
error not nil", "slice aliasing", "integer overflow", "compare floats",
"defer in a loop", "defensive copy", "make this crash-proof".
Not for: data races and goroutine lifecycle (go-concurrency-review),
injection and auth (go-security-audit), a panic that already happened
(go-troubleshooting).
user-invocable: true
license: MIT
compatibility: Designed for Claude Code or similar AI coding agents working on Go projects. Requires the Go toolchain. golangci-lint and gosec are optional, for enforcing the rules below.
allowed-tools: Read Edit Write Glob Grep Bash(go:*) Bash(gofmt:*) Bash(golangci-lint:*) Bash(gosec:*)
metadata:
author: eduardo-sl
version: "1.1.3"
---
# Go Defensive Coding
Go has no exceptions and no null-safety in the type system. Every trap below
compiles cleanly, passes review, and fails in production.
Detailed reference material, loaded on demand:
- `references/nil-and-aliasing.md` — the full typed-nil rules, slice
aliasing scenarios, and memory retention.
- `references/numeric-safety.md` — conversion range checks, overflow
detection, float and time comparison.
- `references/copies-and-lifecycle.md` — `copylocks`/`noCopy`, `defer` in
loops, checking deferred `Close` errors, usable zero values.
Read a reference file only when the section below is not enough.
## Operating Modes
- **Harden** — you are writing or changing code. Apply every rule as you go.
- **Review** — you are auditing existing code. Report findings with severity
(🔴 panic or corruption, 🟡 latent bug, 🟢 style) and cite file:line.
## 1. The Typed-Nil Interface Trap
A non-nil interface can hold a nil pointer. This is the single most common
source of "impossible" nil checks in Go.
```go
type NotFoundError struct{ ID string }
func (e *NotFoundError) Error() string { return "not found: " + e.ID }
// ❌ Bad — returns a non-nil error even on success
func find(id string) error {
var err *NotFoundError // typed nil
if id == "" {
err = &NotFoundError{ID: id}
}
return err // interface is (type=*NotFoundError, value=nil) — NOT nil
}
// ✅ Good — return the untyped nil literal
func find(id string) error {
if id == "" {
return &NotFoundError{ID: id}
}
return nil
}
```
Rules:
- Never declare a concrete error/pointer variable and return it as an
interface. Return `nil` explicitly on the success path.
- Never store a possibly-nil concrete pointer in an `error`, `io.Reader`, or
any interface-typed struct field.
- `staticcheck` reports `SA4023` when a comparison with `nil` can never be
true because of this trap. No tool catches every case.
## 2. Nil Map, Slice, and Channel Behaviour
Memorise this table — half of these are safe and half panic or hang.
| Operation | nil map | nil slice | nil channel |
|---|---|---|---|
| Read / receive | zero value | index panics | blocks forever |
| Write / send | **panics** | `append` works | blocks forever |
| `len` / `cap` | `0` | `0` | `0` |
| `range` | zero iterations | zero iterations | blocks forever |
| `close` | n/a | n/a | **panics** |
```go
// ✅ A nil slice is a valid empty slice — do not guard append
var out []string
out = append(out, "a")
// ❌ A nil map is read-only
var m map[string]int
m["k"] = 1 // panic: assignment to entry in nil map
// ✅ Initialise every map before writing
m := make(map[string]int)
```
Return nil slices, not `[]T{}`. They marshal identically in JSON for
`encoding/json` when the field is `omitempty`, and cost no allocation.
Return an empty non-nil map only when the caller is documented to write to it.
## 3. Slice Aliasing
A slice is a view. `append` writes through that view whenever capacity
allows, mutating data the caller still owns.
```go
a := []int{1, 2, 3, 4}
b := a[:2]
b = append(b, 99) // ❌ overwrites a[2]; a is now [1 2 99 4]
```
```go
// ✅ Full slice expression caps the view — append must reallocate
b := a[:2:2]
b = append(b, 99) // a is untouched
```
Apply this whenever you hand a subslice to code you do not control, and
whenever a struct field holds a subslice of a larger buffer.
A subslice also keeps the entire backing array alive. To release a large
buffer, copy what you need: `head := slices.Clone(buf[:64])`.
## 4. Defensive Copying at Boundaries
Slices and maps are reference types. Storing or returning one without a copy
hands out a mutable handle to your internals.
```go
type Config struct{ hosts []string }
// ❌ Bad — caller can mutate our state, both ways
func NewConfig(hosts []string) *Config { return &Config{hosts: hosts} }
func (c *Config) Hosts() []string { return c.hosts }
// ✅ Good — copy in, copy out
func NewConfig(hosts []string) *Config {
return &Config{hosts: slices.Clone(hosts)}
}
func (c *Config) Hosts() []string { return slices.Clone(c.hosts) }
```
Use `maps.Clone` for maps. Both are shallow — a `[]*User` clone still shares
the pointed-to users.
Copy when the value is retained past the call or exposed to a caller. Do not
copy a slice you only read inside the function; that is wasted allocation.
### Preventing accidental copies
A struct holding a `sync.Mutex`, `sync.WaitGroup` or `atomic.Int64` must
never be copied — a value receiver or by-value parameter gives the copy an
independent lock, and both halves believe they are synchronised. `go vet`'s
`copylocks` catches these; for no-copy types without a lock, embed a
`noCopy` marker so `vet` covers them too. Examples:
`references/copies-and-lifecycle.md`.
## 5. Numeric Conversion and Comparison
Go never panics on numeric conversion. It truncates.
```go
// ❌ Silent corruption when the value does not fit
count := int32(int64Total)
// ✅ Range-check before narrowing
if int64Total > math.MaxInt32 || int64Total < math.MinInt32 {
return fmt.Errorf("total %d out of int32 range", int64Total)
}
count := int32(int64Total)
```
The same applies to `int` → `uint` (negatives wrap to huge values) and to
`len()` results assigned to sized types. `gosec` reports these as G115.
Never compare floats with `==` — compare against a tolerance. Never compare
`time.Time` with `==` — `Equal` compares the instant, while `==` also
compares the monotonic reading and location. Examples:
`references/numeric-safety.md`.
Integer division by zero panics; float division by zero yields `±Inf` or
`NaN`, and `NaN != NaN`. Guard divisors that come from input.
## 6. Resource Lifecycle
- `defer` runs at **function** return, not at the end of a loop body. A
`defer f.Close()` inside a loop keeps every file open until the loop
ends — give each iteration its own function scope. The same applies to
`resp.Body.Close`, `rows.Close`, `mu.Unlock` and `tx.Rollback`.
- Check the error from a deferred `Close` on anything you wrote to: a
failed flush on close is silent data loss. Assign it to a named `err`
result when the body succeeded.
Examples: `references/copies-and-lifecycle.md`.
## 7. Zero-Value and Initialisation Safety
Design types so the zero value works, then no constructor can be forgotten.
A `sync.Mutex` zero value is ready, and a nil map can be read; initialise
the map lazily on first write under the lock. Example:
`references/copies-and-lifecycle.md`.
When lazy init must happen exactly once and may race, use `sync.Once`.
Avoid `init()`. It runs before `main`, cannot fail cleanly, cannot be tested
in isolation, and its cross-file order depends on filenames. Use an explicit
`New...` constructor that returns an error.
## Enforce with Tooling
Run these; do not rely on reading alone. Skip and note any tool that is not
installed.
```bash
go vet ./... # copylocks, lostcancel, printf
golangci-lint run # errcheck, bodyclose, makezero, sqlclosecheck
staticcheck ./... # SA4023 typed-nil comparisons
gosec -include=G115,G104 ./... # integer overflow, unhandled errors
go test -race ./... # aliasing bugs often surface as races
```
Relevant golangci-lint linters: `errcheck`, `bodyclose`, `sqlclosecheck`,
`rowserrcheck`, `makezero`, `nilerr`, `exhaustive`, and `govet` with the
`nilness` analyzer enabled — `go vet` itself does not run `nilness`.
## Verification Checklist
1. No function returns a concrete pointer type as an interface on a success path
2. Every map is initialised before its first write
3. Subslices handed across a package boundary use a full slice expression `a[:n:n]`
4. Slices and maps stored in or returned from a struct are cloned
5. No type holding a mutex or atomic is passed or received by value
6. Every narrowing numeric conversion is range-checked, or documented as bounded
7. No `==` on floats or on `time.Time`
8. Divisors derived from input are checked against zero
9. No `defer` inside a loop body without an enclosing function scope
10. Deferred `Close` on written resources reports its error
11. `go vet`, `golangci-lint` and `go test -race` are clean
© eduardo-sl · MIT · eduardo-sl/go-agent-skills