go解耦靠接口定义位置、依赖注入时机和结构体嵌入方式三件事:command接口应放application/domain目录,避免service层import具体包;禁用interface{}牺牲类型安全;推荐withxxxoption而非工厂函数。

Go 里不需要“套用设计模式”,解耦靠的是接口定义位置、依赖注入时机和结构体嵌入方式这三件事。
Command 接口为什么不能放在 handler 或 repository 目录下
把 Command 接口定义在 handler/ 或 repository/ 里,会导致 service 层必须 import 具体包,等于把执行细节暴露给了调用方。真正该放的位置是 application/ 或 domain/ —— 这里只描述“能做什么”,不关心“怎么做”。
- 错误做法:在
repository/cmd.go里写type Command interface { Execute() error },然后 service import 这个包 - 正确做法:在
application/command.go定义Command,所有业务命令(CreateUserCmd、SendEmailCmd)都实现它,且只依赖*sql.DB或UserRepo这类小接口 - 关键点:handler 只接收
Command类型参数,完全不知道CreateUserCmd的存在;构造命令实例的动作发生在启动时,通过构造函数注入
interface{} 不是解耦,而是放弃类型安全
用 interface{} 做事件总线或命令参数,看似灵活,实则让编译器无法校验契约,测试时容易漏掉字段、拼错方法名,上线后才暴露 panic。
- 错误现象:
bus.Publish(event interface{})→ 订阅者收到map[string]interface{},手动取字段,字段名错一个就 panic - 替代方案:定义具体事件类型,如
type UserCreated struct { ID int64; Email string },再统一实现Topic() string和Payload() interface{} - 好处:IDE 能跳转、go vet 能检查、单元测试可断言具体字段;坏处是多写几行 struct,但换来的是可维护性
WithXXXOption 比 NewXXXFactory 更适合 Go 的依赖注入
工厂函数不是不行,但 Go 里更自然的方式是把依赖作为函数选项传进去,而不是封装成对象再调用方法。前者零分配、无抽象、组合自由;后者容易变成“为了抽象而抽象”。
- 错误做法:
factory := NewUserFactory(db, cache)→factory.Create(),结果 factory 里又 new 了 service、repo,依赖链藏得深 - 正确做法:
cmd := NewCreateUserCmd(WithDB(db), WithEmailer(emailer)),每个WithXXX是个函数,直接赋值字段 - 注意点:option 函数内部不做校验,校验逻辑留在
NewCreateUserCmd主函数里——因为只有主函数知道哪些字段是必填的
最容易被忽略的是:解耦不是靠多建一层目录,而是看 handler 是否还 import 了 database/sql、service 是否还调用了 http.Post、domain 结构体是否还嵌套了 *redis.Client。只要这些还在,再多的设计模式也白搭。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











