接口应由调用方定义,如订单服务声明userrepository接口;依赖须通过构造函数注入;dto与事件需隔离domain并带版本;go mod tidy不能替代人工依赖治理。

接口必须由调用方定义,而不是被调方暴露
很多团队把 UserRepository 接口放在用户服务的 pkg/user 里,结果订单服务一 import 就被迫依赖整个用户模块——这等于把“谁要什么”交给了提供方决定,违背了依赖反转原则。
正确做法是:订单服务自己在 order/service 中声明:type UserRepository interface { GetByID(ctx context.Context, id int64) (*User, error) }。用户服务只需实现它,不感知订单存在。
- 接口命名体现使用场景,比如
OrderNotifier比NotificationService更精准 - 禁止接口中包含未导出字段、
map[string]interface{}或json.RawMessage,否则契约失效 - 如果一个接口只被一个地方使用,且方法 ≤2 个,直接用函数类型更轻量:
type EmailSender func(ctx context.Context, to, subject, body string) error
构造函数显式接收接口,拒绝包内 new 实例
常见错误是在 service/user.go 里写 repo := redisrepo.NewClient() 或 db := sql.Open(...)——这导致测试时无法替换,部署时无法切换存储后端。
所有依赖必须通过构造函数参数传入:func NewUserService(repo UserRepository, sender EmailSender) *UserService。初始化逻辑收口到 cmd/order-service/main.go 或 internal/bootstrap/ 中。
- 不要用全局变量或
init()初始化依赖,那会让依赖链不可见、不可测 - 避免在 service 层 import 具体 infra 包(如
github.com/xxx/redisrepo),只允许 infra 包 import service 接口 - 若需多套实现(如本地缓存 + Redis),让它们都实现同一接口,启动时按配置选择
DTO 和事件 payload 必须与 domain 完全隔离
把数据库 model User 直接当 HTTP 响应返回,或塞进 Kafka 消息体,是强耦合的高发区。上游加个字段、改个 tag,下游就 panic 或静默丢数据。
每个 API 接口定义专属 DTO:UserResponseV1,字段名用下划线(user_id),所有字段导出且可序列化。事件 payload 同理,必须带 version 字段,且只含基本类型、map、数组、纯 struct。
- 禁止在 DTO 中嵌套指针、函数、channel、未导出字段或含方法的类型
- DTO ↔ domain 转换用
mapstructure.Decode或手写映射函数,别用json.Unmarshal直接进 model - 事件中传“事实”而非“意图”,比如
"profile_updated_at": "2026-07-01T02:23:00Z",而不是"action": "update_profile"
go mod tidy 不能替代依赖治理意识
go mod tidy 只清理显式未 import 的模块,对 //go:build 条件编译、_ "net/http/pprof" 这类隐式依赖完全无感。更麻烦的是,go.sum 里的孤儿哈希不会自动清除,间接依赖版本也可能失控。
真正管住依赖,得靠人盯:先 go build -tags dev ./ 覆盖所有构建变体,再 go mod tidy;关键基础库(如 golang.org/x/net)即使没直接 import,也要显式 require 并锁小版本。
- 子 module 是利器:把 CLI 工具、migration 脚本拆成独立
go.mod,避免污染主服务依赖树 - 警惕
replace:局部替换某个模块可能改变其下游依赖解析路径,悄悄引入新// indirect - 高频间接依赖(如 protobuf、grpc)建议统一升级策略,避免不同服务用不同小版本
最难的不是写接口或拆 module,而是判断哪一层该抽象、哪一层该稳定——业务逻辑变化快的地方值得接口化,基础设施细节变化慢的地方可以接受具体类型。过度抽象和欠抽象一样危险。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











