golang-idioms

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Go Idioms

Go语言惯用模式

Error Handling

错误处理

go
// Return errors, never panic in library code
func LoadConfig(path string) (Config, error) {
    data, err := os.ReadFile(path)
    if err != nil {
        return Config{}, fmt.Errorf("reading config %s: %w", path, err)
    }

    var cfg Config
    if err := json.Unmarshal(data, &cfg); err != nil {
        return Config{}, fmt.Errorf("parsing config: %w", err)
    }

    return cfg, nil
}
Rules:
  • Always wrap errors with context using
    fmt.Errorf("context: %w", err)
  • Use
    %w
    to allow callers to use
    errors.Is
    and
    errors.As
  • Handle errors at the appropriate level; do not log and return the same error
  • Define sentinel errors for expected conditions
go
var (
    ErrNotFound    = errors.New("not found")
    ErrUnauthorized = errors.New("unauthorized")
)

func GetUser(id string) (User, error) {
    user, ok := store[id]
    if !ok {
        return User{}, fmt.Errorf("user %s: %w", id, ErrNotFound)
    }
    return user, nil
}

// Caller
user, err := GetUser(id)
if errors.Is(err, ErrNotFound) {
    http.Error(w, "user not found", http.StatusNotFound)
    return
}
go
// Return errors, never panic in library code
func LoadConfig(path string) (Config, error) {
    data, err := os.ReadFile(path)
    if err != nil {
        return Config{}, fmt.Errorf("reading config %s: %w", path, err)
    }

    var cfg Config
    if err := json.Unmarshal(data, &cfg); err != nil {
        return Config{}, fmt.Errorf("parsing config: %w", err)
    }

    return cfg, nil
}
规则:
  • 始终使用
    fmt.Errorf("context: %w", err)
    为错误添加上下文信息
  • 使用
    %w
    允许调用方使用
    errors.Is
    errors.As
    判断错误类型
  • 在合适的层级处理错误;不要既记录日志又返回同一个错误
  • 为预期的错误场景定义哨兵错误
go
var (
    ErrNotFound    = errors.New("not found")
    ErrUnauthorized = errors.New("unauthorized")
)

func GetUser(id string) (User, error) {
    user, ok := store[id]
    if !ok {
        return User{}, fmt.Errorf("user %s: %w", id, ErrNotFound)
    }
    return user, nil
}

// Caller
user, err := GetUser(id)
if errors.Is(err, ErrNotFound) {
    http.Error(w, "user not found", http.StatusNotFound)
    return
}

Interface Design

接口设计

go
// Keep interfaces small (1-3 methods)
type Reader interface {
    Read(p []byte) (n int, err error)
}

type UserStore interface {
    GetUser(ctx context.Context, id string) (User, error)
    CreateUser(ctx context.Context, u User) error
}

// Accept interfaces, return structs
func NewService(store UserStore, logger *slog.Logger) *Service {
    return &Service{store: store, logger: logger}
}
Rules:
  • Define interfaces where they are used (consumer side), not where they are implemented
  • Prefer small, composable interfaces over large ones
  • Use
    io.Reader
    ,
    io.Writer
    ,
    fmt.Stringer
    from the standard library
  • An interface with one method should be named after the method +
    er
    suffix
go
// Keep interfaces small (1-3 methods)
type Reader interface {
    Read(p []byte) (n int, err error)
}

type UserStore interface {
    GetUser(ctx context.Context, id string) (User, error)
    CreateUser(ctx context.Context, u User) error
}

// Accept interfaces, return structs
func NewService(store UserStore, logger *slog.Logger) *Service {
    return &Service{store: store, logger: logger}
}
规则:
  • 在接口的使用方(消费者侧)定义接口,而非实现方
  • 优先选择小型、可组合的接口,而非大型接口
  • 使用标准库中的
    io.Reader
    io.Writer
    fmt.Stringer
    等接口
  • 只有一个方法的接口,命名应采用“方法名+er”后缀

Goroutine and Channel Patterns

Goroutine与Channel模式

Worker Pool

工作池

go
func process(ctx context.Context, jobs <-chan Job, workers int) <-chan Result {
    results := make(chan Result, workers)
    var wg sync.WaitGroup

    for range workers {
        wg.Add(1)
        go func() {
            defer wg.Done()
            for job := range jobs {
                select {
                case <-ctx.Done():
                    return
                case results <- job.Execute():
                }
            }
        }()
    }

    go func() {
        wg.Wait()
        close(results)
    }()

    return results
}
go
func process(ctx context.Context, jobs <-chan Job, workers int) <-chan Result {
    results := make(chan Result, workers)
    var wg sync.WaitGroup

    for range workers {
        wg.Add(1)
        go func() {
            defer wg.Done()
            for job := range jobs {
                select {
                case <-ctx.Done():
                    return
                case results <- job.Execute():
                }
            }
        }()
    }

    go func() {
        wg.Wait()
        close(results)
    }()

    return results
}

Fan-out/Fan-in

