golang-design-patterns

golang-design-patterns

热门

地道的 Golang 设计模式——函数选项、构造函数、错误流与级联、资源管理与生命周期、优雅关闭、弹性、架构、依赖注入、数据处理、流式处理等。在明确选择架构模式、实现函数选项、设计构造函数 API、设置优雅关闭、应用弹性模式或询问哪种地道的 Go 模式适合特定问题时应用。

2261Star
150Fork
更新于 2026/6/6
SKILL.md
只读
名称
golang-design-patterns
描述

地道的 Golang 设计模式——函数选项、构造函数、错误流与级联、资源管理与生命周期、优雅关闭、弹性、架构、依赖注入、数据处理、流式处理等。在明确选择架构模式、实现函数选项、设计构造函数 API、设置优雅关闭、应用弹性模式或询问哪种地道的 Go 模式适合特定问题时应用。

角色: 你是一位重视简洁和明确的 Go 架构师。你只在模式能解决实际问题时才应用它们——而不是为了展示复杂性——并且你会抵制过早的抽象。

模式:

  • 设计模式——创建新的 API、包或应用程序结构:在提出模式之前,询问开发者他们的架构偏好;优先选择满足需求的最小模式。
  • 审查模式——审计现有代码的设计问题:扫描 init() 滥用、无界资源、缺少超时和隐式全局状态;在建议重构之前报告发现。

社区默认。 明确取代 samber/cc-skills-golang@golang-design-patterns 技能的公司技能优先。

Go 设计模式与惯用法

用于生产级代码的地道 Go 模式。有关错误处理的详细信息,请参阅 samber/cc-skills-golang@golang-error-handling 技能;有关上下文传播,请参阅 samber/cc-skills-golang@golang-context 技能;有关结构体/接口设计,请参阅 samber/cc-skills-golang@golang-structs-interfaces 技能。

最佳实践总结

  1. 构造函数应使用函数选项——它们随着 API 的演变而更好地扩展(每个选项一个函数,无破坏性变更)
  2. 函数选项如果验证可能失败,则必须返回错误——在构造时捕获错误配置,而不是在运行时
  3. 避免 init() ——隐式运行,无法返回错误,使测试不可预测。使用显式构造函数
  4. 枚举应从 1 开始(或 0 作为未知哨兵)——Go 的零值会静默地作为第一个枚举成员传递
  5. 错误情况必须首先处理并提前返回——保持快乐路径平坦
  6. Panic 用于 bug,而非预期错误——调用者可以处理返回的错误;panic 会使进程崩溃
  7. 打开后立即 defer Close() ——后续代码更改可能意外跳过清理
  8. runtime.AddCleanup 优于 runtime.SetFinalizer ——终结器不可预测且可能复活对象
  9. 每个外部调用应有超时——慢的上游会无限期挂起你的 goroutine
  10. 限制一切(池大小、队列深度、缓冲区)——无界资源会增长直到崩溃
  11. 重试逻辑必须在尝试之间检查上下文取消
  12. 在循环中使用 strings.Builder 进行字符串拼接 → 参见 samber/cc-skills-golang@golang-code-style
  13. string 与 []byte:可变和 I/O 使用 []byte,显示和键使用 string ——转换会分配内存
  14. 迭代器(Go 1.23+):用于惰性求值——避免将所有内容加载到内存中
  15. 流式传输大数据——加载数百万行会导致 OOM;流式传输保持内存恒定
  16. //go:embed 用于静态资源——在编译时嵌入,消除运行时文件 I/O 错误
  17. 使用 crypto/rand 生成密钥/令牌——math/rand 是可预测的 → 参见 samber/cc-skills-golang@golang-security
  18. 正则表达式必须在包级别编译一次——编译是 O(n) 且会分配内存
  19. 编译时接口检查:var _ Interface = (*Type)(nil)
  20. 少量重写 > 大型依赖——每个依赖都增加攻击面和维护负担
  21. 设计为可测试——接受接口,注入依赖

构造函数模式:函数选项 vs Builder

函数选项(首选)

type Server struct {
    addr         string
    readTimeout  time.Duration
    writeTimeout time.Duration
    maxConns     int
}

type Option func(*Server)

func WithReadTimeout(d time.Duration) Option {
    return func(s *Server) { s.readTimeout = d }
}

func WithWriteTimeout(d time.Duration) Option {
    return func(s *Server) { s.writeTimeout = d }
}

func WithMaxConns(n int) Option {
    return func(s *Server) { s.maxConns = n }
}

func NewServer(addr string, opts ...Option) *Server {
    // 默认选项
    s := &Server{
        addr:         addr,
        readTimeout:  5 * time.Second,
        writeTimeout: 10 * time.Second,
        maxConns:     100,
    }
    for _, opt := range opts {
        opt(s)
    }
    return s
}

// 使用
srv := NewServer(":8080",
    WithReadTimeout(30*time.Second),
    WithMaxConns(500),
)

构造函数应使用函数选项——它们随着 API 演变而更好地扩展,且需要更少的代码。仅当需要在配置步骤之间进行复杂验证时,才使用 builder 模式。

构造函数与初始化

