Go 模式

中级 Intermediate 参考型 Reference ⚡ Claude Code 专属 ⚡ Claude Code Optimized
14 min read · 703 lines

Go 惯用编码模式:并发、错误处理、接口与项目布局

Go 模式

原始文件:skills/golang-patterns/SKILL.md + rules/golang/(coding-style, patterns, hooks, security, testing)+ agents/go-reviewer.md + agents/go-build-resolver.md + commands/go-review.md + commands/go-build.md + commands/go-test.md

核心原则

1. 简洁与清晰(Simplicity and Clarity)

Go 崇尚简洁胜过聪明。代码应该显而易见、易于阅读。

// 正确:清晰直接
func GetUser(id string) (*User, error) {
    user, err := db.FindUser(id)
    if err != nil {
        return nil, fmt.Errorf("get user %s: %w", id, err)
    }
    return user, nil
}

// 错误:过于巧妙
func GetUser(id string) (*User, error) {
    return func() (*User, error) {
        if u, e := db.FindUser(id); e == nil {
            return u, nil
        } else {
            return nil, e
        }
    }()
}

2. 让零值有用(Make the Zero Value Useful)

设计类型时确保零值可以立即使用,无需初始化。

// 正确:零值可用
type Counter struct {
    mu    sync.Mutex
    count int // 零值为 0,可直接使用
}

// bytes.Buffer 零值可直接使用
var buf bytes.Buffer
buf.WriteString("hello")

// 错误:需要初始化
type BadCounter struct {
    counts map[string]int // nil map 会 panic
}

3. 接受接口,返回结构体(Accept Interfaces, Return Structs)

函数应该接受接口参数,返回具体类型。

// 正确:接受接口,返回具体类型
func ProcessData(r io.Reader) (*Result, error) {
    data, err := io.ReadAll(r)
    if err != nil {
        return nil, err
    }
    return &Result{Data: data}, nil
}

编码风格

  • gofmtgoimports 是强制性的 -- 没有风格争论
  • 接口保持小型化(1-3 个方法)
  • 始终用上下文包裹错误
if err != nil {
    return fmt.Errorf("failed to create user: %w", err)
}

错误处理模式

带上下文的错误包裹

func LoadConfig(path string) (*Config, error) {
    data, err := os.ReadFile(path)
    if err != nil {
        return nil, fmt.Errorf("load config %s: %w", path, err)
    }

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

    return &cfg, nil
}

自定义错误类型

// 定义领域特定的错误
type ValidationError struct {
    Field   string
    Message string
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("validation failed on %s: %s", e.Field, e.Message)
}

// 哨兵错误(Sentinel errors)用于常见情况
var (
    ErrNotFound     = errors.New("resource not found")
    ErrUnauthorized = errors.New("unauthorized")
    ErrInvalidInput = errors.New("invalid input")
)

使用 errors.Is 和 errors.As 检查错误

func HandleError(err error) {
    // 检查特定错误
    if errors.Is(err, sql.ErrNoRows) {
        log.Println("未找到记录")
        return
    }

    // 检查错误类型
    var validationErr *ValidationError
    if errors.As(err, &validationErr) {
        log.Printf("字段 %s 验证错误: %s",
            validationErr.Field, validationErr.Message)
        return
    }
}

永远不要忽略错误

// 错误:用空标识符忽略错误
result, _ := doSomething()

// 正确:处理或明确记录为什么可以安全忽略
result, err := doSomething()
if err != nil {
    return err
}

// 可接受:当错误真的不重要时(罕见)
_ = writer.Close() // 尽力清理,错误在其他地方记录

并发模式

工作池(Worker Pool)

func WorkerPool(jobs <-chan Job, results chan<- Result, numWorkers int) {
    var wg sync.WaitGroup

    for i := 0; i < numWorkers; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            for job := range jobs {
                results <- process(job)
            }
        }()
    }

    wg.Wait()
    close(results)
}

Context 用于取消和超时

func FetchWithTimeout(ctx context.Context, url string) ([]byte, error) {
    ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
    defer cancel()

    req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
    if err != nil {
        return nil, fmt.Errorf("create request: %w", err)
    }

    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        return nil, fmt.Errorf("fetch %s: %w", url, err)
    }
    defer resp.Body.Close()

    return io.ReadAll(resp.Body)
}

优雅关闭(Graceful Shutdown)

func GracefulShutdown(server *http.Server) {
    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)

    <-quit
    log.Println("正在关闭服务器...")

    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()

    if err := server.Shutdown(ctx); err != nil {
        log.Fatalf("服务器被迫关闭: %v", err)
    }

    log.Println("服务器已退出")
}

errgroup 用于协调 Goroutine

import "golang.org/x/sync/errgroup"