扇出/扇入

go
func fanOut[T, R any](ctx context.Context, items []T, fn func(T) R, concurrency int) []R {
    sem := make(chan struct{}, concurrency)
    results := make([]R, len(items))
    var wg sync.WaitGroup

    for i, item := range items {
        wg.Add(1)
        sem <- struct{}{}
        go func() {
            defer func() { <-sem; wg.Done() }()
            results[i] = fn(item)
        }()
    }

    wg.Wait()
    return results
}
Rules:
  • Always pass
    context.Context
    as the first parameter
  • Always ensure goroutines can be stopped (via context cancellation or channel close)
  • Use
    sync.WaitGroup
    to wait for goroutine completion
  • Use buffered channels when producer and consumer run at different speeds
  • Never start a goroutine without knowing how it will stop
go
func fanOut[T, R any](ctx context.Context, items []T, fn func(T) R, concurrency int) []R {
    sem := make(chan struct{}, concurrency)
    results := make([]R, len(items))
    var wg sync.WaitGroup

    for i, item := range items {
        wg.Add(1)
        sem <- struct{}{}
        go func() {
            defer func() { <-sem; wg.Done() }()
            results[i] = fn(item)
        }()
    }

    wg.Wait()
    return results
}
规则:
  • 始终将
    context.Context
    作为第一个参数传递
  • 确保Goroutine可以被终止(通过上下文取消或通道关闭)
  • 使用
    sync.WaitGroup
    等待Goroutine完成
  • 当生产者和消费者速度不同时,使用带缓冲的Channel
  • 绝不要在不知道如何终止的情况下启动Goroutine

Context Propagation

上下文传递

go
func (s *Service) HandleRequest(ctx context.Context, req Request) (Response, error) {
    ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
    defer cancel()

    user, err := s.store.GetUser(ctx, req.UserID)
    if err != nil {
        return Response{}, fmt.Errorf("getting user: %w", err)
    }

    ctx = context.WithValue(ctx, userKey, user)
    return s.processRequest(ctx, req)
}
Rules:
  • Pass context as the first parameter of every function that does I/O
  • Use
    context.WithTimeout
    or
    context.WithDeadline
    for all external calls
  • Always
    defer cancel()
    after creating a cancellable context
  • Use
    context.WithValue
    sparingly (request-scoped values only: trace IDs, auth info)
  • Never store context in a struct
go
func (s *Service) HandleRequest(ctx context.Context, req Request) (Response, error) {
    ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
    defer cancel()

    user, err := s.store.GetUser(ctx, req.UserID)
    if err != nil {
        return Response{}, fmt.Errorf("getting user: %w", err)
    }

    ctx = context.WithValue(ctx, userKey, user)
    return s.processRequest(ctx, req)
}
规则:
  • 所有执行I/O操作的函数,都要将上下文作为第一个参数传递
  • 所有外部调用都使用
    context.WithTimeout
    context.WithDeadline
    设置超时
  • 创建可取消的上下文后,始终
    defer cancel()
  • 谨慎使用
    context.WithValue
    (仅用于请求作用域的值:跟踪ID、认证信息等)
  • 绝不要将上下文存储在结构体中

Table-Driven Tests

表格驱动测试

go
func TestValidateEmail(t *testing.T) {
    tests := []struct {
        name  string
        email string
        want  bool
    }{
        {"valid email", "user@example.com", true},
        {"missing @", "userexample.com", false},
        {"empty string", "", false},
        {"multiple @", "user@@example.com", false},
        {"valid with subdomain", "user@mail.example.com", true},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got := ValidateEmail(tt.email)
            if got != tt.want {
                t.Errorf("ValidateEmail(%q) = %v, want %v", tt.email, got, tt.want)
            }
        })
    }
}
go
func TestValidateEmail(t *testing.T) {
    tests := []struct {
        name  string
        email string
        want  bool
    }{
        {"valid email", "user@example.com", true},
        {"missing @", "userexample.com", false},
        {"empty string", "", false},
        {"multiple @", "user@@example.com", false},
        {"valid with subdomain", "user@mail.example.com", true},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got := ValidateEmail(tt.email)
            if got != tt.want {
                t.Errorf("ValidateEmail(%q) = %v, want %v", tt.email, got, tt.want)
            }
        })
    }
}

Test Helpers

测试辅助函数

go
func newTestServer(t *testing.T) *httptest.Server {
    t.Helper()
    handler := setupRoutes()
    srv := httptest.NewServer(handler)
    t.Cleanup(srv.Close)
    return srv
}

func assertEqual[T comparable](t *testing.T, got, want T) {
    t.Helper()
    if got != want {
        t.Errorf("got %v, want %v", got, want)
    }
}
Use
t.Helper()
in all test utility functions. Use
t.Cleanup()
instead of
defer
for test resource cleanup. Use
testdata/
directory for test fixtures.
go
func newTestServer(t *testing.T) *httptest.Server {
    t.Helper()
    handler := setupRoutes()
    srv := httptest.NewServer(handler)
    t.Cleanup(srv.Close)
    return srv
}

func assertEqual[T comparable](t *testing.T, got, want T) {
    t.Helper()
    if got != want {
        t.Errorf("got %v, want %v", got, want)
    }
}
在所有测试工具函数中使用
t.Helper()
。使用
t.Cleanup()
替代
defer
进行测试资源清理。将测试夹具放在
testdata/
目录中。

Module Management

模块管理

go.mod structure:
module github.com/org/project

go 1.23

require (
    github.com/lib/pq v1.10.9
    golang.org/x/sync v0.7.0
)
Commands:
bash
go mod tidy          # remove unused, add missing
go mod verify        # verify checksums
go list -m -u all    # check for updates
go get -u ./...      # update all dependencies
go mod vendor        # vendor dependencies (optional)
Use
go mod tidy
before every commit. Pin major versions. Review changelogs before updating.
go.mod structure:
module github.com/org/project

go 1.23

require (
    github.com/lib/pq v1.10.9
    golang.org/x/sync v0.7.0
)
常用命令:
bash
go mod tidy          # remove unused, add missing
go mod verify        # verify checksums
go list -m -u all    # check for updates
go get -u ./...      # update all dependencies
go mod vendor        # vendor dependencies (optional)
每次提交前都要运行
go mod tidy
。固定主版本号。更新前查看变更日志。

Zero-Value Design

零值设计

Design types so their zero value is useful:
go
// sync.Mutex zero value is an unlocked mutex (ready to use)
var mu sync.Mutex

// bytes.Buffer zero value is an empty buffer (ready to use)
var buf bytes.Buffer
buf.WriteString("hello")

// Custom types: make zero value meaningful
type Server struct {
    Addr    string        // defaults to ""
    Handler http.Handler  // defaults to nil
    Timeout time.Duration // defaults to 0 (no timeout)
}

func (s *Server) ListenAndServe() error {
    addr := s.Addr
    if addr == "" {
        addr = ":8080" // useful default
    }
    handler := s.Handler
    if handler == nil {
        handler = http.DefaultServeMux
    }
    // ...
}
Rules:
  • Prefer structs with meaningful zero values over constructors
  • Use pointer receivers when the method modifies the receiver
  • Use value receivers when the method only reads
  • Never export fields that users should not set directly; use constructor functions
设计类型时,使其零值具备可用性:
go
// sync.Mutex zero value is an unlocked mutex (ready to use)
var mu sync.Mutex

// bytes.Buffer zero value is an empty buffer (ready to use)
var buf bytes.Buffer
buf.WriteString("hello")

// Custom types: make zero value meaningful
type Server struct {
    Addr    string        // defaults to ""
    Handler http.Handler  // defaults to nil
    Timeout time.Duration // defaults to 0 (no timeout)
}

func (s *Server) ListenAndServe() error {
    addr := s.Addr
    if addr == "" {
        addr = ":8080" // useful default
    }
    handler := s.Handler
    if handler == nil {
        handler = http.DefaultServeMux
    }
    // ...
}
规则:
  • 优先选择零值有意义的结构体,而非构造函数
  • 当方法需要修改接收者时,使用指针接收者
  • 当方法仅读取接收者时,使用值接收者
  • 绝不要导出用户不应直接设置的字段;使用构造函数

Structured Logging

结构化日志

go
import "log/slog"

logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
    Level: slog.LevelInfo,
}))

logger.Info("request handled",
    slog.String("method", r.Method),
    slog.String("path", r.URL.Path),
    slog.Int("status", status),
    slog.Duration("latency", time.Since(start)),
)
Use
log/slog
(standard library, Go 1.21+). Use structured fields, never string interpolation. Include request ID, user ID, and operation name in every log entry.
go
import "log/slog"

logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
    Level: slog.LevelInfo,
}))

logger.Info("request handled",
    slog.String("method", r.Method),
    slog.String("path", r.URL.Path),
    slog.Int("status", status),
    slog.Duration("latency", time.Since(start)),
)
使用
log/slog
(标准库,Go 1.21+)。使用结构化字段,绝不使用字符串拼接。每条日志条目都要包含请求ID、用户ID和操作名称。

Common Anti-Patterns

常见反模式

  • Returning
    interface{}
    /
    any
    instead of concrete types
  • Using
    init()
    for complex setup (makes testing hard)
  • Ignoring errors with
    _
    without comment
  • Using goroutines without lifecycle management
  • Mutex contention from overly broad lock scope
  • Channel misuse: prefer mutexes for simple shared state
  • Naked returns in functions longer than a few lines
  • 返回
    interface{}
    /
    any
    而非具体类型
  • 使用
    init()
    进行复杂初始化(会增加测试难度)
  • 无注释的情况下用
    _
    忽略错误
  • 启动Goroutine但不管理其生命周期
  • 锁范围过宽导致互斥锁竞争
  • 错误使用Channel:简单共享状态优先使用互斥锁
  • 多行函数中使用裸返回