Go的面向对象模型围绕接口。 我个人认为接口是最重要的语言构造,所有设计决策都应首先关注接口。
在本教程中,您将学习接口是什么,Go对接口的理解,如何在Go中实现接口以及最后接口与合同的局限性。
Go接口是一种类型,它由方法签名的集合组成。 这是Go接口的示例:
<pre class="has"><code class="notranslate brush: plain noskimlinks noskimwords">type Serializable interface { Serialize() (string, error) Deserialize(s string) error }</code></pre><code class="inline">Serializable</code>接口有两种方法。 <code class="inline">Serialize()</code>方法不接受任何参数,并返回一个字符串和一个错误,而<code class="inline">Deserialize()</code>方法接受一个字符串,并返回一个错误。 如果您遍历该块,那么其他语言可能会让您熟悉<code class="inline">Serializable</code>接口,并且您可以猜测<code class="inline">Serialize()</code>方法返回目标对象的序列化版本,可以通过调用<code class="inline">Deserialize()</code>并传递来重建该对象最初调用<code class="inline">Serialize()</code> 。
请注意,您无需在每个方法声明的开头提供“ func”关键字。 Go已经知道接口只能包含方法,并且不需要任何帮助,只要您说它是“函数”即可。
<h2>Go界面最佳做法</h2>Go接口是构建程序主干的最佳方法。 对象应该通过接口而不是通过具体对象相互交互。 这意味着您应该为程序构造一个仅由接口和基本类型或数据对象(其成员为基本类型或其他数据对象的结构)组成的对象模型。 以下是您应该使用界面进行的一些最佳实践。
<h3>明确意图</h3>重要的是,对于调用者和实现者而言,每种方法和调用顺序背后的意图必须清晰且定义明确。 Go对此没有语言级别的支持。 稍后,我将在“接口与合同”部分中对其进行更多讨论。
<h3>依赖注入</h3>依赖注入意味着通过接口与另一个对象交互的对象将从外部获得该接口作为函数或方法参数,并且不会创建该对象(或调用返回具体对象的函数)。 请注意,该原理也适用于独立功能,而不仅适用于对象。 函数应将其所有依赖项作为接口接收。 例如:
<pre class="has"><code class="notranslate brush: plain noskimlinks noskimwords">type SomeInterface { DoSomethingAesome() } func foo(s SomeInterface) { s.DoSomethingAwesome() }</code></pre>现在,您使用<code class="inline">SomeInterface</code>不同实现调用函数<code class="inline">foo()</code> ,它将对所有这些实现<code class="inline">SomeInterface</code> 。
<h3>工厂名称</h3>显然,必须有人来创建具体对象。 这是专用工厂对象的工作。 工厂在两种情况下使用:
<ol><li> 在程序开始时,工厂用于创建所有长期运行的对象,这些对象的生存期通常与程序的生存期匹配。 </li><li> 在程序运行时,各种对象经常需要动态实例化对象。 工厂也应用于此目的。 </li></ol>为对象提供动态工厂接口以维持仅接口的交互模式通常很有用。 在下面的示例中,我定义了一个<code class="inline">Widget</code>接口和一个<code class="inline">WidgetFactory</code>接口,该接口从其<code class="inline">CreateWidget()</code>方法返回一个<code class="inline">Widget</code>接口。
<code class="inline">PerformMainLogic()</code>函数从其调用者接收<code class="inline">WidgetFactory</code>接口。 现在,它可以根据其窗口小部件规格动态创建一个新窗口小部件,并调用其<code class="inline">Widgetize()</code>方法,而无需了解其具体类型(结构实现接口的方式)。
<pre class="has"><code class="notranslate brush: plain noskimlinks noskimwords">type Widget interface { Widgetize() } type WidgetFactory interface { CreateWidget(widgetSpec string) (Widget, error) } func PerformMainLogic(factory WidgetFactory) { ... widgetSpec := GetWidgetSpec() widget := factroy.CreateWidget(widgetSpec) widget.Widgetize() }</code></pre> <h3>可测性</h3>可测试性是正确进行软件开发的最重要实践之一。 Go接口是支持Go程序中可测试性的最佳机制。 要彻底测试功能或方法,您需要控制和/或测量被测功能的所有输入,输出和副作用。
对于直接与文件系统,系统时钟,数据库,远程服务和用户界面直接通信的非平凡代码,这很难实现。 但是,如果所有交互都通过接口进行,则模拟和管理外部依赖关系非常容易。
考虑一个仅在月底运行的功能,并运行一些代码来清理不良交易。 如果没有接口,则必须采取极端措施,例如更改实际计算机时钟以模拟月末。 使用提供当前时间的界面,您只需传递一个将所需时间设置为的结构。
无需导入<code class="inline">time</code>并直接调用<code class="inline">time.Now()</code> ,您可以传递一个带有<code class="inline">Now()</code>方法的接口,该接口在生产中将通过转发到<code class="inline">time.Now()</code>来实现,但在测试过程中将由一个返回的对象实现固定时间冻结测试环境。
<h2>使用Go界面</h2>使用Go界面非常简单。 您只需像调用其他任何函数一样调用其方法即可。 最大的区别是您不确定会发生什么,因为可能会有不同的实现。
<h2>实施Go接口</h2>Go接口可以作为结构上的方法实现。 考虑以下接口:
<pre class="has"><code class="notranslate brush: plain noskimlinks noskimwords">type Shape interface { GetPerimeter() int GetArea() int }</code></pre>以下是Shape接口的两个具体实现:
<pre class="has"><code class="notranslate brush: plain noskimlinks noskimwords">type Square struct { side uint } func (s *Square) GetPerimeter() uint { return s.side * 4 } func (s *Square) GetArea() uint { return s.side * s.side } type Rectangle struct { width uint height uint } func (r *Rectangle) GetPerimeter() uint { return (r.width r.height) * 2 } func (r *Rectangle) GetArea() uint { return r.width * r.height }</code></pre>正方形和矩形根据其字段和几何属性不同地执行计算。 下一个代码示例演示如何用实现该接口的具体对象填充Shape接口的一个切片,然后遍历该切片并调用每个形状的<code class="inline">GetArea()</code>方法以计算所有形状的总面积。
<pre class="has"><code class="notranslate brush: plain noskimlinks noskimwords">func main() { shapes := []Shape{&Square{side: 2}, &Rectangle{width: 3, height: 5}} var totalArea uint for _, shape := range shapes { totalArea = shape.GetArea() } fmt.Println("Total area: ", totalArea) }</code></pre> <h3>基本实施</h3>在许多编程语言中,都有一个基类的概念,该基类可用于实现所有子类使用的共享功能。 Go(正确地)更喜欢组合而不是继承。
您可以通过嵌入结构来获得类似的效果。 让我们定义一个可以存储先前计算值的<code class="inline">Cache</code>结构。 从案例中检索值时,它还会打印到屏幕上“ cache hit”,而在案例中没有值时,它将打印“ cache miss”并返回-1(有效值为无符号整数)。
<pre class="has"><code class="notranslate brush: plain noskimlinks noskimwords">type Cache struct { cache map[string]uint } func (c *Cache) GetValue(name string) int { value, ok := c.cache[name] if ok { fmt.Println("cache hit") return int(value) } else { fmt.Println("cache miss") return -1 } } func (c *Cache) SetValue(name string, value uint) { c.cache[name] = value }</code></pre>现在,我将此缓存嵌入到Square和Rectangle形状中。 请注意, <code class="inline">GetPerimeter()</code>和<code class="inline">GetArea()</code>现在会首先检查缓存,并仅在缓存中没有该值时才计算该值。
<pre class="has"><code class="notranslate brush: plain noskimlinks noskimwords">type Square struct { Cache side uint } func (s *Square) GetPerimeter() uint { value := s.GetValue("perimeter") if value == -1 { value = int(s.side * 4) s.SetValue("perimeter", uint(value)) } return uint(value) } func (s *Square) GetArea() uint { value := s.GetValue("area") if value == -1 { value = int(s.side * s.side) s.SetValue("area", uint(value)) } return uint(value) } type Rectangle struct { Cache width uint height uint } func (r *Rectangle) GetPerimeter() uint { value := r.GetValue("perimeter") if value == -1 { value = int(r.width r.height) * 2 r.SetValue("perimeter", uint(value)) } return uint(value) } func (r *Rectangle) GetArea() uint { value := r.GetValue("area") if value == -1 { value = int(r.width * r.height) r.SetValue("area", uint(value)) } return uint(value) }</code></pre>最后, <code class="inline">main()</code>函数两次计算总面积以查看缓存效果。
<pre class="has"><code class="notranslate brush: plain noskimlinks noskimwords">func main() { shapes := []Shape{ &Square{Cache{cache: make(map[string]uint)}, 2}, &Rectangle{Cache{cache: make(map[string]uint)}, 3, 5} } var totalArea uint for _, shape := range shapes { totalArea = shape.GetArea() } fmt.Println("Total area: ", totalArea) totalArea = 0 for _, shape := range shapes { totalArea = shape.GetArea() } fmt.Println("Total area: ", totalArea)</code></pre>这是输出:
<pre class="has"><code class="notranslate brush: plain noskimlinks noskimwords">cache miss cache miss Total area: 19 cache hit cache hit Total area: 19</code></pre> <h2>接口与合同</h2>接口很棒,但是它们不能确保实现接口的结构真正满足接口背后的意图。 Go中没有任何表达这种意图的方法。 您只需指定方法的签名即可。
为了超越该基本水平,您需要签订合同。 对象的合同明确规定了每种方法的作用,执行的副作用以及对象在每个时间点的状态。 合同始终存在。 唯一的问题是它是显式的还是隐式的。 在涉及外部API的情况下,合同至关重要。
Go编程模型是围绕接口设计的。 您可以在不带接口的Go语言中进行编程,但是会错过它们的许多好处。 我强烈建议您在Go编程冒险中充分利用接口。
<blockquote>翻译自: https://code.tutsplus.com/tutorials/how-to-define-and-implement-a-go-interface--cms-28962
</blockquote>