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
Sourcecxuu/golang-skills
Added on
NPX Install
npx skill4agent add cxuu/golang-skills go-defensiveTags
Translated version includes tags in frontmatterSKILL.md Content
View Translation Comparison →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 for pointer types, slices, maps; empty struct for value receivers.
nil{}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 to clean up resources (files, locks). Avoids missed cleanup on multiple returns.
deferBad
go
p.Lock()
if p.count < 10 {
p.Unlock()
return p.count
}
p.count++
newCount := p.count
p.Unlock()
return newCount // easy to miss unlocksGood
go
p.Lock()
defer p.Unlock()
if p.count < 10 {
return p.count
}
p.count++
return p.countDefer overhead is negligible. Only avoid in nanosecond-critical paths.
Defer for File Operations
Place immediately after opening a file for clarity:
defer f.Close()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 executes, not when the
deferred function runs:
defergo
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: aStart 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 = iotaUse time.Time and time.Duration
Source: Uber Go Style Guide
Always use the package. Avoid raw for time values.
timeintInstants
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 isn't possible, include unit in field name:
time.DurationBad
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 or to generate keys, even throwaway ones. This is a security concern.
math/randmath/rand/v2Unseeded or time-seeded random generators have predictable output:
- provides only a few bits of entropy
Time.Nanoseconds() - Keys generated this way can be guessed by attackers
Use instead:
crypto/randgo
import (
"crypto/rand"
)
func Key() string {
return rand.Text()
}For text output:
- Use directly (preferred)
crypto/rand.Text - Or encode random bytes with or
encoding/hexencoding/base64
Panic and Recover
Source: Effective Go
Use 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.
panicUse to regain control of a panicking goroutine (only works inside deferred functions):
recovergo
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 if a library truly cannot set itself up
init() - 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 | |
| Receiving slices/maps | Copy before storing |
| Returning slices/maps | Return a copy |
| Resource cleanup | Use |
| Defer argument timing | Evaluated at defer, not call time |
| Enums | Start at |
| Time instants | Use |
| Time durations | Use |
| Mutable globals | Use dependency injection |
| Type embedding | Use explicit delegation |
| Serialization | Always use field tags |
| Key generation | Use |
| Panic usage | Only for truly unrecoverable situations |
| Recover pattern | Use in defer; convert to error at API boundary |
See Also
- - Core Go style principles
go-style-core - - Goroutine and channel patterns
go-concurrency - - Error handling best practices
go-error-handling