func FetchAll(ctx context.Context, urls []string) ([][]byte, error) {
    g, ctx := errgroup.WithContext(ctx)
    results := make([][]byte, len(urls))

    for i, url := range urls {
        i, url := i, url // 捕获循环变量
        g.Go(func() error {
            data, err := FetchWithTimeout(ctx, url)
            if err != nil {
                return err
            }
            results[i] = data
            return nil
        })
    }

    if err := g.Wait(); err != nil {
        return nil, err
    }
    return results, nil
}

避免 Goroutine 泄漏

// 错误:如果上下文被取消会导致 goroutine 泄漏
func leakyFetch(ctx context.Context, url string) <-chan []byte {
    ch := make(chan []byte)
    go func() {
        data, _ := fetch(url)
        ch <- data // 没有接收者时永远阻塞
    }()
    return ch
}

// 正确:正确处理取消
func safeFetch(ctx context.Context, url string) <-chan []byte {
    ch := make(chan []byte, 1) // 带缓冲的 channel
    go func() {
        data, err := fetch(url)
        if err != nil {
            return
        }
        select {
        case ch <- data:
        case <-ctx.Done():
        }
    }()
    return ch
}

接口设计

小型、聚焦的接口

// 正确:单方法接口
type Reader interface {
    Read(p []byte) (n int, err error)
}

// 按需组合接口
type ReadWriteCloser interface {
    Reader
    Writer
    Closer
}

在使用处定义接口

// 在消费者包中,而非提供者包中
package service

// UserStore 定义此服务需要什么
type UserStore interface {
    GetUser(id string) (*User, error)
    SaveUser(user *User) error
}

type Service struct {
    store UserStore
}

函数选项模式(Functional Options Pattern)

type Server struct {
    addr    string
    timeout time.Duration
    logger  *log.Logger
}

type Option func(*Server)

func WithTimeout(d time.Duration) Option {
    return func(s *Server) {
        s.timeout = d
    }
}

func WithLogger(l *log.Logger) Option {
    return func(s *Server) {
        s.logger = l
    }
}

func NewServer(addr string, opts ...Option) *Server {
    s := &Server{
        addr:    addr,
        timeout: 30 * time.Second, // 默认值
        logger:  log.Default(),    // 默认值
    }
    for _, opt := range opts {
        opt(s)
    }
    return s
}

// 用法
server := NewServer(":8080",
    WithTimeout(60*time.Second),
    WithLogger(customLogger),
)

包组织

标准项目布局

myproject/
├── cmd/
│   └── myapp/
│       └── main.go           # 入口点
├── internal/
│   ├── handler/              # HTTP 处理器
│   ├── service/              # 业务逻辑
│   ├── repository/           # 数据访问
│   └── config/               # 配置
├── pkg/
│   └── client/               # 公共 API 客户端
├── api/
│   └── v1/                   # API 定义(proto, OpenAPI)
├── testdata/                 # 测试固定数据
├── go.mod
├── go.sum
└── Makefile

包命名

// 正确:简短、小写、无下划线
package http
package json
package user

// 错误:冗长、混合大小写或冗余
package httpHandler
package json_parser
package userService // 冗余的 'Service' 后缀

避免包级状态

// 错误:全局可变状态
var db *sql.DB

func init() {
    db, _ = sql.Open("postgres", os.Getenv("DATABASE_URL"))
}

// 正确:依赖注入
type Server struct {
    db *sql.DB
}

func NewServer(db *sql.DB) *Server {
    return &Server{db: db}
}

内存与性能

已知大小时预分配切片

// 错误:多次增长切片
func processItems(items []Item) []Result {
    var results []Result
    for _, item := range items {
        results = append(results, process(item))
    }
    return results
}

// 正确:单次分配
func processItems(items []Item) []Result {
    results := make([]Result, 0, len(items))
    for _, item := range items {
        results = append(results, process(item))
    }
    return results
}

使用 sync.Pool 处理频繁分配

var bufferPool = sync.Pool{
    New: func() interface{} {
        return new(bytes.Buffer)
    },
}

func ProcessRequest(data []byte) []byte {
    buf := bufferPool.Get().(*bytes.Buffer)
    defer func() {
        buf.Reset()
        bufferPool.Put(buf)
    }()

    buf.Write(data)
    // 处理...
    return buf.Bytes()
}

避免在循环中拼接字符串

// 错误:大量字符串分配
func join(parts []string) string {
    var result string
    for _, p := range parts {
        result += p + ","
    }
    return result
}

// 正确:使用 strings.Builder 单次分配
func join(parts []string) string {
    var sb strings.Builder
    for i, p := range parts {
        if i > 0 {
            sb.WriteString(",")
        }
        sb.WriteString(p)
    }
    return sb.String()
}

// 最佳:使用标准库
func join(parts []string) string {
    return strings.Join(parts, ",")
}

