go-defensive
npx machina-cli add skill cxuu/golang-skills/go-defensive --openclawGo Defensive Programming Patterns
Verify Interface Compliance
Source: Uber Go Style Guide
Verify interface compliance at compile time using zero-value assertions.
Bad
type Handler struct{}
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// ...
}
Good
type Handler struct{}
var _ http.Handler = (*Handler)(nil)
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// ...
}
Use nil for pointer types, slices, maps; empty struct {} for value receivers.
Copy Slices and Maps at Boundaries
Source: Uber Go Style Guide
Slices and maps contain pointers. Copy at API boundaries to prevent unintended modifications.
Receiving
Bad
func (d *Driver) SetTrips(trips []Trip) {
d.trips = trips // caller can still modify d.trips
}
Good
func (d *Driver) SetTrips(trips []Trip) {
d.trips = make([]Trip, len(trips))
copy(d.trips, trips)
}
Returning
Bad
func (s *Stats) Snapshot() map[string]int {
s.mu.Lock()
defer s.mu.Unlock()
return s.counters // exposes internal state!
}
Good
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
}
Defer to Clean Up
Source: Uber Go Style Guide, Effective Go
Use defer to clean up resources (files, locks). Avoids missed cleanup on multiple returns.
Bad
p.Lock()
if p.count < 10 {
p.Unlock()
return p.count
}
p.count++
newCount := p.count
p.Unlock()
return newCount // easy to miss unlocks
Good
p.Lock()
defer p.Unlock()
if p.count < 10 {
return p.count
}
p.count++
return p.count
Defer overhead is negligible. Only avoid in nanosecond-critical paths.
Defer for File Operations
Place defer f.Close() immediately after opening a file for clarity:
func Contents(filename string) (string, error) {
f, err := os.Open(filename)
if err != nil {
return "", err
}
defer f.Close() // Close sits near Open - much clearer
var result []byte
buf := make([]byte, 100)
for {
n, err := f.Read(buf[0:])
result = append(result, buf[0:n]...)
if err != nil {
if err == io.EOF {
break
}
return "", err // f will be closed
}
}
return string(result), nil // f will be closed
}
Defer Argument Evaluation
Arguments to deferred functions are evaluated when defer executes, not when the
deferred function runs:
for i := 0; i < 5; i++ {
defer fmt.Printf("%d ", i)
}
// Prints: 4 3 2 1 0 (LIFO order, values captured at defer time)
Defer LIFO Order
Multiple defers execute in Last-In-First-Out order:
func trace(s string) string {
fmt.Println("entering:", s)
return s
}
func un(s string) {
fmt.Println("leaving:", s)
}
func a() {
defer un(trace("a")) // trace() runs now, un() runs at return
fmt.Println("in a")
}
// Output: entering: a, in a, leaving: a
Start Enums at One
Source: Uber Go Style Guide
Start enums at non-zero to distinguish uninitialized from valid values.
Bad
const (
Add Operation = iota // Add=0, zero value looks valid
Subtract
Multiply
)
Good
const (
Add Operation = iota + 1 // Add=1, zero value = uninitialized
Subtract
Multiply
)
Exception: When zero is the sensible default (e.g., LogToStdout = iota).
Use time.Time and time.Duration
Source: Uber Go Style Guide
Always use the time package. Avoid raw int for time values.
Instants
Bad
func isActive(now, start, stop int) bool {
return start <= now && now < stop
}
Good
func isActive(now, start, stop time.Time) bool {
return (start.Before(now) || start.Equal(now)) && now.Before(stop)
}
Durations
Bad
func poll(delay int) {
time.Sleep(time.Duration(delay) * time.Millisecond)
}
poll(10) // seconds? milliseconds?
Good
func poll(delay time.Duration) {
time.Sleep(delay)
}
poll(10 * time.Second)
JSON Fields
When time.Duration isn't possible, include unit in field name:
Bad
type Config struct {
Interval int `json:"interval"`
}
Good
type Config struct {
IntervalMillis int `json:"intervalMillis"`
}
Avoid Mutable Globals
Source: Uber Go Style Guide
Use dependency injection instead of mutable globals.
Bad
var _timeNow = time.Now
func sign(msg string) string {
now := _timeNow()
return signWithTime(msg, now)
}
// Test requires save/restore of global
func TestSign(t *testing.T) {
oldTimeNow := _timeNow
_timeNow = func() time.Time { return someFixedTime }
defer func() { _timeNow = oldTimeNow }()
assert.Equal(t, want, sign(give))
}
Good
type signer struct {
now func() time.Time
}
func newSigner() *signer {
return &signer{now: time.Now}
}
func (s *signer) Sign(msg string) string {
now := s.now()
return signWithTime(msg, now)
}
// Test injects dependency cleanly
func TestSigner(t *testing.T) {
s := newSigner()
s.now = func() time.Time { return someFixedTime }
assert.Equal(t, want, s.Sign(give))
}
Avoid Embedding Types in Public Structs
Source: Uber Go Style Guide
Embedded types leak implementation details and inhibit type evolution.
Bad
type ConcreteList struct {
*AbstractList
}
Good
type ConcreteList struct {
list *AbstractList
}
func (l *ConcreteList) Add(e Entity) {
l.list.Add(e)
}
func (l *ConcreteList) Remove(e Entity) {
l.list.Remove(e)
}
Embedding problems:
- Adding methods to embedded interface is a breaking change
- Removing methods from embedded struct is a breaking change
- Replacing the embedded type is a breaking change
Use Field Tags in Marshaled Structs
Source: Uber Go Style Guide
Always use explicit field tags for JSON, YAML, etc.
Bad
type Stock struct {
Price int
Name string
}
Good
type Stock struct {
Price int `json:"price"`
Name string `json:"name"`
// Safe to rename Name to Symbol
}
Tags make the serialization contract explicit and safe to refactor.
Crypto Rand
Source: Go Wiki CodeReviewComments (Normative)
Do not use math/rand or math/rand/v2 to generate keys, even throwaway ones. This is a security concern.
Unseeded or time-seeded random generators have predictable output:
Time.Nanoseconds()provides only a few bits of entropy- Keys generated this way can be guessed by attackers
Use crypto/rand instead:
import (
"crypto/rand"
)
func Key() string {
return rand.Text()
}
For text output:
- Use
crypto/rand.Textdirectly (preferred) - Or encode random bytes with
encoding/hexorencoding/base64
Panic and Recover
Source: Effective Go
Use panic only for truly unrecoverable situations. Library functions should avoid panic—if the problem can be worked around, let things continue rather than taking down the whole program.
Use recover to regain control of a panicking goroutine (only works inside deferred functions):
func safelyDo(work *Work) {
defer func() {
if err := recover(); err != nil {
log.Println("work failed:", err)
}
}()
do(work)
}
Key rules:
- Never expose panics across package boundaries—always convert to errors
- Acceptable to panic in
init()if a library truly cannot set itself up - Use recover to isolate panics in server goroutine handlers
For detailed patterns including server protection and package-internal panic/recover, see references/PANIC-RECOVER.md.
Quick Reference
| Pattern | Rule |
|---|---|
| Interface compliance | var _ Interface = (*Type)(nil) |
| Receiving slices/maps | Copy before storing |
| Returning slices/maps | Return a copy |
| Resource cleanup | Use defer |
| Defer argument timing | Evaluated at defer, not call time |
| Enums | Start at iota + 1 |
| Time instants | Use time.Time |
| Time durations | Use time.Duration |
| Mutable globals | Use dependency injection |
| Type embedding | Use explicit delegation |
| Serialization | Always use field tags |
| Key generation | Use crypto/rand, never math/rand |
| Panic usage | Only for truly unrecoverable situations |
| Recover pattern | Use in defer; convert to error at API boundary |
See Also
go-style-core- Core Go style principlesgo-concurrency- Goroutine and channel patternsgo-error-handling- Error handling best practices
Source
git clone https://github.com/cxuu/golang-skills/blob/main/skills/go-defensive/SKILL.mdView on GitHub Overview
Go defensive programming patterns help you build robust, production-grade code. It emphasizes compile-time interface verification, safe boundary handling for slices and maps, reliable resource cleanup with defer, prudent time handling, and avoiding global state.
How This Skill Works
Key techniques include compile-time interface assertions to enforce adherence, copying slices and maps at API boundaries to prevent external mutation, and using defer for predictable cleanup of files, locks, and other resources. It also advocates using time.Time and time.Duration for timing concerns rather than raw ints to reduce bugs.
When to Use It
- When building production-grade services that must be strictly conforming to interfaces
- When exposing API boundaries where callers could inadvertently mutate internal state
- When managing resources (files, locks, network connections) that require reliable cleanup
- When handling timing, deadlines, or durations using the time package
- When avoiding global state and preferring explicit dependency wiring
Quick Start
- Step 1: Add compile-time interface verification for your types using a nil assertion
- Step 2: At API boundaries, copy input slices and maps, and return copies when exposing internal state
- Step 3: Wrap resource usage with defer for cleanup and use time.Time/time.Duration for timing concerns
Best Practices
- Verify interface compliance at compile time with a zero-value assertion like var _ http.Handler = (*T)(nil)
- Copy slices and maps on assignment and return to avoid aliasing internal state
- Use defer to ensure cleanup is performed on all code paths, including early returns
- Prefer time.Time and time.Duration for timing, not plain ints
- Avoid globals; inject dependencies and avoid package-level mutable state
Example Use Cases
- Interface conformance for a custom HTTP handler using a nil assertion
- Copying input trips in a setter to prevent external modifications
- Returning a copied snapshot map instead of exposing internal dict
- Guarded critical sections with mutex and defer unlock
- Using time.Time for event timestamps and time.Duration for timeouts