Go中的接口允许我们将不同的类型暂时视为同一数据类型。它们是 Go 程序员工具箱的核心,但新的 Go 开发者往往会使用不当......导致代码难以阅读,易于产生Bug。让我们来看看Golang接口的一些最佳实践。
我经常以标准库为例,来说明如何写出干净的Go接口。标准的错误接口很简单:
type error interface {
Error() string
}
error接口封装了任何有Error()方法的类型。该方法不接受任何参数,并返回一个字符串。例如,让我们定义一个表示网络问题的结构。
type networkProblem struct {
message string
code int
}
然后,我们定义一个Error()方法:
func (np networkProblem) Error() string {
return fmt.Sprintf("network error! message: %s, code: %v", np.message, np.code)
}
Now, we can use an instance of the networkProblem struct wherever an error is accepted.
现在,我们可以在任何接受到错误的地方使用networkProblem结构的实例:
func handleErr(err error) {
fmt.Println(err.Error())
}
np := networkProblem{
message: "we received a problem",
code: 404,
}
handleErr(np)
// prints "network error! message: we received a problem, code: 404"
坚持小接口
如果你只能从这篇文章中得到一条建议,那就是:让接口小一点! 接口的目的是为了定义准确表示一个想法或概念所必需的最小行为。
下面是标准HTTP包中一个更大的接口的例子,它是定义最小行为的好例子:
type File interface {
io.Closer
io.Reader
io.Seeker
Readdir(count int) ([]os.FileInfo, error)
Stat() (os.FileInfo, error)
}
任何满足接口行为的类型都可以被HTTP包当作一个文件来处理。这很方便,因为HTTP包不需要知道它处理的是磁盘上的文件、网络缓冲区,还是简单的[]字节。
接口应该没有满足类型的知识
一个接口不应该关心具体的类型。
下面,假设我们构建一个用于描述一辆汽车的组件的接口:
type car interface {
GetColor() string
GetSpeed() int
IsFiretruck() bool
}
GetColor()GetSpeed()IsFiretruck()IsPickup()IsSedan()IsTank()
相反,当给定一个car的接口实例时,开发应该根据类型断言的原生功能来推断出子类型。或者,如果子接口需要一个子接口,它可以定义为:
type firetruck interface {
car
HoseLength() int
}
firetruck接口继承了汽车的必要方法,并增加了一个额外的必要方法,使汽车成为消防车。
接口不是类
接口不是类,应该是小的。
接口不需要构造函数或析构函数,因为它没有必要进行数据的初始化和销毁。
接口在本质上不是分层的,尽管有语法糖来创建接口,而这些接口恰好是其他接口的超集。
接口只负责定义函数签名,不关心其具体实现。在结构方法中,定义一个接口可以减少重复代码。例如,如果5种类型实现了错误接口,它们就需要分别实现Error()函数。
翻译自:https://qvault.io/2020/03/15/best-practices-for-writing-clean-interfaces-in-go/
关注持续交付实践指南
往期文章推荐: