Go微服务中服务间应通过接口注入而非直接new具体实现,以避免硬编码依赖导致测试困难、数据库切换成本高及领域层污染;UserService应依赖UserRepo接口,注入在main层完成,mock测试需满足接口且编译期校验。

Go 微服务里,服务之间不该直接 new 具体实现,而要通过接口注入——否则一改数据库或缓存,就得全量改 service 层代码,测试也得连真实中间件跑。
为什么 UserServiceImpl 不能直接 new MySQLUserRepo
常见错误是让业务 service 直接构造底层依赖:
type UserService struct {
repo *MySQLUserRepo // ❌ 硬编码具体类型
}
func NewUserService() *UserService {
return &UserService{
repo: NewMySQLUserRepo(), // ❌ 启动时就绑死 MySQL
}
}
这会导致三个实际问题:
- 单元测试必须启动 MySQL 容器,
go test变慢且不稳定 - 想换成 PostgreSQL?得改所有 new
MySQLUserRepo的地方,包括 test 文件 - 领域层(domain)被基础设施细节污染,比如
User结构体里出现gorm.Model字段
UserService 应该依赖什么接口
接口定义要放在 internal/user/domain 或 pkg/user 这类高层包里,而不是跟实现混在 internal/user/infrastructure:
立即学习“go语言免费学习笔记(深入)”;
package domain
type UserRepo interface {
Save(u *User) error
FindByID(id int) (*User, error)
}
type UserService struct {
repo UserRepo // ✅ 依赖抽象,不关心谁实现
}
注意两点:
- 接口名用
UserRepo而不是IUserRepo,Go 不需要前缀 - 接口方法参数和返回值尽量用 domain 类型(如
*User),避免泄漏sql.NullString等基础设施类型 - 如果某 service 同时需要读写,不要强行塞进一个大接口,按需拆成
UserReader和UserWriter
如何把 PostgresUserRepo 注入到 UserService
Go 没有官方 DI 容器,但可以用构造函数参数传入,或借助轻量库(如 uber-go/fx):
// cmd/user-service/main.go
func main() {
db := connectPostgres()
repo := NewPostgresUserRepo(db) // 具体实现
svc := NewUserService(repo) // 接口注入
http.ListenAndServe(":8080", handler(svc))
}
关键点:
- 注入时机在
main()或cmd/层,**绝不**在 service 内部做 new - 如果多个 service 共享同一 repo 实例,确保它线程安全(比如
*sql.DB本身是 safe 的) - 避免在
internal/user/application层 importinfrastructure包,否则倒置失效
测试时怎么替换为 MockUserRepo
mock 实现只需满足接口,不用继承或声明 implements:
type MockUserRepo struct {
users map[int]*User
}
func (m *MockUserRepo) Save(u *User) error {
m.users[u.ID] = u
return nil
}
func (m *MockUserRepo) FindByID(id int) (*User, error) {
u, ok := m.users[id]
if !ok {
return nil, errors.New("not found")
}
return u, nil
}
// 测试直接 new mock 并传入
func TestUserService_Create(t *testing.T) {
mockRepo := &MockUserRepo{users: make(map[int]*User)}
svc := NewUserService(mockRepo)
err := svc.Create(&User{ID: 123})
assert.NoError(t, err)
}
容易踩的坑:
- mock 实现漏了某个方法,编译不报错,运行时 panic —— 建议加
var _ domain.UserRepo = (*MockUserRepo)(nil)编译期校验 - mock 状态没重置,导致测试间相互污染 —— 每个 test case 新建 mock 实例
- 在 mock 里调用真实 DB 或 HTTP,那就不是 mock 了
接口倒置不是为了写更多代码,而是把「换实现」这件事从编译期错误变成运行时配置——比如今天用 MySQL,明天切 TiDB,只要新 repo 实现了 UserRepo,service 层一行都不用动。


















