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 约定,变成一个由编译器持续保证的约束。

它的价值不在于运行时逻辑(无运行时开销),而在于:当接口发生变化时,让不完整的实现尽可能早地暴露出来。

评论