Go语言应避免硬套传统设计模式,优先用小而精的interface隔离实现、依赖倒置、显式依赖注入,并以组合替代滥用嵌入,核心是控制抽象粒度与依赖流向。

Go 语言本身没有类、继承和泛型(旧版本),也不鼓励过度抽象,所以直接套用传统设计模式容易水土不服。真正提升可维护性的,不是“用了多少种模式”,而是用对了 interface、控制好依赖流向、把变与不变清晰分离。
用 interface 隔离实现,而不是为模式而抽象
很多人一上来就定义 UserService、UserRepository、UserFactory,但 Go 里更自然的做法是:先写具体逻辑,等出现两个以上不同实现(比如内存版 vs PostgreSQL 版 UserStore),再抽 interface。
关键点:
-
interface应由调用方(如 handler 或 service)定义,而不是被调用方(如 db 包)提前声明 —— 这叫「依赖倒置」,避免包间强耦合 - 接口尽量小:比如
type Storer interface { Get(id string) (User, error) },比大而全的UserRepository更易测试和替换 - 不要导出接口名带
Impl或Concrete—— 实现类型本就不该被外部 import 依赖
避免在 Go 里硬套单例、工厂、抽象工厂
Go 的包级变量 + sync.Once 足以处理绝大多数“全局唯一资源”场景(如 DB 连接池、配置实例)。硬套单例模式反而会让测试变难 —— 因为无法在单元测试中注入 mock 实例。
立即学习“go语言免费学习笔记(深入)”;
常见误用:
- 写一个
NewUserFactory()返回接口,但实际只有一种实现,且生命周期和配置完全固定 → 直接传 struct 指针更清晰 - 用工厂封装
http.Client创建逻辑,却忽略http.DefaultClient本身已可配置 → 多余抽象 - 为每个结构体配一个 builder,但字段少、构造逻辑简单 →
func NewUser(name string, email string) User更直白
依赖注入要轻量,别用第三方 DI 框架
Go 生态里最稳妥的依赖注入方式,就是主函数(main())里显式 new 所有顶层依赖,再逐层传入。例如:
func main() {
db := setupDB()
cache := setupRedis()
svc := &UserService{DB: db, Cache: cache}
handler := &UserHandler{Service: svc}
http.ListenAndServe(":8080", handler)
}
这样做的好处:
- 启动流程一目了然,所有依赖在哪初始化、怎么串联,不用查 DI 容器注册表
- 测试时可直接 new 各层对象并传入 mock,不依赖反射或 tag 解析
- 避免像
dig或fx这类框架带来的隐式生命周期管理(比如忘记Close()导致资源泄漏)
组合优于继承,但别滥用嵌入(embedding)
Go 用结构体嵌入模拟“继承”,但嵌入本质是字段复用,不是类型关系。滥用会导致方法集爆炸、语义模糊。
判断是否该嵌入:
- 嵌入的类型是否真的代表“是一个”(is-a)?比如
type AdminUser struct { User }是错的 —— AdminUser 不是 User,它只是有 User 字段;应改为type AdminUser struct { user User } - 嵌入后是否暴露了不该暴露的方法?比如嵌入
sync.Mutex后,外部能直接调mu.Lock(),破坏封装 —— 此时应封装成私有字段 + 显式方法 - 嵌入多个同名方法的接口?Go 会报错,这时必须显式重命名或拆解行为
可维护性的最大敌人,从来不是“没用设计模式”,而是过早抽象、接口膨胀、依赖隐藏。Go 项目里最值得花时间的地方,其实是把错误处理路径理清楚、把配置加载时机定死、把跨层边界(比如 domain / infra)用 interface 划明白 —— 这些事比记住 23 种模式管用得多。










