golang 编译期检查接口实现
Go 中如何在编译期检查接口实现
Go 的接口是隐式实现的:一个类型只要拥有接口要求的全部方法,就自动实现了该接口。
这种设计很灵活,但也带来一个问题:代码本身没有明确声明“这个类型就是为了实现这个接口”。
场景
假设定义一个存储接口:
type Store interface {
Save(key string, value []byte) error
Delete(key string) error
}
然后有一个文件存储实现:
type FileStore struct{}
func (s *FileStore) Save(key string, value []byte) error {
return nil
}
func (s *FileStore) Delete(key string) error {
return nil
}
从方法上看,*FileStore 已经实现了 Store。
但这种关系只是隐式存在的。
如果以后 Store 新增了一个方法:
type Store interface {
Save(key string, value []byte) error
Delete(key string) error
Get(key string) ([]byte, error)
}
而 FileStore 忘记同步实现 Get,那么只有当代码真正尝试把 *FileStore 当作 Store 使用时,编译器才会发现问题。
更好的做法,是直接在实现附近声明:
var _ Store = (*FileStore)(nil)
这样可以明确表达:
*FileStore的设计目标就是实现Store,如果不满足这个接口,应该直接编译失败。
实现方式
常见写法是:
var _ Store = (*FileStore)(nil)
可以拆成三部分理解:
Store:目标接口类型。(*FileStore)(nil):一个类型为*FileStore的 nil 值。_:空白标识符,不保存这个值。
本质上是在做一次赋值检查:
var store Store = (*FileStore)(nil)
只不过我们并不需要 store 这个变量,所以用 _ 丢弃。
原理
Go 规定:
一个具体类型的值只有在实现了接口的全部方法时,才能赋值给该接口类型。
因此:
var _ Store = (*FileStore)(nil)
会迫使编译器检查:*FileStore 是否实现了 Store?
如果缺少任何方法,代码就无法通过编译。
例如删除 Delete 方法后,编译器会直接报错:
*FileStore does not implement Store
(missing method Delete)
这里使用 nil 的原因也很简单:我们只关心类型是否满足接口,并不需要真的创建一个 FileStore 对象。
注意
var _ Store = (*FileStore)(nil)在检查FileStore的指针类型是否实现了 Store 接口。
如果想检查值类型 FileStore 是否实现接口,可以写var _ Store = FileStore{}
总结
var _ Interface = (*Implementation)(nil)
是一种编译期接口实现检查。
它把原本隐式的:Implementation 应该实现 Interface 约定,变成一个由编译器持续保证的约束。
它的价值不在于运行时逻辑(无运行时开销),而在于:当接口发生变化时,让不完整的实现尽可能早地暴露出来。
评论