首页
/ ECC golang-patterns 技能实战指南:Go 函数式选项、并发、错误处理与包组织惯用法

ECC golang-patterns 技能实战指南:Go 函数式选项、并发、错误处理与包组织惯用法

2026-09-06 17:56:56作者:尤峻淳Whitney

本篇围绕 ECC 仓库中的 golang-patterns 技能文档(.kiro/skills/golang-patterns/SKILL.md)展开,系统讲解 Go 项目中函数式选项、小接口、依赖注入、Worker Pool 并发、错误处理与包组织等惯用模式的完整写法,并结合仓库中的规则文件、审查 Agent 与命令注册表说明这套模式如何被接入 ECC 的自动化编码与评审流程。读完后你既能直接复制这些模式到自己的 Go 项目,也能理解 ECC 是如何让 Agent 在编写和审查 Go 代码时强制遵循这些惯例的。

技能定位与触发条件

golang-patterns 是 ECC 面向 Go 代码的专用模式技能。它的 frontmatter 元数据声明了触发范围:

---
name: golang-patterns
description: >
  Go-specific design patterns and best practices including functional options,
  small interfaces, dependency injection, concurrency patterns, error handling,
  and package organization. Use when working with Go code to apply idiomatic
  Go patterns.
metadata:
  origin: ECC
  globs: ["**/*.go", "**/go.mod", "**/go.sum"]
---

globs 字段表明:当工作对象匹配 **/*.go**/go.mod**/go.sum 时,该技能应被激活。文档末尾的 “When to Use This Skill” 进一步列出了适用场景:

  • 设计 Go API 和包
  • 实现并发系统
  • 组织 Go 项目结构
  • 编写地道的(idiomatic)Go 代码
  • 重构现有 Go 代码库

在仓库层面,该技能被多个组件显式引用:

值得注意的是,仓库中还存在一份内容更详尽的主技能版本 skills/golang-patterns/SKILL.md,它包含优雅关闭、errgroup 协调、goroutine 泄漏规避、内存与性能优化以及 lint 配置等进阶内容。下文以 .kiro 版本文档为骨架,在主技能中有对应佐证的地方会加以标注。

函数式选项(Functional Options)

当构造器的可选参数越来越多时,Go 惯用的方案是函数式选项模式。文档给出的标准写法:

type Option func(*Server)

func WithPort(port int) Option {
    return func(s *Server) { s.port = port }
}

func NewServer(opts ...Option) *Server {
    s := &Server{port: 8080}
    for _, opt := range opts {
        opt(s)
    }
    return s
}

其工作机制是:Option 类型本身是一个“修改器闭包”,每个 WithXxx 工厂函数返回一个对 *Server 私有字段的赋值闭包;NewServer 先建立带默认值(port: 8080)的零配置实例,再依次应用调用者传入的选项覆盖默认值。文档总结了三点收益:

  • 向后兼容的 API 演进:新增 WithXxx 不会破坏既有调用方签名;
  • 带默认值的可选参数:省略某个选项即回落到结构体字面量中的默认值;
  • 自文档化的配置:调用点 NewServer(WithPort(9090), WithLogger(l)) 读起来就是配置清单。

主技能版本 skills/golang-patterns/SKILL.md 给出了更完整的工程化示例:Server 同时配置 addrtimeout(默认 30 * time.Second)、logger(默认 log.Default()),并提供 WithTimeoutWithLogger 两个选项——这展示了“每个选项都对应一个默认值”的完整范式。规则文件 rules/golang/patterns.md 中也内嵌了同一示例,确保规则层与技能层口径一致。

小接口(Small Interfaces)

文档强调的核心原则是:接口定义在使用方(consumer),而不是实现方,并遵循 “Accept interfaces, return structs”(接收接口,返回结构体)。

// Good: Small, focused interface defined at point of use
type UserStore interface {
    GetUser(id string) (*User, error)
}

func ProcessUser(store UserStore, id string) error {
    user, err := store.GetUser(id)
    // ...
}

收益包括:更易测试和打桩(mock)、松耦合、依赖关系清晰。把 UserStore 定义在调用它的服务包内,意味着实现该接口的具体存储(数据库、内存、RPC)无需知道自己满足了这个接口,也无需反向依赖服务包。rules/golang/coding-style.md 将这条原则进一步量化为“接口保持 1–3 个方法”,并同样要求“接收接口、返回结构体”。

依赖注入(Dependency Injection)

文档推荐的注入方式是构造器函数

func NewUserService(repo UserRepository, logger Logger) *UserService {
    return &UserService{
        repo:   repo,
        logger: logger,
    }
}

