go-defensive-coding — скилл для Claude Code: обзор и установка
Защищает код на Go от паник и скрытых багов: типизированные nil в интерфейсах, алиасинг слайсов, переполнение чисел и утечки ресурсов.
npx skills add eduardo-sl/go-agent-skills --skill go-defensive-coding -a claude-codeЧто делает go-defensive-coding
Скилл внедряет в диалог с агентом набор правил защитного программирования на Go. Он помогает избегать неочевидных ловушек компилятора: интерфейсов с типизированным nil, перезаписи чужой памяти через append по срезу и тихого усечения значений при приведении чисел вроде int64 к int32.
Работает в двух режимах. В режиме Harden агент сразу пишет безопасный код с правильным копированием структур через slices.Clone и проверками диапазонов. В режиме Review он размечает найденные проблемы по уровням опасности: от паник и порчи данных до стилевых правок с указанием файлов и строк.
Для запуска проверок агенту требуется установленный в системе Go toolchain. Утилиты golangci-lint и gosec остаются опциональными, но без них часть скрытых проблем не подтвердить линтером.
Какие задачи решает
- Предотвратить паники из-за скрытых типизированных nil в интерфейсах
- Исключить перезапись данных при работе со срезами слайсов
- Проверять диапазоны значений перед числовыми преобразованиями
- Найти отложенные вызовы defer внутри циклов
Примеры запросов агенту
- Проверь этот пакет на nil-safety и утечки памяти
- Сделай безопасным метод экспорта конфига, чтобы вызывающий код не мутировал срез
- Найди места, где сужение числовых типов может привести к потере данных
Как установить go-defensive-coding
1Через Skills CLI (Vercel)
npx skills add eduardo-sl/go-agent-skills --skill go-defensive-coding -a claude-code2Вручную: положить файлы
~/.claude/skills/go-defensive-coding/SKILL.mdПапка скилла целиком; для одного проекта — .claude/skills/go-defensive-coding/
1Через Skills CLI (Vercel)
npx skills add eduardo-sl/go-agent-skills --skill go-defensive-coding -a codex2Вручную: положить файлы
.agents/skills/go-defensive-coding/SKILL.mdДля всех проектов — ~/.agents/skills/
1Через Skills CLI (Vercel)
npx skills add eduardo-sl/go-agent-skills --skill go-defensive-coding -a cursor2Вручную: положить файлы
.cursor/skills/go-defensive-coding/SKILL.mdCursor читает и .agents/skills/, и .claude/skills/
1GitHub CLI
gh skill install eduardo-sl/go-agent-skills go-defensive-coding2Вручную: положить файлы
.github/skills/go-defensive-coding/SKILL.md1Через Skills CLI (Vercel)
npx skills add eduardo-sl/go-agent-skills --skill go-defensive-coding -a gemini-cli2Командой платформы
gemini skills install https://github.com/eduardo-sl/go-agent-skills --path 'skills/(safety)/go-defensive-coding'Вручную: положить файлы
.devin/skills/go-defensive-coding/SKILL.mdСтарый путь .windsurf/skills/ тоже читается
Через Skills CLI (Vercel)
npx skills add eduardo-sl/go-agent-skills --skill go-defensive-coding -a opencodeClaude.ai
Упакуйте папку в zip и загрузите в Settings → Capabilities → Skills
По шагам, с командами для каждой платформы: Как установить скилл в Claude Code: пять способов
Вопросы о go-defensive-coding
Как установить go-defensive-coding в Claude Code?
Выполните в терминале команду npx skills add eduardo-sl/go-agent-skills --skill go-defensive-coding -a claude-code. Файл навыка сохранится по пути ~/.claude/skills/go-defensive-coding/SKILL.md.
Что делает этот скилл?
Он направляет агента при ревью и написании Go-кода, отслеживая мутации слайсов, вызовы defer в циклах, сравнение time.Time через == и ловушки интерфейсов.
Подходит ли скилл для аудита конкурентности или гонок данных?
Нет, автор скилла прямо исключает разбор жизненного цикла горутин и гонок данных — для таких задач в репозитории предусмотрен отдельный навык go-concurrency-review.
Исходный текст
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