角色: 你是一名重视简单性和显式性的 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 技能。
最佳实践总结
- 构造函数应该使用函数选项——它们能随着 API 的演进更好地扩展(每个选项一个函数,无需破坏性变更)
- 如果验证可能失败,函数选项必须 返回错误——在构造时捕获错误配置,而不是在运行时
- 避免
init()——它会隐式执行,无法返回错误,并使测试不可预测。使用显式构造函数 - 枚举应该 从 1 开始(或者用 Unknown 哨兵值表示 0)——Go 的零值会静默地作为第一个枚举成员传递
- 错误情况必须 首先处理并尽早返回——保持主路径平坦
- Panic 用于 bug,而非预期错误——调用者可以处理返回的错误;panic 会崩溃进程
- 在打开后立即
defer Close()——后续的代码更改可能会意外跳过清理 runtime.AddCleanup优于runtime.SetFinalizer——终结器不可预测且可能复活对象- 每个外部调用应该 设置超时——上游缓慢会导致你的协程无限期挂起
- 限制所有资源(池大小、队列深度、缓冲区)——无限制的资源会一直增长直到崩溃
- 重试逻辑必须在每次尝试之间检查上下文取消
- 在循环中使用
strings.Builder进行字符串拼接 → 参阅samber/cc-skills-golang@golang-code-style - string 与 []byte:对于可变操作和 I/O 使用
[]byte,对于显示和键使用string——转换会分配内存 - 迭代器(Go 1.23+):用于惰性求值——避免将所有内容加载到内存中
- 流式传输大型数据——加载百万行数据会导致 OOM;流式传输可以保持内存恒定
- 使用
//go:embed处理静态资源——在编译时嵌入,消除运行时文件 I/O 错误 - 使用
crypto/rand生成密钥/令牌——math/rand是可预测的 → 参阅samber/cc-skills-golang@golang-security - 正则表达式必须在包级别编译一次——编译是 O(n) 并且会分配内存
- 编译时接口检查:
var _ Interface = (*Type)(nil) - 少量重写代码优于大的依赖——每个依赖都会增加攻击面和维护负担
- 为可测试性而设计——接受接口,注入依赖
构造函数模式:函数选项 vs 构建器
函数选项(推荐)
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 演进更好地扩展,并且需要更少的代码。只有在配置步骤之间需要复杂验证时才使用构建器模式。
构造函数与初始化
避免 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:在不应出现的代码位置出现空指针、违反不变量、在初始化时使用
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+)和流式处理模式来处理大数据集,无需将所有内容加载到内存。对于服务间的大规模数据传输(例如,从 DB 到 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(),并通过 select 配合 ctx.Done() 使用指数/线性退避。长循环必须定期检查 ctx.Err()。→ 参阅 samber/cc-skills-golang@golang-context 技能。
数据库模式
→ 参阅 samber/cc-skills-golang@golang-database 技能了解 sqlx/pgx、事务、可空列、连接池、仓库接口、测试。
架构
询问开发者他们更喜欢哪种架构:整洁架构、六边形架构、领域驱动设计或扁平布局。不要对小项目强加复杂的架构。
无论哪种架构,核心原则:
- 保持领域纯净——领域层中没有框架依赖
- 快速失败——在边界处验证,信任内部代码
- 使非法状态不可表示——使用类型来强制不变量
- 遵循 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技能了解协程生命周期和优雅关闭 - → 参阅
samber/cc-skills-golang@golang-context技能了解超时和取消模式 - → 参阅
samber/cc-skills-golang@golang-project-layout技能了解架构和目录结构 - → 参阅
samber/cc-skills-golang@golang-refactoring技能了解如何在现有代码库中安全地逐步迁移到这些模式之一(选项结构体、DI、消费者端接口)