介绍
编写灵活、可复用和模块化的代码对于开发多功能的程序至关重要。这种工作方式确保代码更容易维护,因为它避免了在多个地方做出相同更改的需求。您如何实现这一点取决于语言的不同。例如,继承 是 Java、C++、C# 等语言中常用的方法。
开发者还可以通过 组合 实现同样的设计目标。组合是一种将对象或数据类型组合成更复杂的一个的方法。这是 Go 用来促进代码重用、模块化和灵活性的方法。Go 中的接口提供了一种组织复杂组合的方法,学习如何使用它们将允许您创建常见的可重用代码。
在本文中,我们将学习如何组合具有共同行为的自定义类型,这将允许我们重用我们的代码。我们还将学习如何为我们的自定义类型实现接口,以满足另一个包中定义的接口。
定义行为
一个核心的组成实现是使用接口。接口定义了一个类型的行为。Go标准库中最常用的接口之一是fmt.Stringer
接口:
第一行代码定义了一个名为Stringer
的type
。然后它声明它是一个interface
。就像定义一个结构体,Go使用花括号({}
)来包围接口的定义。与定义结构体相比,我们只定义接口的行为;即“这个类型能做什么”。
在Stringer
接口的情况下,唯一的行为是String()
方法。该方法不接受任何参数,并返回一个字符串。
接下来,让我们看一些具有fmt.Stringer
行为的代码:
我们首先创建一个名为Article
的新类型。这个类型有一个Title
和一个Author
字段,两者都是字符串数据类型:
接下来,我们在Article
类型上定义一个方法
,称为String
。String
方法将返回一个表示Article
类型的字符串:
然后在我们的main
函数中,我们创建了Article
类型的一个实例,并将其赋值给名为a
的变量。我们为Title
字段提供了"Understanding Interfaces in Go"
的值,为Author
字段提供了"Sammy Shark"
的值:
接着,我们通过调用fmt.Println
并传入a.String()
方法调用的结果,输出了String
方法的结果:
运行程序后,您将看到以下输出:
OutputThe "Understanding Interfaces in Go" article was written by Sammy Shark.
到目前为止,我们还没有使用接口,但我们确实创建了一个具有所需行为的数据类型。这种行为与fmt.Stringer
接口相匹配。接下来,让我们看看如何使用这种行为来使我们的代码更加可重用。
定义接口
现在我们已经定义了具有所需行为的类型,我们可以看看如何使用这种行为。
但在我们这样做之前,让我们来看看如果我们想在函数中从Article
类型调用String
方法需要做些什么:
package main
import "fmt"
type Article struct {
Title string
Author string
}
func (a Article) String() string {
return fmt.Sprintf("The %q article was written by %s.", a.Title, a.Author)
}
func main() {
a := Article{
Title: "Understanding Interfaces in Go",
Author: "Sammy Shark",
}
Print(a)
}
func Print(a Article) {
fmt.Println(a.String())
}
在这段代码中,我们添加了一个名为Print
的新函数,它接收一个Article
作为参数。注意,Print
函数所做的唯一事情是调用String
方法。因此,我们可以定义一个接口传递给该函数:
package main
import "fmt"
type Article struct {
Title string
Author string
}
func (a Article) String() string {
return fmt.Sprintf("The %q article was written by %s.", a.Title, a.Author)
}
type Stringer interface {
String() string
}
func main() {
a := Article{
Title: "Understanding Interfaces in Go",
Author: "Sammy Shark",
}
Print(a)
}
func Print(s Stringer) {
fmt.Println(s.String())
}
在这里,我们创建了一个名为Stringer
的接口:
Stringer
接口只有一个方法,名为String()
,它返回一个string
。一个方法是Go中针对特定类型的作用域的特殊函数。与函数不同,方法只能从它定义的类型实例上调用。
然后,我们将Print
方法的签名更新为接受一个Stringer
,而不是具体的Article
类型。因为编译器知道Stringer
接口定义了String
方法,所以它只会接受也有String
方法的类型。
现在,我们可以使用满足Stringer
接口的任何东西调用Print
方法。让我们创建另一个类型来演示这一点:
package main
import "fmt"
type Article struct {
Title string
Author string
}
func (a Article) String() string {
return fmt.Sprintf("The %q article was written by %s.", a.Title, a.Author)
}
type Book struct {
Title string
Author string
Pages int
}
func (b Book) String() string {
return fmt.Sprintf("The %q book was written by %s.", b.Title, b.Author)
}
type Stringer interface {
String() string
}
func main() {
a := Article{
Title: "Understanding Interfaces in Go",
Author: "Sammy Shark",
}
Print(a)
b := Book{
Title: "All About Go",
Author: "Jenny Dolphin",
Pages: 25,
}
Print(b)
}
func Print(s Stringer) {
fmt.Println(s.String())
}
我们现在添加了第二个名为Book
的类型。它也定义了String
方法。这意味着它也满足了Stringer
接口。因此,我们也可以将它发送到我们的Print
函数:
OutputThe "Understanding Interfaces in Go" article was written by Sammy Shark.
The "All About Go" book was written by Jenny Dolphin. It has 25 pages.
目前为止,我们已经展示了如何使用单个接口。但是,接口可以定义多个行为。接下来,我们将看到如何使我们的接口更加灵活,通过声明更多方法。
接口中的多重行为
编写Go代码的一个核心原则是编写小而简洁的类型,然后将它们组合成更大、更复杂的类型。接口也是如此。要了解我们如何构建接口,首先我们需要定义一个接口,该接口只定义了一个方法。我们将定义两个形状,Circle
和Square
,它们都将定义一个名为Area
的方法。这个方法将返回它们各自的几何面积:
由于每个类型都定义了Area
方法,我们可以创建一个定义此行为的接口。我们创建以下Sizer
接口:
然后我们定义一个名为Less
的函数,它接受两个Sizer
并返回最小的那个:
注意我们不仅接受两个参数为Sizer
类型,而且返回结果也为Sizer
类型。这意味着我们不再返回一个Square
或一个Circle
,而是Sizer
接口。
最后,我们打印出哪个具有最小的面积:
Output{Width:5 Height:10} is the smallest
接下来,让我们为每种类型添加另一个行为。这次我们将添加一个返回字符串的 String()
方法。这将满足 fmt.Stringer
接口:
package main
import (
"fmt"
"math"
)
type Circle struct {
Radius float64
}
func (c Circle) Area() float64 {
return math.Pi * math.Pow(c.Radius, 2)
}
func (c Circle) String() string {
return fmt.Sprintf("Circle {Radius: %.2f}", c.Radius)
}
type Square struct {
Width float64
Height float64
}
func (s Square) Area() float64 {
return s.Width * s.Height
}
func (s Square) String() string {
return fmt.Sprintf("Square {Width: %.2f, Height: %.2f}", s.Width, s.Height)
}
type Sizer interface {
Area() float64
}
type Shaper interface {
Sizer
fmt.Stringer
}
func main() {
c := Circle{Radius: 10}
PrintArea(c)
s := Square{Height: 10, Width: 5}
PrintArea(s)
l := Less(c, s)
fmt.Printf("%v is the smallest\n", l)
}
func Less(s1, s2 Sizer) Sizer {
if s1.Area() < s2.Area() {
return s1
}
return s2
}
func PrintArea(s Shaper) {
fmt.Printf("area of %s is %.2f\n", s.String(), s.Area())
}
因为 Circle
和 Square
类型都实现了 Area
和 String
方法,所以现在我们可以创建另一个接口来描述更广泛的行为集合。为此,我们将创建一个名为 Shaper
的接口。我们将由 Sizer
接口和 fmt.Stringer
接口组成:
注意: 通常认为以 er
结尾来命名接口是习惯用法,比如 fmt.Stringer
,io.Writer
等。这就是为什么我们将我们的接口命名为 Shaper
,而不是 Shape
。
现在我们可以创建一个名为 PrintArea
的函数,它接收一个 Shaper
作为参数。这意味着我们可以对传入的值调用 Area
和 String
方法:
如果我们运行程序,将收到以下输出:
Outputarea of Circle {Radius: 10.00} is 314.16
area of Square {Width: 5.00, Height: 10.00} is 50.00
Square {Width: 5.00, Height: 10.00} is the smallest
我们现在已经看到了如何创建更小的接口并将它们构建成更大的接口。虽然我们可以从更大的接口开始,并将其传递给所有的函数,但通常认为最佳实践是只将所需的最小接口传递给函数。这通常会导致代码更清晰,因为任何接受特定更小接口的东西只打算与定义的行为一起工作。
例如,如果我们向 Less 函数传递了 Shaper
,我们可能会假设它会调用其 Area
和 String
方法。然而,由于我们只想调用 Area
方法,那么 Less 函数就变得清晰明了,因为我们知道它只能调用任何传递给它的参数的 Area
方法。
结论
我们已经看到了如何创建更小的接口,并将其构建为更大的接口,从而允许我们在函数或方法中只共享我们需要的东西。我们还了解到,我们可以从其他包定义的接口中组合接口,而不仅仅是我们的包。
如果你想了解更多关于 Go 编程语言的知识,可以查看整个 《如何在 Go 中编码》系列。
Source:
https://www.digitalocean.com/community/tutorials/how-to-use-interfaces-in-go