go-defensive
Skill7 starsChanged 10 days agoApache-2.0View on GitHub
Use when hardening Go API boundaries or checking slice/map copies, aliasing, typed nils, cleanup/defer, narrowing overflow, float equality, nil channels, time.Time and Duration values, globals, interface compliance, or filesystem/crypto safety. Error strategy belongs to go-error-handling.
1 other project uses a version of this, 1 of them edited.
Adopted by
Most recently taken up by
- droxey/skillsedited, first seen Nov 8, 2026
Other versions1 project uses a version of this.
Word for word · 0
None found.
Edited · 1
- droxey/skills★ 116% same
Wrote their own version · 0
None found.
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository25 other files it gives its agents.
- AGENTS.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- go-code-refactorSkill
- go-code-reviewSkill
- go-codeSkill
- go-concurrencySkill
- go-contextSkill
- go-databaseSkill
- go-data-structuresSkill
- go-documentationSkill
- go-error-handlingSkill
- go-functionsSkill
- go-genericsSkill
- go-httpSkill
- go-interfacesSkill
- go-lintingSkill
- go-loggingSkill
- go-namingSkill
- go-packagesSkill
- go-performanceSkill
- go-resilienceSkill
- go-securitySkill
- go-style-coreSkill
- go-testingSkill
- go-troubleshootingSkill
The file
We split this skill into the rules it gives an agent
3 rules. About 0 are common, 0 are distinctive, and 3 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈0
Distinctive · estimate
This author's own choices. Worth a look.
3
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 99
Use time.Time and time.Duration for instants and spans in process, never raw integers.
- Very specific: line 115
Do not use math/rand or math/rand/v2 to generate keys.
- Very specific: line 126
When a path comes from a caller, request, config file, or archive entry, open it through os.Root (Go 1.24+) instead of filepath.Join + …
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Go Defensive Programming Patterns
Compatibility: Baseline Go 1.27 (see COMPATIBILITY.md).
url.URL.Cloneandurl.Values.Clonerequire Go 1.27+;Root.ReadFileandRoot.MkdirAllGo 1.25+;crypto/rand.Textandos.RootGo 1.24+;slices.Clone/maps.CloneGo 1.21+.
Resource Routing
references/BOUNDARY-COPYING.md- Read when copying slices/maps across API boundaries.references/GLOBAL-STATE.md- Read when introducing or removing package globals.references/MUST-FUNCTIONS.md- Read when deciding whether a panic-on-error helper is acceptable.references/PANIC-RECOVER.md- Read when evaluating panic, recover, or crash containment.references/ARCHIVES.md- Read when extracting a tar or zip archive.references/TIME-ON-THE-WIRE.md- Read when atime.Durationortime.Timefield crosses JSON or another wire format.../go-http/references/JSON-V2.md- Read when JSON v2 changes nil collections, tags, accepted input, or compatibility at an API boundary (Go 1.27+).
Common Pitfalls
| Pitfall | Rule |
| Typed nil in an interface | A *T(nil) stored in an interface — an error result, a field of interface type — is non-nil: err != nil is true and a call through it dereferences nil. Return a literal nil, and declare the result as error, not *MyErr (go-error-handling owns the API rule) |
Bare x.(T) assertion | Comma-ok unless a mismatch is a programming error that should panic (go-interfaces); reflection code prefers reflect.TypeAssert[T] (Go 1.25+) |
append aliasing | Both slices share the backing array while capacity allows. s[:len(s):len(s)] only caps capacity so the next append reallocates — existing elements still alias; slices.Clone(s) is the copy (see go-data-structures) |
int64 to int32 without a bounds check | Values wrap silently; compare against math.MaxInt32/math.MinInt32 first |
Float == | math.Abs(a-b) <= max(absTol, relTol*max(math.Abs(a), math.Abs(b))) — the relative term alone is never true against 0 (0.1+0.2-0.3 vs 0), a fixed epsilon alone is wrong at large magnitudes; absTol is the size below which the domain treats a value as zero; money in integer minor units or big.Rat, never float64 or big.Float (binary: ten 0.1s do not sum to 1) |
time.Time ==, or as a map key | == also compares the Location and the monotonic reading, so it fails after a JSON or database round trip or t.In(loc): compare with t.Equal(u); a key stores t.UTC().Round(0) or t.UnixNano() |
defer in a loop | Calls fire at function exit, not per iteration — extract the body (behavior note in go-code-refactor) |
| Nil channel | Send and receive block forever, so an unmade channel field is a hang, not an error — a deliberate nil in a select is the idiom for disabling that case (channel ownership: go-concurrency) |
| Integer division by zero | Panics; guard the divisor. Float division yields Inf/NaN instead, and every comparison with NaN is false, so x/0.0 >= limit silently answers no: guard a float divisor too |
| Elapsed time from a converted start | time.Now().UTC(), .In, .Local, and .Round(0) drop the monotonic reading, so end.Sub(start) jumps with a wall-clock step: take start := time.Now() and time.Since(start) |
Copy Slices and Maps at Boundaries
Slices and maps contain pointers to underlying data. Copy at API boundaries to prevent unintended modifications. Prefer stdlib clones when their nil and capacity behavior fits the contract (conditions in the reference below):
d.trips = slices.Clone(trips) // receiving; a nil argument stays nil
d.tags = append([]string{}, tags...) // receiving, when d.tags must encode as []
return maps.Clone(s.counters) // returning; a nil map stays nil
return u.Clone() // *url.URL, Go 1.27+
return params.Clone() // url.Values, Go 1.27+ (deep-copies values)slices.Clone and maps.Clone are shallow: []*T and map[K][]V copies
still alias their elements. A type-specific Clone has only the copy depth in
its documented contract; do not substitute shallow and deep copies as a style
change. slices.Clone and maps.Clone also preserve nil, which
encoding/json v1 writes as null; for a required [] or {} output, keep
the non-nil append or make copy above. See
BOUNDARY-COPYING.md.
Defer to Clean Up
Close on a written file can report a write that failed late, so its error
joins the function's result through a named err:
func save(name string, r io.Reader) (err error) {
f, err := os.Create(name)
if err != nil {
return err
}
defer func() { err = errors.Join(err, f.Close()) }()
_, err = io.Copy(f, r)
return err
}A file that is only read takes os.ReadFile or root.ReadFile when its whole
content fits in memory, and the same deferred errors.Join when it is
streamed.
Struct Field Tags
Advisory: Add explicit field tags to structs that are marshaled or unmarshaled.
type User struct {
Name string `json:"name" yaml:"name"`
Email string `json:"email" yaml:"email"`
}JSON v2 tag options (embed, not the experimental inline) are in
JSON-V2.md.
Time
Use time.Time and time.Duration for instants and spans in process, never
raw integers. On the wire, encoding/json/v2 (Go 1.27+) has no default
representation for time.Duration and fails to marshal it, so a wire field is
an integer with the unit in its name or a string parsed with
time.ParseDuration (TIME-ON-THE-WIRE.md).
Avoid Mutable Globals
Inject dependencies instead of mutating package-level variables. This makes
code testable without global save/restore. The smallest injection is an
argument: the caller passes time.Now() and a test passes a fixed instant;
code that sleeps is tested inside synctest.Test, whose clock is fake
(GLOBAL-STATE.md).
Crypto Rand
Do not use math/rand or math/rand/v2 to generate keys.
token := rand.Text() // crypto/rand: at least 128 bits, base32For raw key material, rand.Read(buf) has no error to check: it never
returns one, and it crashes the program if the OS source fails.
Confine Filesystem Access
When a path comes from a caller, request, config file, or archive entry,
open it through os.Root (Go 1.24+) instead of filepath.Join + os.Open.
Root resolves every component inside the directory, so ../../etc/passwd
and a symlink pointing out of the tree both fail instead of escaping. A
server opens the root once at startup and keeps it on the struct that serves
requests:
type server struct {
uploads *os.Root // opened once, held for the server's lifetime
}
func newServer(dir string) (*server, error) {
uploads, err := os.OpenRoot(dir)
if err != nil {
return nil, err
}
return &server{uploads: uploads}, nil
}
func (s *server) upload(name string) ([]byte, error) {
return s.uploads.ReadFile(name) // cannot escape dir
}filepath.Clean is not a substitute: it keeps a leading .., so
filepath.Join(dir, filepath.Clean("../../etc/passwd")) is /etc/passwd. A
name that is stored or compared but never opened — an object-store key, a
manifest entry — is checked with filepath.IsLocal(name), which is lexical:
a name that is opened still goes through the root.
A subprocess is outside the root, because it resolves a name itself. Open the
file through the root and hand the child the open file (cmd.Stdin,
cmd.ExtraFiles), kept open until the child exits; a program that needs a
path needs a trusted staging directory or OS isolation, and cmd.Dir
confines nothing.
Deletes Name What They Remove
os.RemoveAll(filepath.Join(dir, name)) deletes dir itself when the name
cleans to .: filepath.Join(dir, "") and filepath.Join(dir, "a/..") are
both dir, and filepath.IsLocal("a/..") is true. root.RemoveAll refuses
. and .., but a name that reaches the root through an existing entry
does not: on go1.27.1 root.RemoveAll("ws/.."), with ws present, deletes
everything under the root before it returns invalid argument. A {id}
wildcard receives ws/.. from ws%2F.., . from %2E, and .. from
%2E%2E. Rebuild the
name from a parsed value, so it names exactly one entry:
id, err := uuid.Parse(r.PathValue("id"))
if err != nil {
http.Error(w, "invalid workspace id", http.StatusBadRequest)
return
}
err = s.workspaces.RemoveAll(id.String()) // a canonical UUID is one entryArchive Entries
An archive entry's name, type, and size are all input: parents through
root.MkdirAll, no link an entry names, and a cap on the total size
(ARCHIVES.md has the archive/tar loop and the
archive/zip checks).
Panic and Recover
An error caused by input or the environment — a malformed request, a missing
file, a refused connection — never crosses a package boundary as a panic:
return it. A programming error may cross as one: an argument the API's
documentation forbids, a MustX call at initialization, the unreachable
default of a switch over a closed enum. A panic a package raises for its own
control flow is recovered at its boundary and returned as an error.
net/http recovers only the goroutine running the handler, so a goroutine you
start recovers in its own deferred function or its panic ends the process.
The patterns and their traps are in
PANIC-RECOVER.md.
Must Functions
Must functions panic on error — use them only at program
initialization on a value fixed when the program is built (regexp.MustCompile
on a pattern literal, template.Must on an embedded template), so a failure is
a bug; never on a file read at run time, the environment, or request-time input.
MUST-FUNCTIONS.md has the shape and the exceptions.
Related Skills
- go-error-handling: wrapping, sentinels, and typed errors at boundaries; this skill owns panic versus return.
- go-interfaces: compile-time interface assertions.
- go-style-core: the
iotablock and whether zero is a valid member. - go-security: untrusted values: injection, SSRF, secrets, TLS; this skill owns the
os.Rootandcrypto/randforms.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

