mcpField
G
golang-idioms

golang-idioms

github.com/rohitg00开发2.8k189 次安装24 周前更新
代码仓库

关于 golang-idioms

面向 Go 开发者的编码参考技能,介绍错误、接口、并发、测试、模块和日志惯用法,也提供上下文传递与类型设计的实用规则。

主要功能

  1. 01带上下文的错误处理
  2. 02小型组合式接口
  3. 03Goroutine 与 Channel 模式
  4. 04上下文传递
  5. 05表驱动测试与辅助函数
  6. 06模块、类型和日志指引

适用场景

  1. 01审查 Go 错误处理
  2. 02设计可取消的并发任务
  3. 03组织表驱动测试
  4. 04检查模块和类型设计

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/rohitg00/awesome-claude-code-toolkit --skill golang-idioms

安装后在 Claude Code 中输入/golang-idioms确认已加载。

详细介绍

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
  • 在适当的层级处理错误;不要记录错误后又返回同一个错误
  • 为预期情况定义哨兵错误
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
}

接口设计

// 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 和 Channel 模式

工作池
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(通过取消 context 或关闭 channel)
  • 使用 sync.WaitGroup 等待 goroutine 完成
  • 当生产者和消费者的运行速度不同时,使用带缓冲的 channel
  • 启动 goroutine 前,必须明确它将如何停止

Context 传递

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 作为第一个参数传入
  • 对所有外部调用使用 context.WithTimeout 或 context.WithDeadline
  • 创建可取消的 context 后,始终使用 defer cancel()
  • 谨慎使用 context.WithValue(仅用于请求级值:跟踪 ID、身份验证信息)
  • 切勿在结构体中存储 context

表驱动测试

func TestValidateEmail(t *testing.T) {
    tests := []struct {
        name  string
        email string
        want  bool
    }{
        {"valid email", "[email protected]", true},
        {"missing @", "userexample.com", false},
        {"empty string", "", false},
        {"multiple @", "user@@example.com", false},
        {"valid with subdomain", "[email protected]", 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)
            }
        })
    }
}
测试辅助函数
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/ 目录存放测试数据。

模块管理

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
)

命令:

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。固定主版本号。更新前查看变更日志。

零值设计

设计类型时,应让其零值也有用:

// 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
    }
    // ...
}

规则:

  • 优先使用零值有实际意义的结构体,而不是构造函数
  • 方法会修改接收者时,使用指针接收者
  • 方法仅读取接收者时,使用值接收者
  • 切勿导出用户不应直接设置的字段;应使用构造函数

结构化日志

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 和操作名称。

常见反模式

  • 返回 interface{} / any,而不是具体类型
  • 使用 init() 执行复杂的初始化(会增加测试难度)
  • 使用 _ 忽略错误且不加注释
  • 使用 goroutine 却不管理其生命周期
  • 锁的作用范围过大,导致互斥争用
  • 误用 channel:简单的共享状态优先使用互斥锁
  • 在超过几行的函数中使用裸返回

评论

支持 Markdown