
Go Defensive
Apply Uber-style defensive copy rules when your Go service stores or returns slices and maps so callers cannot mutate internal state.
Overview
go-defensive is an agent skill for the Build phase that teaches defensive copying of Go slices and maps at API boundaries so internal state cannot be mutated by callers.
Install
npx skills add https://github.com/cxuu/golang-skills --skill go-defensiveWhat is this skill?
- Bad vs good examples for storing caller-provided slices with make + copy
- Defensive map copies on receive using make and key iteration
- Return-copy pattern for maps (e.g. Stats.Snapshot) to avoid exposing mutex-protected internals
- Sourced from the Uber Go Style Guide for API boundary hygiene
- Prevents subtle data races and unintended mutation after function return
- Uber Style Guide source
- Separate bad/good patterns for slices and maps on receive and return
Adoption & trust: 653 installs on skills.sh; 110 GitHub stars; 3/3 security scanners passed (skills.sh audits).
What problem does it solve?
You store or return slices and maps in Go without copying, so callers can change your service’s data after the function returns.
Who is it for?
Indie builders writing Go HTTP handlers, repositories, or in-memory caches that accept or expose collections.
Skip if: Teams that only need high-level Go lint rules without slice/map boundary patterns, or codebases that already enforce this via immutable types or code review gates.
When should I use this skill?
When writing or reviewing Go functions that store caller-provided slices or maps, or return internal slices/maps from methods.
What do I get? / Deliverables
Your setters, config methods, and snapshot APIs allocate copies on receive and return, matching Uber’s style guide and safer concurrency assumptions.
- Setter/getter/snapshot functions that copy slices and maps on store and return
Recommended Skills
Journey fit
Defensive copying is implemented while you write Go APIs and domain types—the canonical moment is backend build, before those boundaries ship. Setter, getter, and snapshot patterns live in backend service code, not in frontend or ops tooling.
How it compares
Use as a focused API-boundary checklist instead of generic “write idiomatic Go” chat without concrete copy patterns.
Common Questions / FAQ
Who is go-defensive for?
Solo and indie developers building Go backends who want Uber-style discipline for slices and maps at public method boundaries.
When should I use go-defensive?
Use it while implementing Build/backend setters, getters, and snapshots; again in Ship/review when auditing exposure of internal collections; and when hardening Operate/iterate fixes for subtle mutation bugs.
Is go-defensive safe to install?
It is documentation-style guidance with example snippets only; review the Security Audits panel on this page before trusting any third-party skill package in your agent.
SKILL.md
READMESKILL.md - Go Defensive
# Copying Slices and Maps at API Boundaries > **Source**: Uber Style Guide Slices and maps contain references to their underlying data. Copy them at API boundaries to prevent callers from mutating internal state (or vice versa). ## Receiving Slices and Maps When a function stores a slice or map passed by the caller, always make a defensive copy. The caller retains the original reference and can modify it after your function returns. ### Slices **Bad** ```go func (d *Driver) SetTrips(trips []Trip) { d.trips = trips // caller can still modify d.trips } ``` **Good** ```go func (d *Driver) SetTrips(trips []Trip) { d.trips = make([]Trip, len(trips)) copy(d.trips, trips) } ``` ### Maps **Bad** ```go func (s *Server) SetConfig(cfg map[string]string) { s.config = cfg // caller can still modify s.config } ``` **Good** ```go func (s *Server) SetConfig(cfg map[string]string) { s.config = make(map[string]string, len(cfg)) for k, v := range cfg { s.config[k] = v } } ``` ## Returning Slices and Maps When returning internal slices or maps, return a copy to prevent callers from modifying your internal state. ### Returning a Map **Bad** ```go func (s *Stats) Snapshot() map[string]int { s.mu.Lock() defer s.mu.Unlock() return s.counters // exposes internal state! } ``` **Good** ```go func (s *Stats) Snapshot() map[string]int { s.mu.Lock() defer s.mu.Unlock() result := make(map[string]int, len(s.counters)) for k, v := range s.counters { result[k] = v } return result } ``` ### Returning a Slice **Bad** ```go func (q *Queue) Items() []Item { return q.items // caller can append, modify, or reslice } ``` **Good** ```go func (q *Queue) Items() []Item { result := make([]Item, len(q.items)) copy(result, q.items) return result } ``` ## When Copies Are Not Needed Defensive copies have a cost. Skip them when: - The data is **immutable by convention** and clearly documented - The slice/map is **created fresh** for the caller (not stored internally) - Performance profiling shows the copy is a bottleneck in a hot path When in doubt, copy. The cost is usually negligible compared to the bugs that shared references cause. # Global State Patterns > **Source**: Google Style Guide, Effective Go Global state makes programs harder to test, reason about, and maintain. Dependency injection is the preferred alternative, but some global state is acceptable when used carefully. ## When Global State Is Acceptable Not all package-level variables are harmful. Global state is appropriate when it is **truly process-wide** and **not worth injecting**: - **Default instances** — `http.DefaultClient`, `log.Default()`, `flag.CommandLine` - **Compiled-once values** — `regexp.MustCompile(...)` at package level - **Registries** — `database/sql.Register`, `image.RegisterFormat` - **Singleton infrastructure** — a process-wide metric collector or trace exporter ## Litmus Test for Global Variables Before adding a package-level variable, ask: 1. **Is it truly process-wide?** If two goroutines or tests might need different values, it should not be global 2. **Does it prevent testing?** If tests must save/restore the variable or cannot run in parallel because of it, inject it instead 3. **Could it be a constant?** If the value never changes after init, prefer `const` or an unexported `var` initialized once 4. **Does it carry mutable state?** Mutable globals are the most dangerous — only acceptable for well-documented, concurrency-safe singletons ## Package State API Pattern: New() + Default() The standard library pattern provides both a customizable constructor and a convenient default. This lets callers use the default for simple cases and inject a custom instance for testing or specialized behavior. **Good** ```go package mylog type Logger struct { prefix string out io.Writer } func New(prefix string, out io.Writer) *Logger { return &Logger{prefix: prefix, out: out} } var de