Go中的接口允许我们将不同的类型暂时视为同一数据类型。它们是 Go 程序员工具箱的核心,但新的 Go 开发者往往会使用不当......导致代码难以阅读,易于产生Bug。让我们来看看Golang接口的一些最佳实践。

我经常以标准库为例,来说明如何写出干净的Go接口。标准的错误接口很简单:

  1. type error interface {

  2. Error() string

  3. }

error接口封装了任何有Error()方法的类型。该方法不接受任何参数,并返回一个字符串。例如,让我们定义一个表示网络问题的结构。

  1. type networkProblem struct {

  2. message string

  3. code int

  4. }

然后,我们定义一个Error()方法:

  1. func (np networkProblem) Error() string {

  2. return fmt.Sprintf("network error! message: %s, code: %v", np.message, np.code)

  3. }

Now, we can use an instance of the networkProblem struct wherever an error is accepted.

现在,我们可以在任何接受到错误的地方使用networkProblem结构的实例:

  1. func handleErr(err error) {

  2. fmt.Println(err.Error())

  3. }


  4. np := networkProblem{

  5. message: "we received a problem",

  6. code: 404,

  7. }


  8. handleErr(np)


  9. // prints "network error! message: we received a problem, code: 404"

坚持小接口

如果你只能从这篇文章中得到一条建议,那就是:让接口小一点! 接口的目的是为了定义准确表示一个想法或概念所必需的最小行为。

下面是标准HTTP包中一个更大的接口的例子,它是定义最小行为的好例子:

  1. type File interface {

  2. io.Closer

  3. io.Reader

  4. io.Seeker

  5. Readdir(count int) ([]os.FileInfo, error)

  6. Stat() (os.FileInfo, error)

  7. }

任何满足接口行为的类型都可以被HTTP包当作一个文件来处理。这很方便,因为HTTP包不需要知道它处理的是磁盘上的文件、网络缓冲区,还是简单的[]字节。

接口应该没有满足类型的知识

一个接口不应该关心具体的类型。

下面,假设我们构建一个用于描述一辆汽车的组件的接口:

  1. type car interface {

  2. GetColor() string

  3. GetSpeed() int

  4. IsFiretruck() bool

  5. }

GetColor()GetSpeed()IsFiretruck()IsPickup()IsSedan()IsTank()

相反,当给定一个car的接口实例时,开发应该根据类型断言的原生功能来推断出子类型。或者,如果子接口需要一个子接口,它可以定义为:

  1. type firetruck interface {

  2. car

  3. HoseLength() int

  4. }

firetruck接口继承了汽车的必要方法,并增加了一个额外的必要方法,使汽车成为消防车。

接口不是类

接口不是类,应该是小的。

接口不需要构造函数或析构函数,因为它没有必要进行数据的初始化和销毁。

接口在本质上不是分层的,尽管有语法糖来创建接口,而这些接口恰好是其他接口的超集。

接口只负责定义函数签名,不关心其具体实现。在结构方法中,定义一个接口可以减少重复代码。例如,如果5种类型实现了错误接口,它们就需要分别实现Error()函数。


翻译自:https://qvault.io/2020/03/15/best-practices-for-writing-clean-interfaces-in-go/


关注持续交付实践指南


往期文章推荐: