Go编程规范 golang-idioms

Go编程规范是Go语言开发中的最佳实践指南,涵盖了错误处理、接口设计、并发编程、测试技巧和模块管理等关键内容,适用于后端开发,关键词:错误处理、接口设计、并发模式、测试、Go语言、后端开发、编程规范。

后端开发 0 次安装 0 次浏览 更新于 3/8/2026

name: golang-idioms description: Go语言中关于错误处理、接口设计、并发编程、测试和模块管理的惯用模式

Go编程规范

错误处理

// 返回错误,永远不要在库代码中panic
func LoadConfig(path string) (Config, error) {
    data, err := os.ReadFile(path)
    if err != nil {
        return Config{}, fmt.Errorf("读取配置 %s: %w", path, err)
    }

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

    return cfg, nil
}

规则:

  • 始终使用 fmt.Errorf("上下文: %w", err) 包装错误以提供上下文
  • 使用 %w 以便调用者可以使用 errors.Iserrors.As
  • 在适当的层级处理错误;不要同时记录并返回相同的错误
  • 为预期条件定义哨兵错误
var (
    ErrNotFound    = errors.New("未找到")
    ErrUnauthorized = errors.New("未授权")
)

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

// 调用者
user, err := GetUser(id)
if errors.Is(err, ErrNotFound) {
    http.Error(w, "用户未找到", http.StatusNotFound)
    return
}

接口设计

// 保持接口小(1-3个方法)
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
}

// 接受接口,返回结构体
func NewService(store UserStore, logger *slog.Logger) *Service {
    return &Service{store: store, logger: logger}
}

规则:

  • 在接口被使用的地方定义接口(消费者侧),而不是实现的地方
  • 优先选择小、可组合的接口,而不是大的
  • 使用标准库中的 io.Readerio.Writerfmt.Stringer
  • 只有一个方法的接口应以方法名加上 er 后缀命名

Goroutine和通道模式

工作池

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
}

扇出/扇入

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完成
  • 当生产者和消费者运行速度不同时,使用缓冲通道
  • 永远不要启动一个不知道如何停止的goroutine

上下文传播

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("获取用户: %w", err)
    }

    ctx = context.WithValue(ctx, userKey, user)
    return s.processRequest(ctx, req)
}

规则:

  • 对每个进行I/O操作的函数,将上下文作为第一个参数传递
  • 对所有外部调用使用 context.WithTimeoutcontext.WithDeadline
  • 创建可取消上下文后始终使用 defer cancel()
  • 谨慎使用 context.WithValue(仅用于请求范围的值:跟踪ID、认证信息)
  • 永远不要在结构体中存储上下文

表驱动测试

func TestValidateEmail(t *testing.T) {
    tests := []struct {
        name  string
        email string
        want  bool
    }{
        {"有效邮箱", "user@example.com", true},
        {"缺少@", "userexample.com", false},
        {"空字符串", "", false},
        {"多个@", "user@@example.com", false},
        {"带子域名的有效邮箱", "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, 期望 %v", tt.email, got, tt.want)
            }
        })
    }
}

测试辅助函数

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("得到 %v, 期望 %v", got, want)
    }
}

在所有测试实用函数中使用 t.Helper()。使用 t.Cleanup() 而不是 defer 来清理测试资源。使用 testdata/ 目录存放测试夹具。

模块管理

go.mod 结构:
module github.com/org/project

go 1.23

require (
    github.com/lib/pq v1.10.9
    golang.org/x/sync v0.7.0
)

命令:

go mod tidy          # 删除未使用的,添加缺失的
go mod verify        # 验证校验和
go list -m -u all    # 检查更新
go get -u ./...      # 更新所有依赖
go mod vendor        # 供应商化依赖(可选)

每次提交前使用 go mod tidy。锁定主要版本。更新前审查变更日志。

零值设计

设计类型,使其零值有用:

// sync.Mutex 的零值是未锁定的互斥锁(可直接使用)
var mu sync.Mutex

// bytes.Buffer 的零值是空缓冲区(可直接使用)
var buf bytes.Buffer
buf.WriteString("hello")

// 自定义类型:使零值有意义
type Server struct {
    Addr    string        // 默认为 ""
    Handler http.Handler  // 默认为 nil
    Timeout time.Duration // 默认为 0(无超时)
}

func (s *Server) ListenAndServe() error {
    addr := s.Addr
    if addr == "" {
        addr = ":8080" // 有用的默认值
    }
    handler := s.Handler
    if handler == nil {
        handler = http.DefaultServeMux
    }
    // ...
}

规则:

  • 优先选择具有有意义零值的结构体,而不是构造函数
  • 当方法修改接收器时,使用指针接收器
  • 当方法只读取时,使用值接收器
  • 永远不要导出用户不应直接设置的字段;使用构造函数

结构化日志记录

import "log/slog"

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

logger.Info("请求处理",
    slog.String("方法", r.Method),
    slog.String("路径", r.URL.Path),
    slog.Int("状态", status),
    slog.Duration("延迟", time.Since(start)),
)

使用 log/slog(标准库,Go 1.21+)。使用结构化字段,永远不要字符串插值。每个日志条目都包含请求ID、用户ID和操作名称。

常见反模式

  • 返回 interface{} / any 而不是具体类型
  • 使用 init() 进行复杂设置(使测试困难)
  • 忽略错误而不加注释
  • 使用goroutine而不进行生命周期管理
  • 互斥锁争用由于锁范围过宽
  • 通道误用:对于简单共享状态,优先选择互斥锁
  • 在函数较长时使用裸返回