学完这篇你能得到一套可直接套用的 Go 接口设计方法:知道接口该切多小、怎么用小接口拼出大能力,以及哪些场景干脆不该用接口。
第一步:先接受「接口不是越大越好」
Go 标准库给了最好的示范。io 包里最核心的两个接口各自只有一个方法:
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
任何类型只要有一个 Read 方法,就自动是 io.Reader——文件、网络连接、字符串、压缩流、加密流全部通吃。这就是小接口的威力:实现成本低,能被满足的类型就多,复用面自然就广。
注意:接口大小直接决定实现成本。一个 5 方法的接口意味着任何想「接入」的类型都得把 5 个方法全写一遍,哪怕只用得上 1 个;写单元测试时还得造 5 个桩方法。
第二步:从调用方出发定义接口
接口应该由使用方定义,而不是实现方。这是 Go 社区最容易被忽略的一条。
// 不好:实现方定义了大接口,调用方被迫依赖全部能力
type UserRepo interface {
Get(id int) (*User, error)
Save(u *User) error
Delete(id int) error
List(page int) ([]*User, error)
Count() (int, error)
}
// 好:调用方只声明自己真正用到的
type UserGetter interface {
Get(id int) (*User, error)
}
一个只读用户信息的函数,参数写成 UserGetter 就够了。传 *UserRepo 实现进去照样能用(隐式实现),但测试时你只需要 mock 一个方法。
注意:别把接口堆在实现包里。接口放在消费方所在包(或紧邻消费方的位置),实现包才不会绑死一堆「给下游用」的抽象。
第三步:用小接口组合出大接口
Go 的接口嵌入是「并集」语义,把几个小接口嵌进去,就得到能力更全的接口。io 包就是这么搭的:
type ReadWriter interface {
Reader
Writer
}
type ReadWriteCloser interface {
Reader
Writer
Closer
}
业务代码同理:
type Store interface {
UserGetter
UserSaver
Closer
}
需要读写就组合读写,需要加关闭就再嵌一个 Closer——按需叠加,而不是一开始就设计一个万能接口。
注意:组合出来的接口一旦超过 3~4 个方法,就该怀疑自己是不是在造「上帝接口」。接口嵌入是编译期静态展开的,不会带来额外调用开销,但会带来实现负担。
第四步:三种常用的组合模式
1. 装饰器(中间件)
http.Handler 只有一个方法,所以它能被无限套娃:
func WithLogging(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Println(r.Method, r.URL.Path)
next.ServeHTTP(w, r)
})
}
日志、鉴权、限流、恢复 panic,一层层包上去,各自只关心一件事。
2. 能力探测
http.ResponseWriter 本身只有 3 个方法,Flush、Hijack 被拆成独立的小接口,需要时再断言:
if f, ok := w.(http.Flusher); ok {
f.Flush()
}
这样不支持流式输出的实现不用被迫写一个空 Flush。把小能力从主接口里拆出来,正是小接口的直接收益。
3. 空实现占位
给接口写一个 noop 实现,嵌入到结构体里,只需要覆盖关心的那一个方法:
type noopWriter struct{}
func (noopWriter) Write(p []byte) (int, error) { return len(p), nil }
第五步:知道什么时候不该用接口
- 只有一个实现,且短期不会有第二个 → 直接用具体类型,
*UserService 就挺好。
- 变化点在数据而不是行为 → 用函数类型或 struct 传参,比抽接口轻。
- 抽接口只是为了 mock → 先想想能不能用一个基于 map + mutex 的内存实现,往往比 mock 更真实也更省事。
注意:Go 接口是隐式实现的,「写的时候不用声明」,也就意味着「改接口时不知道谁会编译失败」。动手改之前先用 go doc 或 IDE 的 Find Implementations 查一遍实现方,能省掉一轮返工。
小结
- 接口宁小勿大,1~2 个方法是官方基线
- 接口由调用方定义,紧贴使用处,别堆在实现包里
- 大接口靠嵌入小接口组合出来,按需叠加而非一次写全
- 装饰器、能力探测、空实现嵌入是小接口的三个典型用法
- 没有第二个实现、变化点在数据、纯粹为了 mock——这三种情况都不必抽接口