可复用的Go代码需遵循接口窄、实现松原则:接口仅含1–3个必要方法,命名体现职责,配置通过构造函数传入,错误分层处理并避免过早抽象。

接口定义要窄,实现要松
Go 里复用的核心不是继承,而是组合 + 接口。如果 interface{} 太宽(比如塞了 10 个方法),调用方很难只用其中 2 个,又得实现全部——这直接劝退复用。真正可复用的接口往往只有 1–3 个方法,比如 io.Reader 就只有 Read([]byte) (int, error) 一个方法。
- 定义接口时,从使用者视角出发:“我只需要它能做这件事”,而不是“它理论上能做什么”
- 把多个小接口组合成大行为,比如
io.ReadWriter就是io.Reader和io.Writer的嵌入,而非重新定义 - 避免在包内提前定义“未来可能需要”的接口;等真有两处以上代码产生相同依赖时,再抽象出来
导出标识符命名要体现职责边界
Go 包里导出的类型、函数、变量,名字本身就在传递复用意图。比如叫 NewHTTPClient() 暗示它返回的是通用 HTTP 客户端;而叫 NewPaymentClient() 就锁死了场景,别人不敢拿去复用。
- 优先用动词+名词结构:如
ParseJSON()、RetryWithBackoff(),比DoSomething()明确得多 - 避免带业务词的导出名(如
SendOrderEmail()),换成更通用的SendEmail(),让调用方自己组装参数 - 包名本身也要克制:用
retry而非paymentretry,否则其他模块想用还得复制一份
配置通过结构体传入,不依赖全局变量或 init()
一旦包初始化时读环境变量、硬编码 endpoint、或者在 init() 里注册 handler,这个包就很难被不同项目安全复用——你改配置就得改源码,或者引入副作用。
- 所有可变参数走构造函数参数,比如
NewService(opts ...ServiceOption),用函数式选项模式 - 拒绝
SetGlobalTimeout(30 * time.Second)这类全局状态控制,它会让并发测试和多实例共存变得脆弱 - 如果必须读配置,让调用方传进来,而不是包自己去
os.Getenv("API_URL")
type ServiceOption func(*service)func WithTimeout(d time.Duration) ServiceOption { return func(s *service) { s.timeout = d } }
func NewService(opts ...ServiceOption) *service { s := &service{} for _, opt := range opts { opt(s) } return s }
错误处理要分层,别把底层错误直接透出
如果包里所有错误都直接返回 fmt.Errorf("failed to dial: %w", err),上层无法区分是网络问题、认证失败还是参数错误,也就没法针对性重试或降级——复用时只能全盘兜底,成本飙升。
立即学习“go语言免费学习笔记(深入)”;
- 定义明确的错误类型(如
ErrNotFound、ErrTimeout),用errors.Is()判断 - 对第三方库错误做转换,比如把
redis.Nil映射为ErrNotFound,隐藏实现细节 - 避免在错误信息里拼接敏感字段(如密码、token),否则日志一打就泄露
真正难的不是写可复用的代码,是忍住不在第一个项目里就把“看起来以后会用到”的逻辑提前封装。多数复用发生在第二、第三个相似需求出现时,过早抽象只会增加维护负担。










