go-idioms

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Go Idioms

Go语言惯用写法

Boring, explicit, race-safe Go. Accept interfaces, return structs.
遵循简洁明确、无竞态的Go风格:依赖接口,返回结构体。

Error Handling

错误处理

Always wrap with context at package boundaries:
go
// Use %w to preserve error chain
return fmt.Errorf("fetching user %s: %w", userID, err)

// Check wrapped errors
if errors.Is(err, sql.ErrNoRows) { ... }

var paymentErr *PaymentError
if errors.As(err, &paymentErr) { ... }
Never:
%v
(loses type), raw errors from exported functions, generic context.
在包的边界处始终携带上下文包装错误:
go
// Use %w to preserve error chain
return fmt.Errorf("fetching user %s: %w", userID, err)

// Check wrapped errors
if errors.Is(err, sql.ErrNoRows) { ... }

var paymentErr *PaymentError
if errors.As(err, &paymentErr) { ... }
切勿:使用
%v
(会丢失类型信息)、对外暴露的函数返回原始错误、使用通用上下文。

Interface Design

接口设计

Define interfaces in consuming package, not provider:
go
// notification/sender.go (consumer defines interface)
type EmailSender interface {
    Send(ctx context.Context, to, subject, body string) error
}

// email/client.go (provider implements)
type Client struct { ... }
func (c *Client) Send(ctx context.Context, to, subject, body string) error { ... }
Small interfaces (1-3 methods). Compose larger from smaller:
go
type Reader interface { Read(p []byte) (n int, err error) }
type Writer interface { Write(p []byte) (n int, err error) }
type ReadWriter interface { Reader; Writer }
Compile-time verification:
go
var _ EmailSender = (*Client)(nil)
在消费包中定义接口,而非提供方:
go
// notification/sender.go (consumer defines interface)
type EmailSender interface {
    Send(ctx context.Context, to, subject, body string) error
}

// email/client.go (provider implements)
type Client struct { ... }
func (c *Client) Send(ctx context.Context, to, subject, body string) error { ... }
接口要小(1-3个方法),通过组合小接口形成大接口:
go
type Reader interface { Read(p []byte) (n int, err error) }
type Writer interface { Write(p []byte) (n int, err error) }
type ReadWriter interface { Reader; Writer }
编译时验证:
go
var _ EmailSender = (*Client)(nil)

Concurrency

并发处理

Always propagate context:
go
func FetchData(ctx context.Context) ([]byte, error) {
    select {
    case <-ctx.Done():
        return nil, ctx.Err()
    case result := <-dataChan:
        return result, nil
    }
}
Bounded concurrency (semaphore):
go
sem := make(chan struct{}, 10)
for _, item := range items {
    sem <- struct{}{}
    go func(item Item) {
        defer func() { <-sem }()
        process(item)
    }(item)
}
Race safety: Always run
go test -race ./...
始终传递上下文:
go
func FetchData(ctx context.Context) ([]byte, error) {
    select {
    case <-ctx.Done():
        return nil, ctx.Err()
    case result := <-dataChan:
        return result, nil
    }
}
有界并发(信号量实现):
go
sem := make(chan struct{}, 10)
for _, item := range items {
    sem <- struct{}{}
    go func(item Item) {
        defer func() { <-sem }()
        process(item)
    }(item)
}
竞态安全: 始终运行
go test -race ./...
命令检测竞态条件。

Package Design

包设计

internal/
  user/          # Domain: single purpose
    user.go
    service.go
    repository.go
  order/         # Another domain
  app/           # Dependency wiring
cmd/
  api/           # Entry points
Rules:
  • Single purpose per package
  • No generic names (utils, helpers, common)
  • No circular dependencies
  • Export only what's necessary
internal/
  user/          # Domain: single purpose
    user.go
    service.go
    repository.go
  order/         # Another domain
  app/           # Dependency wiring
cmd/
  api/           # Entry points
规则:
  • 每个包单一职责
  • 避免使用通用名称(如utils、helpers、common)
  • 禁止循环依赖
  • 仅导出必要内容

Dependency Injection

依赖注入

go
// Constructor accepts interfaces
func NewUserService(repo UserRepository, mailer EmailSender) *UserService {
    return &UserService{repo: repo, mailer: mailer}
}

// Wire in app/ package
func NewApp() *App {
    repo := postgres.NewUserRepo(db)
    mailer := sendgrid.NewClient(apiKey)
    userSvc := user.NewUserService(repo, mailer)
    return &App{UserService: userSvc}
}
go
// Constructor accepts interfaces
func NewUserService(repo UserRepository, mailer EmailSender) *UserService {
    return &UserService{repo: repo, mailer: mailer}
}

// Wire in app/ package
func NewApp() *App {
    repo := postgres.NewUserRepo(db)
    mailer := sendgrid.NewClient(apiKey)
    userSvc := user.NewUserService(repo, mailer)
    return &App{UserService: userSvc}
}

Deps Struct Pattern (5+ Parameters)

依赖结构体模式(参数≥5个时使用)

When constructor takes 5+ parameters, use a deps struct:
go
// Group dependencies into named struct
type ServiceDeps struct {
    Repo     Repository
    Mailer   EmailSender
    Logger   Logger
    Config   *Config
    Cache    Cache
}

// Panic on nil - catches programming errors at construction
func NewService(deps ServiceDeps) *Service {
    if deps.Repo == nil {
        panic("NewService: Repo cannot be nil")
    }
    if deps.Mailer == nil {
        panic("NewService: Mailer cannot be nil")
    }
    // ... check all required deps
    return &Service{...}
}
Benefits:
  • Named fields = self-documenting call sites
  • Adding deps doesn't change signature
  • Nil panics catch bugs at construction, not at runtime
When changing old constructor to deps struct:
  • Update ALL call sites to struct literal pattern
  • Check for tests expecting old nil-tolerance behavior
当构造函数参数超过5个时,使用依赖结构体:
go
// Group dependencies into named struct
type ServiceDeps struct {
    Repo     Repository
    Mailer   EmailSender
    Logger   Logger
    Config   *Config
    Cache    Cache
}

// Panic on nil - catches programming errors at construction
func NewService(deps ServiceDeps) *Service {
    if deps.Repo == nil {
        panic("NewService: Repo cannot be nil")
    }
    if deps.Mailer == nil {
        panic("NewService: Mailer cannot be nil")
    }
    // ... check all required deps
    return &Service{...}
}
优势:
  • 命名字段让调用处自文档化
  • 添加依赖无需修改函数签名
  • 空指针 panic 在构造阶段捕获 bug,而非运行时
将旧构造函数改为依赖结构体时:
  • 更新所有调用处为结构体字面量写法
  • 检查测试是否依赖旧的空值兼容行为

Test Cleanup

测试清理

Use
t.Cleanup()
for teardown, not
defer
with error suppression:
go
// BAD - Error suppression hides failures
defer func() { _ = os.Chdir(orig) }()  // SA4017 warning

// GOOD - Proper cleanup with error handling
t.Cleanup(func() {
    if err := os.Chdir(orig); err != nil {
        t.Errorf("cleanup failed: %v", err)
    }
})
t.Cleanup
runs after test completes (even on panic), integrates with test reporting.
使用
t.Cleanup()
进行清理,而非带错误抑制的
defer
go
// BAD - Error suppression hides failures
defer func() { _ = os.Chdir(orig) }()  // SA4017 warning

// GOOD - Proper cleanup with error handling
t.Cleanup(func() {
    if err := os.Chdir(orig); err != nil {
        t.Errorf("cleanup failed: %v", err)
    }
})
t.Cleanup
会在测试完成后运行(即使发生panic),并集成到测试报告中。

Anti-Patterns

反模式

  • Goroutines without cancellation path (leaks)
  • Monolithic interfaces (10+ methods)
  • Framework-like inheritance patterns
  • Reflection when explicit types work
  • Global singletons for dependencies
  • Generic everything (overuse of generics)
  • interface{}
    /
    any
    without justification
  • defer
    with error suppression in tests
  • 没有取消路径的Goroutine(会导致泄漏)
  • 单体式接口(包含10+个方法)
  • 类框架的继承模式
  • 能用显式类型却使用反射
  • 依赖全局单例
  • 过度使用泛型
  • 无正当理由使用
    interface{}
    /
    any
  • 测试中使用
    defer
    并抑制错误

Embrace Boring

拥抱简洁

  • Explicit error handling at each step
  • Standard library first (
    map
    ,
    []T
    ,
    sort.Slice
    )
  • Table-driven tests
  • Struct composition, not inheritance
  • Clear, verbose code over clever code
  • 每一步都显式处理错误
  • 优先使用标准库(如
    map
    []T
    sort.Slice
  • 表驱动测试
  • 结构体组合而非继承
  • 清晰 verbose 的代码优于巧妙的代码