如何在Go中使用界面



在主要工作的业余时间里,该材料的作者在Go上进行了咨询并解析了代码。 自然,在这样的活动过程中,他阅读了很多其他人编写的代码。 最近,本文的作者给人一种印象(是,印象是,没有统计数据),程序员更有可能使用“ Java风格”的接口。

这篇文章根据作者在编写代码方面的经验,提出了有关在Go中最佳使用接口的建议。

在这篇文章的示例中,我们将使用animalcircus这两个包。 这篇文章中的许多内容描述了如何在常规使用软件包时使用代码边界。

怎么办


我观察到一个非常普遍的现象:

 package animals type Animal interface { Speaks() string } //  Animal type Dog struct{} func (a Dog) Speaks() string { return "woof" } 

 package circus import "animals" func Perform(a animal.Animal) string { return a.Speaks() } 

这就是所谓的Java风格接口的使用。 它可以通过以下步骤来表征:

  1. 定义一个接口。
  2. 定义一种满足接口行为的类型。
  3. 定义满足接口实现的方法。

总而言之,我们正在处理“编写满足接口的类型”。 这段代码有其独特的气味 ,提示以下想法:

  • 只有一种类型满足该接口,而无意进一步扩展它。
  • 函数通常采用具体类型而不是接口类型。

怎么做


Go中的接口鼓励采用一种惰性方法,这很好。 您应该编写满足实际实际需求的接口,而不是编写满足接口的类型。

这意味着:不是在animals包中定义animals ,而是在使用时即circus *包中对其进行定义。

 package animals type Dog struct{} func (a Dog) Speaks() string { return "woof" } 

 package circus type Speaker interface { Speaks() string } func Perform(a Speaker) string { return a.Speaks() } 

一个更自然的方法是这样的:

  1. 定义类型
  2. 在使用时定义接口。

这种方法减少了对animals包装组件的依赖性。 减少依赖性是构建容错软件的正确方法。

贝德定律


编写好的软件有一个好的原则。 这是Postel法则,通常定律如下:
“对您所引用的内容保持保守,对您所接受的内容保持宽松”
就Go而言,法律是:

“接受接口,返回结构”

总而言之,这是设计容错,稳定的东西的一个很好的规则。 Go中的主要代码单元是一个函数。 设计功能和方法时,遵循以下模式很有用:

 func funcName(a INTERFACETYPE) CONCRETETYPE 

在这里,我们接受所有实现接口的东西,包括空的接口。 建议使用特定类型的值。 当然,限制a是有意义的。 正如一句谚语所说:

“空的界面什么也没说,” Rob Pike

因此,强烈建议防止函数接受interface{}

应用示例:模仿


测试案例是应用Postel法的好处的一个突出例子。 假设您有一个看起来像这样的函数:

 func Takes(db Database) error 

如果Database是接口,则在测试代码中,您可以简单地提供对Database实现的模仿,而无需传递真实的数据库对象。

如果可以预先定义接口


实话实说,编程是表达思想的一种非常免费的方法。 没有不可动摇的规则。 当然,您始终可以预先定义接口,而不必担心被代码警察逮捕。 在许多软件包的上下文中,如果您知道自己的功能并打算接受软件包中的特定接口,则可以这样做。

定义接口通常会有过度设计的痕迹,但是在某些情况下,您显然应该这样做。 尤其是以下示例:

  • 密封接口
  • 抽象数据类型
  • 递归接口

接下来,我们简要地考虑每个。

密封接口


密封接口只能在多个程序包的上下文中进行讨论。 密封接口是具有非导出方法的接口。 这意味着该程序包之外的用户无法创建满足此接口的类型。 这对于模拟变量类型以彻底搜索满足接口的类型很有用。

如果您定义了以下内容:

 type Fooer interface { Foo() sealed() } 

只有Fooer定义的程序包可以使用它并从中创建任何有价值的东西。 这使您可以为类型创建蛮力开关操作符。

密封的界面还允许分析工具轻松获取任何非冲突模式匹配项。 BurntSushi的sumtypes包旨在解决此问题。

抽象数据类型


预先定义接口的另一种情况涉及创建抽象数据类型。 它们可以密封也可以不密封。

sort包就是一个很好的例子,它是标准库的一部分。 它定义了一个可排序的集合,如下所示

 type Interface interface { // Len —    . Len() int // Less      //   i     j. Less(i, j int) bool // Swap     i  j. Swap(i, j int) } 

这段代码使很多人感到不安,因为如果您想使用sort包,则必须实现该接口的方法。 许多人不喜欢需要添加三行代码。

但是,我发现这是Go语言中非常优雅的泛型形式。 应该更经常地鼓励使用它。

替代的同时,优雅的设计方案将需要更高的订单类型。 在这篇文章中,我们将不考虑它们。

递归接口


这可能是带有库存代码的另一个示例,但是有时候无法避免使用它。 简单的操作即可让您得到类似

 type Fooer interface { Foo() Fooer } 

递归接口模式显然需要事先定义。 使用点接口定义建议在此处不适用。

该模式对于创建具有后续工作的上下文很有用。 上下文加载的代码通常将自身封装在包内,仅导出上下文(即张量包),因此,在实践中,我很少遇到这种情况。 我可以告诉您有关上下文模式的其他信息,但请留意其他内容。

结论


尽管该帖子的标题之一显示为“如何不做”,但我绝不试图禁止任何事情。 相反,我想让读者更多地考虑边界条件,因为在这种情况下会出现各种紧急情况。

我发现使用点声明原则非常有用。 由于其在实践中的应用,如果忽略它,我不会遇到任何问题。

但是,我有时也偶尔编写Java风格的接口。 通常,如果不久之前我用Java或Python编写了很多代码,就会发生这种情况。 过度复杂化和“以类的形式呈现一切”的愿望有时会非常强烈地体现出来,尤其是如果您在编写了许多面向对象的代码之后编写了Go代码。

因此,这篇文章也可以提醒自己,编写代码的路径看起来将不会引起头痛。 等待您的评论!

图片

Source: https://habr.com/ru/post/zh-CN411029/


All Articles