尽量不要返回接口
在设计函数签名时,函数的返回值要么是一个接口,要么是一个具体类型。本文将分析为什么在很多情况下返回接口在Go语言中不是一种好的做法。在Go语言中常见100问题-#6 生产者端接口中讨论了接口通常定义在消费者端。通过下图,来看看如果一个函数返回的是一个接口而不是具体类型,依赖关系会发生什么变化,这会导致什么问题。下图中包含store和client两个包,client包中定义了一个Store接口,store包中 InMemoryStore 结构体实现了Store接口。
在store包中定义了一个实现Store接口的InMemoryStore结构体,同时创建一个 NewInMemoryStore 函数,该函数的返回值为一个Store接口。这样设计编码会导致store包和client包之间存在依赖关系。例如,客户端client不能再调用 NewInMemoryStore 函数,否则将导致循环依赖。解决这种循环依赖的一种可能方法是从另外一个包中调用此函数并将Store实现注入到客户端中。然而,被迫这样做意味编码设计应该受到讨论和质疑。同样,如果有另一个客户端需要使用 InMemoryStore 结构体,怎么办呢?将Store接口移动到另一个包中?还是将其定义到store包中?处理起来都不优雅,像是一种代码坏味道。说了这么多,是想表达为什么在大多数情况下它不是最佳实践。
因此,通常来说,返回一个接口会限制灵活性,因为这会强制所有客户端使用一种特定类型的抽象。在大多数情况下,我们可以遵从伯斯塔尔法则(Postel law).
对内保存保守,对外保持自由
如果我们把这个法则应用到Go语言编程中,想表达的意思是:
-
返回结构体而不是接口
-
尽可能接收接口
当然,也有一些例外情况。作为软件工程师,我们熟知没有一成不变的规则,即在100%的情况下,规则永远不会是真的。最好的例子证明是错误类型,很多函数都会返回一个接口类型的错误(error). 我们还可以使用io包检查标准库中的另外一个异常,像下面的函数返回一个可导出的结构体:io.LimitedReader,但是函数的签名是一个接口:io.Reader, 这不是不符合我们前面的讨论分析吗,为什么要这样实现呢?因为io.Reader是一个提前确定的抽象,它不是由客户端定义的,而是强制存在的,这其实就是前一篇文章中讲的生产者端接口,语言设计者事先知道这种抽象级别在可重用性和可组合性方面有帮助。
golang
func LimitReader(r Reader, n int64) Reader {
return &LimitedReader{r, n}
}
总而言之,在大多数情况下,我们不应该返回接口,而是返回具体的实现。否则,由于存在包依赖性和灵活性受到限制,会使我们的设计编码更加复杂。因为所有的客户端都必须依赖相同的抽象。如果我们知道(不是预想)抽象对客户端有所帮助,可以考虑返回一个接口。否则,我们不应该强制抽象,应该交给客户端发现。如果客户端处于某种原因需要抽象实现,它可以将抽象定义在自己包中,这样具有很强的灵活性。