go项目解耦核心是守住变更边界:用接口定义行为契约、避免实现强依赖、合并高耦合资源到同一包、显式依赖注入、谨慎提取公共能力。

Go 项目中模块强依赖的典型症状不是运行时报错,而是编译失败、测试难写、改一个地方要动三个包——根本原因往往不是代码写得差,而是包结构和接口契约没对齐。
用接口定义行为契约,而不是用结构体暴露实现
强依赖最常见于业务层直接 import 数据库包、HTTP 客户端包或第三方 SDK 包。比如 UserService 里直接 new mysql.DB 或调用 http.DefaultClient.Do(),这会让整个服务无法脱离 MySQL 或网络环境做单元测试。
- 把“能做什么”抽成接口,放在调用方所在包或上层
interfaces/ports包里(不放底层包) - 接口方法名聚焦行为,如
SaveUser、SendNotification,避免带实现痕迹的命名(如InsertIntoMySQL) - 接口粒度宁小勿大:一个接口只对应一类协作场景,别堆砌十几个方法
- 生产实现和 mock 实现都放在各自包里,但都实现同一接口;测试时注入 mock,生产时注入真实 client
合并高耦合资源到同一包,分文件组织
当 user 和 group、order 和 payment 出现双向引用(比如 User.Groups []Group + Group.Members []User),硬拆成两个包必然触发 import cycle not allowed 编译错误。
- 放弃按资源名建包的惯性,统一放进
models或domain包 - 每个结构体单独一个文件:
user.go、group.go、user_group_relation.go - 关联逻辑(如
AddMember、GetUsersInGroup)写在独立文件或 receiver 方法里,不跨包调用 - 对外暴露的只有结构体和方法,不暴露包路径细节;上层 service 层只 import 这一个包
依赖注入必须显式传参,拒绝全局变量或 init 注入
靠 init() 函数初始化 DB 或通过包级变量存 client,表面省事,实则让依赖关系不可见、不可替换、不可测试。
- 所有外部依赖(DB、cache、mailer、第三方 API client)都通过构造函数参数传入,类型是接口
- 避免在 struct 字段里用具体类型(如
db *sql.DB),而用接口(如repo UserRepository) - 手动 DI 足够清晰;项目大了再引入
wire,但不要为用而用——wire只是生成构造链的工具,不能替代接口设计 - Gin / Echo 等框架的 handler 函数里,别直接 new service,从容器或闭包里取已注入好依赖的实例
公共能力提前提取,但别过早抽象
看到两个包都用 time.Now().UTC().Format(...) 就建个 timeutil 包?没必要。只有当逻辑真正复用、且有明确契约时才提取。
- 先共用一个
types包放共享结构体、常量、错误码(如ErrNotFound),它不依赖任何业务包 - 接口定义可放在
interfaces包,但该包只 export 接口,不 import 任何业务实现 - 警惕“中间包陷阱”:为解耦而建
shared,结果里面塞满未收敛的工具函数,反而变成新的依赖黑洞 - 如果只是临时需要某个类型,考虑用内嵌或 type alias,而非新建包——Go 的包粒度本就不该太细
最易被忽略的一点:解耦不是目标,而是手段;真正要守住的是变更边界——改数据库驱动,不该动 HTTP handler;换通知渠道,不该重写订单状态机。接口和包结构只是帮你看清那条线在哪,而不是越多越好。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