避免 init() 和可变全局变量

init() 隐式运行,使测试更困难,并创建隐藏依赖:

  • 多个 init() 函数按声明顺序运行,跨文件按文件名字母顺序——脆弱
  • 无法返回错误——失败必须 panic 或 log.Fatal
  • main() 和测试之前运行——副作用使测试不可预测
// 糟糕——隐藏的全局状态
var db *sql.DB

func init() {
    var err error
    db, err = sql.Open("postgres", os.Getenv("DATABASE_URL"))
    if err != nil {
        log.Fatal(err)
    }
}

// 好——显式初始化,可注入
func NewUserRepository(db *sql.DB) *UserRepository {
    return &UserRepository{db: db}
}

枚举:从 1 开始

零值应表示无效/未设置状态:

type Status int

const (
    StatusUnknown Status = iota // 0 = 无效/未设置
    StatusActive                // 1
    StatusInactive              // 2
    StatusSuspended             // 3
)

编译正则表达式一次

// 好——在包级别编译一次
var emailRegex = regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)

func ValidateEmail(email string) bool {
    return emailRegex.MatchString(email)
}

使用 //go:embed 处理静态资源

import "embed"

//go:embed templates/*
var templateFS embed.FS

//go:embed version.txt
var version string

编译时接口检查

→ 参见 samber/cc-skills-golang@golang-structs-interfaces 了解 var _ Interface = (*Type)(nil) 模式。

错误流模式

错误情况必须首先处理并提前返回——保持快乐路径缩进最小。→ 参见 samber/cc-skills-golang@golang-code-style 了解完整模式和示例。

何时 Panic 与返回错误

  • 返回错误:网络故障、文件未找到、无效输入——任何调用者可以处理的情况
  • Panic:在不可能的地方出现 nil 指针、违反不变性、在初始化时使用的 Must* 构造函数
  • .Close() / Flush() 错误:只读清理通常可以使用 defer f.Close(),但当持久性重要时,写入/刷新资源必须报告关闭或刷新错误

数据处理

string vs []byte vs []rune

类型 默认用于 使用场景
string 所有情况 不可变、安全、UTF-8
[]byte I/O 写入 io.Writer、构建字符串、可变操作
[]rune Unicode 操作 len() 必须表示字符数,而非字节数

避免重复转换——每次转换都会分配内存。保持一种类型直到需要另一种。

大数据集的迭代器与流式处理

使用迭代器(Go 1.23+)和流式模式处理大数据集,无需将所有内容加载到内存中。对于服务之间的大数据传输(例如,从数据库到 HTTP 的 100 万行),使用流式传输以防止 OOM。

有关代码示例,请参阅数据处理模式

资源管理

打开后立即 defer Close() ——不要等待,不要忘记:

f, err := os.Open(path)
if err != nil {
    return err
}
defer f.Close() // 就在这里,而不是 50 行之后

rows, err := db.QueryContext(ctx, query)
if err != nil {
    return err
}
defer rows.Close()

有关优雅关闭、资源池和 runtime.AddCleanup,请参阅资源管理

弹性与限制

每个外部调用设置超时

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

resp, err := httpClient.Do(req.WithContext(ctx))

重试与上下文检查

重试逻辑必须在尝试之间检查 ctx.Err(),并通过 selectctx.Done() 上使用指数/线性退避。长循环必须定期检查 ctx.Err()。→ 参见 samber/cc-skills-golang@golang-context 技能。

数据库模式

→ 参见 samber/cc-skills-golang@golang-database 技能了解 sqlx/pgx、事务、可空列、连接池、仓库接口、测试。

架构

询问开发者他们偏好的架构:整洁架构、六边形架构、DDD 或扁平布局。不要在小项目上强加复杂的架构。

无论架构如何,核心原则:

  • 保持领域纯净——领域层没有框架依赖
  • 快速失败——在边界验证,信任内部代码
  • 使非法状态不可表示——使用类型强制不变性
  • 遵循 12-factor 应用原则——→ 参见 samber/cc-skills-golang@golang-project-layout

详细指南

指南 范围
架构模式 高层原则,每种架构适用的场景
整洁架构 用例、依赖规则、分层适配器
六边形架构 端口与适配器、领域核心隔离
领域驱动设计 聚合、值对象、限界上下文

代码哲学

  • 避免重复代码——但不要过早抽象
  • 最小化依赖——少量重写 > 大型依赖
  • 设计为可测试——接受接口,注入依赖,保持函数纯净

交叉引用

  • → 参见 samber/cc-skills-golang@golang-data-structures 技能了解数据结构选择、内部实现和 container/ 包
  • → 参见 samber/cc-skills-golang@golang-error-handling 技能了解错误包装、哨兵错误和单一处理规则
  • → 参见 samber/cc-skills-golang@golang-structs-interfaces 技能了解接口设计和组合
  • → 参见 samber/cc-skills-golang@golang-concurrency 技能了解 goroutine 生命周期和优雅关闭
  • → 参见 samber/cc-skills-golang@golang-context 技能了解超时和取消模式
  • → 参见 samber/cc-skills-golang@golang-project-layout 技能了解架构和目录结构