Go 100 常见错误 7: 返回接口

87 阅读3分钟

在设计函数签名时,我们可能需要返回接口或具体实现。让我们理解为什么在许多情况下,返回接口在 Go 中被认为是一种不良实践。

我们刚刚介绍了接口通常位于消费者端。Figure 2.7 展示了如果函数返回接口而不是结构体,依赖关系会发生什么。我们将看到这会导致问题。

我们将考虑两个包:

  • client,其中包含一个 Store 接口。
  • store,其中包含 Store 的实现。

WX20240515-164845@2x.png

在 store 包中,我们定义了一个 InMemoryStore 结构体,它实现了 Store 接口。同时,我们创建了一个 NewInMemoryStore 函数来返回一个 Store 接口。在这个设计中,实现包对客户端包有一个依赖,这听起来可能有点奇怪。

例如,client 包不能再调用 NewInMemoryStore 函数;否则,将会出现循环依赖。一个可能的解决方案是从另一个包调用这个函数,并将 Store 实现注入到 client 中。然而,被迫这样做意味着设计应该受到挑战。

此外,如果另一个客户端使用 InMemoryStore 结构体会发生什么?在这种情况下,我们可能希望将 Store 接口移动到另一个包中,或者回到实现包中,但我们讨论过,在大多数情况下,这不是最佳实践。这看起来像是一个代码异味。

因此,通常来说,返回接口限制了灵活性,因为我们强迫所有客户端使用一种特定的抽象类型。在大多数情况下,我们可以从 Postel 的法则(datatracker.ietf.org/doc/html/rf…)中获得灵感:

在你自己的行为上要保守,在你对他人的行为上要宽容。——传输控制协议

如果我们将这个格言应用到 Go 中,它的意思是:

  • 返回结构体而不是接口。
  • 如果可能的话,接受接口。

当然,有一些例外。作为软件工程师,我们知道规则永远不会 100% 的时候都是正确的。最相关的一个是错误类型,许多函数返回的接口。我们还可以在标准库中的 io 包中检查另一个例外:

func LimitReader(r Reader, n int64) Reader {
    return &LimitedReader{r, n}
}

这里,函数返回了一个导出的结构体,io.LimitedReader。然而,函数签名是一个接口,io.Reader。打破我们迄今为止讨论的规则的合理性是什么?io.Reader 是一个预先定义的抽象。这不是由客户端定义的,而是由语言设计者强制的,因为他们事先知道这种抽象水平将是有帮助的(例如,在可重用性和可组合性方面)。

总之,在大多数情况下,我们不应该返回接口,而是返回具体实现。否则,由于包依赖关系,它可能会使我们的设计更加复杂,并且可能会限制灵活性,因为所有客户端都将不得不依赖于相同的抽象。再次强调,结论与前面的部分类似:如果我们知道(而不是预见)抽象对客户端有帮助,我们可以考虑返回一个接口。否则,我们不应该强迫抽象;它们应该由客户端发现。如果客户端出于任何原因需要对实现进行抽象,它仍然可以在客户端进行。