Go 惯用法速查表

惯用法 描述
接受接口,返回结构体 函数接受接口参数,返回具体类型
错误即值 将错误视为一等值,而非异常
不要通过共享内存通信 使用 channel 在 goroutine 间协调
让零值有用 类型应无需显式初始化即可工作
少量复制优于少量依赖 避免不必要的外部依赖
清晰优于聪明 优先考虑可读性
gofmt 没人喜欢但人人的朋友 始终使用 gofmt/goimports 格式化
提前返回 先处理错误,保持正常路径不缩进

反模式

// 错误:长函数中的裸返回
func process() (result int, err error) {
    // ... 50 行 ...
    return // 返回的是什么?
}

// 错误:使用 panic 做控制流
func GetUser(id string) *User {
    user, err := db.Find(id)
    if err != nil {
        panic(err) // 不要这样做
    }
    return user
}

// 错误:在结构体中传递 context
type Request struct {
    ctx context.Context // Context 应该作为第一个参数
    ID  string
}

// 正确:Context 作为第一个参数
func ProcessRequest(ctx context.Context, id string) error {
    // ...
}

附录A:Go 代码审查 Agent

来源:agents/go-reviewer.md

审查优先级

级别 检查项
严重 -- 安全 SQL 注入、命令注入、路径遍历、竞态条件、unsafe 包使用、硬编码密钥、不安全的 TLS
严重 -- 错误处理 忽略错误(使用 _)、缺少错误包裹、用 panic 处理可恢复错误、缺少 errors.Is/As
高 -- 并发 goroutine 泄漏、无缓冲 channel 死锁、缺少 sync.WaitGroup、mutex 未使用 defer
高 -- 代码质量 大函数(超过 50 行)、深嵌套(超过 4 层)、非惯用代码、包级变量、接口过度抽象
中 -- 性能 循环中字符串拼接、切片未预分配、N+1 查询、热路径中不必要的分配
中 -- 最佳实践 context 应为第一个参数、表驱动测试、错误消息小写无标点、包名简短小写

诊断命令

go vet ./...
staticcheck ./...
golangci-lint run
go build -race ./...
go test -race ./...
govulncheck ./...

附录B:Go 构建错误解决器

来源:agents/go-build-resolver.md

常见修复模式

错误 原因 修复
undefined: X 缺少导入、拼写错误、未导出 添加导入或修正大小写
cannot use X as type Y 类型不匹配、指针/值 类型转换或解引用
X does not implement Y 缺少方法 用正确的接收者实现方法
import cycle not allowed 循环依赖 提取共享类型到新包
cannot find package 缺少依赖 go get pkg@versiongo mod tidy
declared but not used 未使用的变量/导入 删除或使用空标识符

解决流程

1. go build ./...     -> 解析错误消息
2. 读取受影响文件     -> 理解上下文
3. 应用最小修复       -> 只做需要的
4. go build ./...     -> 验证修复
5. go vet ./...       -> 检查警告
6. go test ./...      -> 确保没有破坏

关键原则

  • 只做手术式修复 -- 不要重构,只修复错误
  • 永远不要在没有明确批准的情况下添加 //nolint
  • 永远不要非必要地更改函数签名
  • 总是在添加/删除导入后运行 go mod tidy

附录C:Go TDD 测试模式

来源:commands/go-test.md

表驱动测试(Table-Driven Tests)

func TestValidateEmail(t *testing.T) {
    tests := []struct {
        name    string
        email   string
        wantErr bool
    }{
        // 有效邮箱
        {"简单邮箱", "user@example.com", false},
        {"带子域名", "user@mail.example.com", false},

        // 无效邮箱
        {"空字符串", "", true},
        {"无 @ 符号", "userexample.com", true},
        {"无域名", "user@", true},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            err := ValidateEmail(tt.email)
            if tt.wantErr && err == nil {
                t.Errorf("ValidateEmail(%q) = nil; 期望错误", tt.email)
            }
            if !tt.wantErr && err != nil {
                t.Errorf("ValidateEmail(%q) = %v; 期望 nil", tt.email, err)
            }
        })
    }
}

并行测试

for _, tt := range tests {
    tt := tt // 捕获循环变量
    t.Run(tt.name, func(t *testing.T) {
        t.Parallel()
        // 测试体
    })
}

测试辅助函数

func setupTestDB(t *testing.T) *sql.DB {
    t.Helper()
    db := createDB()
    t.Cleanup(func() { db.Close() })
    return db
}

覆盖率目标

代码类型 目标
关键业务逻辑 100%
公共 API 90%+
通用代码 80%+
生成的代码 排除

记住:Go 代码应该以最好的方式"无聊" -- 可预测、一致、易于理解。在不确定时,保持简单。

相关技能 Related Skills