go-defensive

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Go Defensive Programming Patterns

Go语言防御式编程模式

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.
来源:Uber Go 风格指南
使用零值断言在编译时验证接口合规性。
不良示例
go
type Handler struct{}

func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  // ...
}
良好示例
go
type Handler struct{}

var _ http.Handler = (*Handler)(nil)

func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  // ...
}
指针类型、切片、映射使用
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.
来源:Uber Go 风格指南
切片和映射包含指针。在API边界处进行复制,以防止意外修改。

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)
}
不良示例
go
func (d *Driver) SetTrips(trips []Trip) {
  d.trips = trips  // 调用者仍可修改d.trips
}
良好示例
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
}
不良示例
go
func (s *Stats) Snapshot() map[string]int {
  s.mu.Lock()
  defer s.mu.Unlock()
  return s.counters  // 暴露内部状态!
}
良好示例
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

使用defer进行资源清理

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.
来源:Uber Go 风格指南、Effective Go
使用
defer
清理资源(文件、锁)。避免在多返回路径中遗漏资源清理。
不良示例
go
p.Lock()
if p.count < 10 {
  p.Unlock()
  return p.count
}
p.count++
newCount := p.count
p.Unlock()
return newCount  // 容易忘记解锁
良好示例
go
p.Lock()
defer p.Unlock()

if p.count < 10 {
  return p.count
}
p.count++
return p.count
defer
的性能开销可以忽略不计。仅在纳秒级关键路径中避免使用。

Defer for File Operations

文件操作中使用defer

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 f.Close()
,以提升代码清晰度:
go
func Contents(filename string) (string, error) {
    f, err := os.Open(filename)
    if err != nil {
        return "", err
    }
    defer f.Close()  // Close与Open相邻 - 清晰度大幅提升

    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  // 文件会被关闭
        }
    }
    return string(result), nil  // 文件会被关闭
}

Defer Argument Evaluation

defer的参数求值时机

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
执行时求值,而非延迟函数运行时:
go
for i := 0; i < 5; i++ {
    defer fmt.Printf("%d ", i)
}
// 输出:4 3 2 1 0(后进先出顺序,值在defer时捕获)

Defer LIFO Order

defer的后进先出顺序

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
多个defer语句按照后进先出的顺序执行:
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()立即执行,un()在返回时执行
    fmt.Println("in a")
}
// 输出:entering: a, in a, leaving: a

Start Enums at One

枚举从1开始

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
).
来源:Uber Go 风格指南
枚举从非零值开始,以区分未初始化值与有效取值。
不良示例
go
const (
  Add Operation = iota  // Add=0,零值看起来像有效取值
  Subtract
  Multiply
)
良好示例
go
const (
  Add Operation = iota + 1  // Add=1,零值代表未初始化
  Subtract
  Multiply
)
例外情况:当零值是合理默认值时(例如
LogToStdout = iota
)。

Use time.Time and time.Duration

使用time.Time和time.Duration

Source: Uber Go Style Guide
Always use the
time
package. Avoid raw
int
for time values.
来源:Uber Go 风格指南
始终使用
time
包。避免使用原始
int
类型表示时间值。

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)
}
不良示例
go
func isActive(now, start, stop int) bool {
  return start <= now && now < stop
}
良好示例
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)
不良示例
go
func poll(delay int) {
  time.Sleep(time.Duration(delay) * time.Millisecond)
}
poll(10)  // 是秒?还是毫秒?
良好示例
go
func poll(delay time.Duration) {
  time.Sleep(delay)
}
poll(10 * time.Second)

JSON Fields

JSON字段

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"`
}
当无法使用
time.Duration
时,在字段名中包含单位:
不良示例
go
type Config struct {
  Interval int `json:"interval"`
}
良好示例
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))
}
来源:Uber Go 风格指南
使用依赖注入替代可变全局变量。
不良示例
go
var _timeNow = time.Now

func sign(msg string) string {
  now := _timeNow()
  return signWithTime(msg, now)
}

// 测试需要保存/恢复全局变量
func TestSign(t *testing.T) {
  oldTimeNow := _timeNow
  _timeNow = func() time.Time { return someFixedTime }
  defer func() { _timeNow = oldTimeNow }()
  assert.Equal(t, want, sign(give))
}
良好示例
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)
}

// 测试可以干净地注入依赖
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
来源:Uber Go 风格指南
嵌入类型会暴露实现细节,阻碍类型演进。
不良示例
go
type ConcreteList struct {
  *AbstractList
}
良好示例
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)
}
嵌入类型的问题:
  • 为嵌入接口添加方法属于破坏性变更
  • 从嵌入结构体中移除方法属于破坏性变更
  • 替换嵌入类型属于破坏性变更

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.

来源:Uber Go 风格指南
始终为JSON、YAML等序列化格式使用显式字段标签。
不良示例
go
type Stock struct {
  Price int
  Name  string
}
良好示例
go
type Stock struct {
  Price int    `json:"price"`
  Name  string `json:"name"`
  // 可以安全地将Name重命名为Symbol
}
标签使序列化契约显式化,且在重构时更安全。

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

来源:Go Wiki CodeReviewComments(规范性内容)
不要使用
math/rand
math/rand/v2
生成密钥,即使是临时密钥也不行。这是一个安全问题
未初始化种子或基于时间种子的随机生成器输出可预测:
  • Time.Nanoseconds()
    仅提供少量熵
  • 以此方式生成的密钥可能被攻击者猜测
请使用
crypto/rand
替代:
go
import (
	"crypto/rand"
)

func Key() string {
	return rand.Text()
}
对于文本输出:
  • 直接使用
    crypto/rand.Text
    (推荐)
  • 或者使用
    encoding/hex
    encoding/base64
    编码随机字节

Panic and Recover

Panic与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.

来源:Effective Go
仅在真正无法恢复的情况下使用
panic
。库函数应避免panic——如果问题可以解决,应让程序继续运行而非终止整个程序。
使用
recover
重新控制发生panic的goroutine(仅在延迟函数中有效):
go
func safelyDo(work *Work) {
    defer func() {
        if err := recover(); err != nil {
            log.Println("work failed:", err)
        }
    }()
    do(work)
}
核心规则:
  • 永远不要跨包边界暴露panic——始终将其转换为错误
  • 如果库确实无法完成初始化,在
    init()
    中panic是可接受的
  • 使用recover隔离服务器goroutine处理器中的panic
有关包括服务器保护和包内panic/recover在内的详细模式,请参阅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
模式规则
接口合规性
var _ Interface = (*Type)(nil)
接收切片/映射存储前先复制
返回切片/映射返回副本
资源清理使用
defer
Defer参数时机在defer时求值,而非调用时
枚举
iota + 1
开始
时间点使用
time.Time
时长使用
time.Duration
可变全局变量使用依赖注入
类型嵌入使用显式委托
序列化始终使用字段标签
密钥生成使用
crypto/rand
,切勿使用
math/rand
Panic使用仅用于真正无法恢复的情况
Recover模式在defer中使用;在API边界转换为错误

See Also

另请参阅

  • go-style-core
    - Core Go style principles
  • go-concurrency
    - Goroutine and channel patterns
  • go-error-handling
    - Error handling best practices
  • go-style-core
    - Go语言核心风格原则
  • go-concurrency
    - Goroutine与通道模式
  • go-error-handling
    - 错误处理最佳实践