返回 Skills
cxuu/golang-skills· Apache-2.0 内容可用

go-defensive

Use when hardening Go code at API boundaries — copying slices/maps, verifying interface compliance, using defer for cleanup, time.Time/time.Duration, or avoiding mutable globals. Also use when reviewing for robustness concerns like missing cleanup or unsafe crypto usage, even if the user doesn't mention "defensive programming." Does not cover error handling strategy (see go-error-handling).

安装

与 skills.sh 相同的 Command / Prompt 安装方式


name: go-defensive description: Use when hardening Go code at API boundaries — copying slices/maps, verifying interface compliance, using defer for cleanup, time.Time/time.Duration, or avoiding mutable globals. Also use when reviewing for robustness concerns like missing cleanup or unsafe crypto usage, even if the user doesn't mention "defensive programming." Does not cover error handling strategy (see go-error-handling).

Go Defensive Programming Patterns

Compatibility: Crypto examples may use crypto/rand.Text, which requires Go 1.24+.

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/TIME-ENUMS-TAGS.md - Read when handling time types, enum zero values, or struct tags.

Defensive Checklist Priority

When hardening code at API boundaries, check in this order:

Reviewing an API boundary?
├─ 1. Error handling     → Return errors; don't panic (see go-error-handling)
├─ 2. Input validation   → Copy slices/maps received from callers
├─ 3. Output safety      → Copy slices/maps before returning to callers
├─ 4. Resource cleanup   → Use defer for Close/Unlock/Cancel
├─ 5. Interface checks   → Route compile-time assertions to go-interfaces
├─ 6. Time correctness   → Use time.Time and time.Duration, not int/float
├─ 7. Enum safety        → Start iota at 1 so zero-value is invalid
└─ 8. Crypto safety      → crypto/rand for keys, never math/rand

Quick Reference

PatternRuleDetails
Boundary copiesCopy slices/maps on receive and returnBOUNDARY-COPYING.md
Defer cleanupdefer f.Close() right after os.OpenBelow
Interface checkCompile-time satisfaction assertionSee go-interfaces
Time typestime.Time / time.Duration, never raw intTIME-ENUMS-TAGS.md
Enum startiota + 1 so zero = invalidBelow
Crypto randcrypto/rand for keys, never math/randBelow
Must functionsOnly at init; panic on failureMUST-FUNCTIONS.md
Panic/recoverNever expose panics across packagesPANIC-RECOVER.md
Mutable globalsReplace with dependency injectionBelow

Verify Interface Compliance

Route compile-time interface assertions to go-interfaces. Use this skill only to notice API-boundary robustness risk; the interface skill owns when an assertion is appropriate and the exact assertion shape.

Copy Slices and Maps at Boundaries

Slices and maps contain pointers to underlying data. Copy at API boundaries to prevent unintended modifications.

// Receiving: copy incoming slice
d.trips = make([]Trip, len(trips))
copy(d.trips, trips)

// Returning: copy map before returning
result := make(map[string]int, len(s.counters))
for k, v := range s.counters { result[k] = v }

Defer to Clean Up

Use defer to clean up resources (files, locks). Avoids missed cleanup on multiple return paths.

p.Lock()
defer p.Unlock()

if p.count < 10 {
  return p.count
}
p.count++
return p.count

Defer overhead is negligible. Place defer f.Close() immediately after os.Open for clarity. Arguments to deferred functions are evaluated when defer executes, not when the function runs. Multiple defers execute in LIFO order.

Struct Field Tags

Advisory: Always 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"`
}

Field tags are a serialization contract — renaming a struct field without updating the tag silently breaks wire compatibility. Treat tags as part of the public API for any type that crosses a serialization boundary.

Start Enums at One

Start enums at non-zero to distinguish uninitialized from valid values.

const (
  Add Operation = iota + 1  // Add=1, zero value = uninitialized
  Subtract
  Multiply
)

Exception: When zero is the sensible default (e.g., LogToStdout = iota).

Time, Struct Tags, and Embedding

Avoid Mutable Globals

Inject dependencies instead of mutating package-level variables. This makes code testable without global save/restore.

type signer struct {
  now func() time.Time  // injected; tests replace with fixed time
}

func newSigner() *signer {
  return &signer{now: time.Now}
}

Crypto Rand

Do not use math/rand or math/rand/v2 to generate keys — this is a security concern. Time-seeded generators have predictable output.

import "crypto/rand"

func Key() string { return rand.Text() }

For text output, use crypto/rand.Text directly, or encode random bytes with encoding/hex or encoding/base64.


Panic and Recover

Use panic only for truly unrecoverable situations. Library functions should avoid panic.

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

Must Functions

Must functions panic on error — use them only during program initialization where failure means the program cannot run.

var validID = regexp.MustCompile(`^[a-z][a-z0-9-]{0,62}$`)
var tmpl = template.Must(template.ParseFiles("index.html"))

Related Skills

  • Error handling: See go-error-handling when choosing between returning errors and panicking, or wrapping errors at boundaries
  • Concurrency safety: See go-concurrency when protecting shared state with mutexes, atomics, or channels
  • Interface checks: See go-interfaces when adding compile-time interface satisfaction checks
  • Data structure copying: See go-data-structures when working with slice/map internals or pointer aliasing

附带文件

references/BOUNDARY-COPYING.md
# 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.
references/GLOBAL-STATE.md
# 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 defaultLogger = New("", os.Stderr)

func Default() *Logger { return defaultLogger }

func (l *Logger) Info(msg string) {
    fmt.Fprintf(l.out, "%s%s\n", l.prefix, msg)
}

// Package-level convenience functions delegate to the default instance.
func Info(msg string) { defaultLogger.Info(msg) }
```

```go
// Callers use the default for simple cases
mylog.Info("starting server")

// Tests or specialized code create custom instances
logger := mylog.New("[test] ", &buf)
logger.Info("test message")
```

Standard library examples of this pattern:
- `log.New()` + `log.Default()` + `log.Println()`
- `http.NewServeMux()` + `http.DefaultServeMux`
- `flag.NewFlagSet()` + `flag.CommandLine`

## Dependency Injection as the Preferred Alternative

When code needs configurable behavior, accept dependencies as constructor
parameters or struct fields instead of reading package-level variables.

**Bad**
```go
var db *sql.DB

func GetUser(id int) (*User, error) {
    return db.QueryRow("SELECT ...", id) // depends on global
}
```

**Good**
```go
type UserStore struct {
    db *sql.DB
}

func NewUserStore(db *sql.DB) *UserStore {
    return &UserStore{db: db}
}

func (s *UserStore) GetUser(id int) (*User, error) {
    return s.db.QueryRow("SELECT ...", id)
}
```

Benefits of injection:
- Tests provide mock or in-memory implementations
- Multiple instances can coexist (e.g., read replica vs primary)
- Dependencies are explicit in the constructor signature

## Injecting Time

A common case: replacing `time.Now` for deterministic tests.

**Bad**
```go
func IsExpired(expiry time.Time) bool {
    return time.Now().After(expiry) // untestable
}
```

**Good**
```go
type Checker struct {
    now func() time.Time
}

func NewChecker() *Checker {
    return &Checker{now: time.Now}
}

func (c *Checker) IsExpired(expiry time.Time) bool {
    return c.now().After(expiry)
}
```

Tests replace `now` with a fixed function:

```go
c := &Checker{now: func() time.Time {
    return time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)
}}
```

## Summary

| Situation | Approach |
|-----------|----------|
| Process-wide singleton (logger, metrics) | Default instance + `New()` constructor |
| Compiled-once regex or template | Package-level `var` with `MustCompile` |
| Registry (database drivers, codecs) | Package-level `Register()` function |
| Configurable behavior | Dependency injection via constructor |
| Time-dependent logic | Inject `func() time.Time` |
| Anything tests need to vary | Do not use global state |
references/MUST-FUNCTIONS.md
# Must Functions

> **Source**: Uber Style Guide, Go standard library conventions

`Must` functions wrap a fallible function and panic on error. Use them **only**
during program initialization where failure means the program cannot run.

## Standard Library Examples

```go
// regexp.MustCompile panics if the pattern is invalid
var validID = regexp.MustCompile(`^[a-z][a-z0-9-]{0,62}$`)

// template.Must panics if template parsing fails
var tmpl = template.Must(template.ParseFiles("index.html"))
```

These are safe because they run at package init time — if they fail, the
program cannot function correctly.

## When to Use Must

```
Is this called during program initialization (package-level var, init, main setup)?
├─ Yes → Is failure unrecoverable (config, regex, template)?
│        ├─ Yes → Must is appropriate
│        └─ No  → Return error instead
└─ No  → Never use Must — return error
```

### Appropriate Uses

- **Package-level `var`**: Compiling regexps, parsing templates, loading
  required config
- **`init()` or early `main()`**: Setting up resources that must exist for
  the program to run
- **Test helpers**: `t.Fatal` is preferred in tests, but Must can be
  acceptable for test fixtures

### Never Use Must For

- Runtime request handling
- User-supplied input
- Network or file operations that can legitimately fail
- Anything called after program startup

## Writing a Must Function

Follow the naming convention `MustX` where `X` is the fallible function name:

```go
func MustParseConfig(path string) *Config {
    cfg, err := ParseConfig(path)
    if err != nil {
        panic(fmt.Sprintf("parsing config %s: %v", path, err))
    }
    return cfg
}
```

### Guidelines

- **Name**: `Must` prefix + the fallible function name (e.g., `MustParse`,
  `MustNew`, `MustCompile`)
- **Panic message**: Include the input and the error for debuggability
- **Document**: Always document that the function panics on error

```go
// MustParseConfig parses the config file at path.
// It panics if the file cannot be read or contains invalid configuration.
func MustParseConfig(path string) *Config { ... }
```

### Generic Must Helper

For one-off uses, a generic Must helper avoids boilerplate:

```go
func Must[T any](v T, err error) T {
    if err != nil {
        panic(err)
    }
    return v
}