配套的模式约定有四点:

  • 使用 New* 前缀的构造器函数;
  • 依赖显式地作为参数传入,而不是从全局变量获取;
  • 返回具体类型(而非接口),把接口的选择权留给调用方;
  • 在构造器中校验依赖(如 repo == nil 时直接报错),让非法状态在创建期而非运行期暴露。

这条路线与主技能中“避免包级可变状态”的反例形成呼应:var db *sql.DBinit() 的写法会让测试无法替换依赖,而 NewServer(db *sql.DB) 式的显式注入则天然可测。

并发模式(Concurrency Patterns)

Worker Pool

文档给出的工作池骨架:

func workerPool(jobs <-chan Job, results chan<- Result, workers int) {
    var wg sync.WaitGroup
    for i := 0; i < workers; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            for job := range jobs {
                results <- processJob(job)
            }
        }()
    }
    wg.Wait()
    close(results)
}

从源码结构看,这个实现包含三个关键协作点:

  1. 单向 channel 收敛方向jobs <-chan Jobresults chan<- Result 强制 worker 只能读任务、只能写结果,编译器层面杜绝误用;
  2. sync.WaitGroup 协调生命周期wg.Add(1) 在 goroutine 启动前调用,defer wg.Done() 保证异常路径也不漏计数;
  3. 收尾责任在协调方:worker 在 jobs 被关闭后自然退出,调用方在 wg.Wait() 之后统一 close(results),由它来终结结果通道——这与主技能中“避免 goroutine 泄漏”一节(缓冲通道 + select 兜底 ctx.Done())互为补充,说明 ECC 对 channel 生命周期管理的统一要求。

Context Propagation

文档要求 context 永远作为第一个参数传递,并在关键路径检查取消:

func FetchUser(ctx context.Context, id string) (*User, error) {
    // Check context cancellation
    select {
    case <-ctx.Done():
        return nil, ctx.Err()
    default:
    }
    // ... fetch logic
}

这里的 select { case <-ctx.Done(): default: } 是非阻塞的“取消探针”:若上下文尚未取消则直接走 default 分支继续执行,一旦取消立即返回 ctx.Err()context.Canceledcontext.DeadlineExceeded)。agents/go-reviewer.md 把 “Context not propagated” 列为 HIGH 级问题、把 “ctx context.Context 应为第一参数” 列为最佳实践检查项,可见这一条在 ECC 评审流程中是硬性红线。

错误处理(Error Handling)

错误包装(Error Wrapping)

if err != nil {
    return fmt.Errorf("failed to fetch user %s: %w", id, err)
}

%w 动词是核心:它让外层错误“包裹”内层错误,调用方既能通过 errors.Is 沿链匹配哨兵错误,也能通过 errors.As 提取特定错误类型。rules/golang/coding-style.md 将“始终用 context 包装错误”列为强制条款,agents/go-reviewer.md 则把“缺少错误包装(裸 return err)”归入 CRITICAL 级错误处理问题,与“用 _ 丢弃错误”“可恢复错误使用 panic”并列。

自定义错误(Custom Errors)

type ValidationError struct {
    Field string
    Msg   string
}

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

实现 error 接口只需一个 Error() string 方法。结构体携带的 FieldMsg 字段让上层可以程序化地读取错误细节(例如在 HTTP 层按字段渲染表单错误),而不仅仅是打印消息字符串。

哨兵错误(Sentinel Errors)

var (
    ErrNotFound = errors.New("not found")
    ErrInvalid  = errors.New("invalid input")
)

// Check with errors.Is
if errors.Is(err, ErrNotFound) {
    // handle not found
}

哨兵错误适合“不需要额外数据、只需判断种类”的常见失败。注意判断必须用 errors.Is 而不是 err == ErrNotFound——因为错误链中任意一层 errors.Is 命中即算匹配,直接比较会漏掉被 %w 包装过的情况。agents/go-reviewer.md 明确将 “Missing errors.Is/As: Use errors.Is(err, target) not err == target” 列为 CRITICAL 检查点。

包组织(Package Organization)

目录结构

文档给出的标准三层布局:

project/
├── cmd/              # Main applications
│   └── server/
│       └── main.go
├── internal/         # Private application code
│   ├── domain/       # Business logic
│   ├── handler/      # HTTP handlers
│   └── repository/   # Data access
└── pkg/              # Public libraries

三个目录各司其职:cmd/ 只放可执行入口;internal/ 借助 Go 编译器强制禁止外部模块导入,是“私有应用代码”的物理边界,内部按 domain(业务逻辑)、handler(HTTP 层)、repository(数据访问)分层;pkg/ 存放打算被其他模块复用的公共库。

