go-defensive

Original🇺🇸 English
Translated

Defensive programming patterns in Go including interface verification, slice/map copying at boundaries, time handling, avoiding globals, and defer for cleanup. Use when writing robust, production-quality Go code.

2installs
Added on

NPX Install

npx skill4agent add cxuu/golang-skills go-defensive

Go Defensive Programming Patterns

Verify Interface Compliance

Source: Uber Go Style Guide
Verify interface compliance at compile time using zero-value assertions.
Bad
go
type Handler struct{}

func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  // ...
}
Good
go
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
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)
}

Returning

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
}

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
go
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
go
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:
go
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:
go
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:
go
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
go
const (
  Add Operation = iota  // Add=0, zero value looks valid
  Subtract
  Multiply
)
Good
go
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
go
func isActive(now, start, stop int) bool {
  return start <= now && now < stop
}
Good
go
func isActive(now, start, stop time.Time) bool {
  return (start.Before(now) || start.Equal(now)) && now.Before(stop)
}

Durations

Bad
go
func poll(delay int) {
  time.Sleep(time.Duration(delay) * time.Millisecond)
}
poll(10)  // seconds? milliseconds?
Good
go
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
go
type Config struct {
  Interval int `json:"interval"`
}
Good
go
type Config struct {
  IntervalMillis int `json:"intervalMillis"`
}

Avoid Mutable Globals

Source: Uber Go Style Guide
Use dependency injection instead of mutable globals.
Bad
go
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
go
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
go
type ConcreteList struct {
  *AbstractList
}
Good
go
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
go
type Stock struct {
  Price int
  Name  string
}
Good
go
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:
go
import (
	"crypto/rand"
)

func Key() string {
	return rand.Text()
}
For text output:
  • Use
    crypto/rand.Text
    directly (preferred)
  • Or encode random bytes with
    encoding/hex
    or
    encoding/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):
go
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

PatternRule
Interface compliance
var _ Interface = (*Type)(nil)
Receiving slices/mapsCopy before storing
Returning slices/mapsReturn a copy
Resource cleanupUse
defer
Defer argument timingEvaluated at defer, not call time
EnumsStart at
iota + 1
Time instantsUse
time.Time
Time durationsUse
time.Duration
Mutable globalsUse dependency injection
Type embeddingUse explicit delegation
SerializationAlways use field tags
Key generationUse
crypto/rand
, never
math/rand
Panic usageOnly for truly unrecoverable situations
Recover patternUse in defer; convert to error at API boundary

See Also

  • go-style-core
    - Core Go style principles
  • go-concurrency
    - Goroutine and channel patterns
  • go-error-handling
    - Error handling best practices