// Usage at package level
var cfg = Must(ParseConfig("app.yaml"))
```

## Relationship to Panic/Recover

Must functions are a controlled use of `panic`. They should:

- Only run during initialization (so recover is unnecessary)
- Produce clear, actionable panic messages
- Never be used where returning an error is possible

See [PANIC-RECOVER.md](PANIC-RECOVER.md) for the full panic/recover pattern.
references/PANIC-RECOVER.md
# Panic and Recover Patterns

> **Source**: Effective Go

## Panic Guidelines

`panic` creates a run-time error that stops the program. Use it only for truly
unrecoverable situations.

### When to Panic

Real library functions should **avoid panic**. If the problem can be masked or
worked around, let things continue rather than taking down the whole program.

```go
// Acceptable: Truly impossible situation
func CubeRoot(x float64) float64 {
    z := x/3
    for i := 0; i < 1e6; i++ {
        prevz := z
        z -= (z*z*z-x) / (3*z*z)
        if veryClose(z, prevz) {
            return z
        }
    }
    // A million iterations has not converged; something is wrong.
    panic(fmt.Sprintf("CubeRoot(%g) did not converge", x))
}
```

### Panic in Initialization

Exception: If a library truly cannot set itself up during `init()`, it may be
reasonable to panic:

```go
var user = os.Getenv("USER")

func init() {
    if user == "" {
        panic("no value for $USER")
    }
}
```

### When Panic IS Acceptable

Beyond initialization, panic is acceptable in these narrow cases:

1. **API misuse** — analogous to how core language panics on out-of-bounds
   access. The `reflect` package uses this approach.
2. **Internal implementation detail with matching `recover`** at the package
   boundary. Panic simplifies deeply-nested control flow while the public API
   still returns errors (the Parse/parseInt pattern below).
3. **`panic("unreachable")`** after `log.Fatal` when the compiler can't detect
   unreachable code.

#### Parse/parseInt Pattern

Use panic internally to unwind complex recursion, but always convert to an error
at the package boundary:

```go
func parseInt(in string) int {
    n, err := strconv.Atoi(in)
    if err != nil {
        panic(&syntaxError{"not a valid integer"})
    }
    return n
}

func Parse(in string) (_ *Node, err error) {
    defer func() {
        if p := recover(); p != nil {
            sErr, ok := p.(*syntaxError)
            if !ok {
                panic(p)  // not ours — re-panic
            }
            err = fmt.Errorf("syntax error: %v", sErr.msg)
        }
    }()
    // ... calls parseInt internally
}
```

**Key**: The type check `p.(*syntaxError)` ensures only *our* panics are caught.
Unexpected panics (nil pointer, etc.) propagate normally.

---

## Recover Patterns

`recover` regains control of a panicking goroutine. It only works inside
deferred functions.

### Basic Recovery Pattern

```go
func safelyDo(work *Work) {
    defer func() {
        if err := recover(); err != nil {
            log.Println("work failed:", err)
        }
    }()
    do(work)
}
```

### Server Goroutine Protection

Isolate panics to individual goroutines in servers:

```go
func server(workChan <-chan *Work) {
    for work := range workChan {
        go safelyDo(work)  // Each worker is protected
    }
}
```

If `do(work)` panics, the result is logged and the goroutine exits cleanly
without disturbing others.

### Package-Internal Panic/Recover

Use panic internally but convert to errors at API boundaries:

```go
// Error is a parse error type
type Error string
func (e Error) Error() string { return string(e) }

// Internal: panic with Error type
func (regexp *Regexp) error(err string) {
    panic(Error(err))
}

// External API: converts panic to error return
func Compile(str string) (regexp *Regexp, err error) {
    regexp = new(Regexp)
    defer func() {
        if e := recover(); e != nil {
            regexp = nil
            err = e.(Error)  // Re-panics if not our Error type
        }
    }()
    return regexp.doParse(str), nil
}
```

**Key points:**

- Deferred functions can modify named return values
- Type assertion `e.(Error)` re-panics on unexpected error types
- Never expose panics to clients—always convert at API boundary

---

## Quick Reference

| Pattern | Description |
|---------|-------------|
| Basic recovery | `defer func() { if err := recover(); err != nil { ... } }()` |
| Server protection | Wrap each goroutine handler in safelyDo |
| Package-internal | Panic internally, recover and return error at API boundary |
| Type-safe recovery | Use type assertion to re-panic on unexpected errors |

## When to Use

- **Panic**: Only for truly unrecoverable situations or init failures
- **Recover**: Server handlers, package-internal error simplification
- **Never**: Expose panics across package boundaries—always convert to errors
references/TIME-ENUMS-TAGS.md
# Time, Struct Tags, and Embedding Patterns

## Use time.Time and time.Duration

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 Embedding Types in Public Structs

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

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.