命名约定

  • 包名:全小写、单个词;
  • 避免 stutter( stuttering):写 user.User 而不是 user.UserModel——包名已经提供命名空间,类型名不应再重复它;
  • 私有代码放入 internal/
  • 保持 main 包尽量薄,只做依赖装配与启动。

测试模式(Testing Patterns)

表驱动测试(Table-Driven Tests)

func TestValidate(t *testing.T) {
    tests := []struct {
        name    string
        input   string
        wantErr bool
    }{
        {"valid", "test@example.com", false},
        {"invalid", "not-an-email", true},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            err := Validate(tt.input)
            if (err != nil) != tt.wantErr {
                t.Errorf("got error %v, wantErr %v", err, tt.wantErr)
            }
        })
    }
}

匿名结构体切片把“用例名、输入、期望”声明式地组织成一张表,t.Run(tt.name, ...) 使每个用例在 go test -v 输出中成为独立的子测试,可用 -run 'TestValidate/invalid' 单独运行。这一模式在 ECC 中被提升为强制约定:rules/golang/testing.md 规定“使用标准 go test + 表驱动测试”,commands/go-test.md/go-test 命令把“先写失败的表驱动测试(RED)→ 最小实现(GREEN)→ 重构”作为 TDD 循环,并给出覆盖率目标:关键业务逻辑 100%、公共 API 90%+、一般代码 80%+。

测试辅助函数(Test Helpers)

func testDB(t *testing.T) *sql.DB {
    t.Helper()
    db, err := sql.Open("sqlite3", ":memory:")
    if err != nil {
        t.Fatalf("failed to open test db: %v", err)
    }
    t.Cleanup(func() { db.Close() })
    return db
}

两个细节值得注意:

  • t.Helper() 让失败堆栈指向调用辅助函数的测试用例本身,而不是辅助函数内部;
  • t.Cleanup(func() { db.Close() }) 注册的清理函数在该测试结束时自动执行,无需手写 defer,也不会与测试断言顺序纠缠。

仓库中的工具链与审查闭环

这些模式在 ECC 中不是孤立文档,而是被一条完整的工具链承接。rules/golang/hooks.md 建议在 ~/.claude/settings.json 中配置 PostToolUse 钩子:编辑 .go 文件后自动运行 gofmt/goimports,随后跑 go vet 静态分析,并对改动包执行 staticcheck 扩展检查。

commands/go-review.md 描述的 /go-review 命令则调用 agents/go-reviewer.md 定义的 go-reviewer Agent 执行综合审查,其检查手段与问题分级为:

# 静态分析
go vet ./...
staticcheck ./...
golangci-lint run

# 竞态检测
go build -race ./...
go test -race ./...

# 安全漏洞
govulncheck ./...
严重级 典型问题(摘自 go-reviewer 审查优先级)
CRITICAL 忽略关键路径错误、缺少 %w 错误包装、数据竞争、goroutine 泄漏、可恢复错误用 panic
HIGH context 未传播、非缓冲 channel 死锁、缺少 WaitGroup 协调、可变全局状态、接口滥用
MEDIUM 循环内字符串拼接(应用 strings.Builder)、切片未预分配(make([]T, 0, cap))、未使用表驱动测试、错误消息未用小写

其审批准则是:无 CRITICAL/HIGH 问题则 Approve,仅 MEDIUM 则 Warning,出现 CRITICAL 或 HIGH 则 Block 合并。而更完整的进阶模式——优雅关闭(signal.Notify + server.Shutdown(ctx))、errgroup 协调并发任务、sync.Pool 复用分配、golangci-lint 推荐配置——可在主技能 skills/golang-patterns/SKILL.md 中对照阅读。

小结

.kiro/skills/golang-patterns/SKILL.md 的价值在于把 Go 社区共识浓缩为一组可直接落地的模式卡片:函数式选项解决构造器参数爆炸,小接口加依赖注入让依赖方向单向且可测,Worker Pool 加 context 传递确立并发协作的生命周期纪律,错误包装与哨兵错误构成可诊断的错误链,cmd/internal/pkg 三层布局加命名约定保证项目可维护,表驱动测试加 t.Helper/t.Cleanup 提供可复现的验证基座。在 ECC 的体系里,这些模式又通过 rules/golang/ 下的规则文件、agents/go-reviewer.md 审查 Agent 和 docs/COMMAND-REGISTRY.json 中的 go-build/go-review 命令形成“编写—格式化—静态检查—审查”的自动化闭环,使 Agent 生成和评审 Go 代码时始终锚定同一套惯用法基准。

登录后查看全文
热门项目推荐
相关项目推荐