Go接口解耦靠“先写接口、再让类型自然满足”,需定义小接口、匹配接收者、安全断言、轻量mock,并仅在存在两个以上实现时才抽象接口。

Go 语言里接口解耦不是靠“设计模式堆砌”,而是靠「先写接口、再让类型自然满足」这一条铁律。只要方法签名对得上,哪怕结构体早于接口存在,也能立刻被当作该接口使用——这才是真正零侵入的解耦。
定义小接口时,只按当前调用方需要的方法来写
别一上来就定义 UserRepository,然后塞进 Get、Save、Delete、List 全套方法。如果当前只有 GetUserByID 这一个调用点,那就只定义:
type UserReader interface {
GetUserByID(id int) (*User, error)
}
这样做的好处:
- 实现方(比如内存 mock 或 MySQL)只需提供这一个方法,不用补一堆空实现
- 测试时可以只 mock 这个行为,不暴露无关能力
- 未来新增
SaveUser时,再单独定义UserWriter,避免“大接口爆炸”
值接收者和指针接收者必须跟接口方法签名严格匹配
这是最容易 panic 的地方:接口方法用指针接收者,但你传的是值类型,编译不报错,运行时却无法满足接口。
立即学习“go语言免费学习笔记(深入)”;
例如:
type Saver interface {
Save() error
}
func (u *User) Save() error { ... } // 指针接收者
那么下面这行会失败:
var u User var s Saver = u // ❌ 编译错误:*User 满足,User 不满足
常见应对方式:
- 统一用指针接收者(生产代码推荐),因为多数结构体要改内部字段
- 如果真要用值接收者,确保接口方法也按值来定义
- 别依赖
var _ Saver = (*User)(nil)来“检查”,它只在编译期起作用,掩盖了实际调用时的不匹配
用 interface{} 做通用容器时,断言前必须加 ok 判断
像 map[string]interface{} 解析 JSON 后取值,每层都得做类型断言。漏掉 ok 就是 runtime panic:
v := data["score"]
score := v.(int) // ❌ panic: interface conversion: interface {} is float64, not int
正确写法:
if score, ok := data["score"].(float64); ok {
// 注意:JSON 数字默认是 float64,不是 int
user.Score = int(score)
}
更稳妥的做法是尽早转成结构体:json.Unmarshal(b, &User{}),而不是长期抱着 interface{} 打转。
测试时不要手写 fake 实现,优先用接口 + 依赖注入
写单元测试时,别这样手动补全所有方法:
type FakeDB struct{}
func (f FakeDB) GetUserByID(id int) (*User, error) { return &User{}, nil }
func (f FakeDB) SaveUser(u *User) error { return nil }
func (f FakeDB) DeleteUser(id int) error { return nil }
// ……还剩 5 个方法没填
应该只实现测试用到的那个接口,比如:
type MockUserReader struct {
ReturnUser *User
}
func (m MockUserReader) GetUserByID(id int) (*User, error) {
return m.ReturnUser, nil
}
然后注入给被测对象:
svc := &UserService{
repo: MockUserReader{ReturnUser: &User{Name: "test"}},
}
重点在于:接口越小,mock 越轻;mock 越轻,测试越快、越稳定。
最常被忽略的一点:接口不是为“将来可能扩展”而提前定义的,而是为「此刻已有两个以上不同实现」才提炼出来的。没到那一步就抽象,只会让代码变重、理解成本变高。


















