如何在 Go 中使用接口

介绍

编写灵活、可重复使用和模块化代码对于开发多功能程序至关重要。以这种方式工作,确保代码更容易维护,避免在多个地方进行相同的更改。

开发人员还可以通过 composition实现相同的设计目标)。 构成是将对象或数据类型合并为更复杂的方法。 这是 Go 使用的方法来促进代码的重复使用、模块化和灵活性。

在本文中,我们将学习如何编写具有共同行为的自定义类型,这将使我们能够重复使用我们的代码。

定义一种行为

构成的核心实现之一是使用接口. 接口定义一种类型的行为. Go 标准库中最常用的接口之一是 fmt.Stringer接口:

1type Stringer interface {
2    String() string
3}

代码的第一行定义了一个名为Stringer类型。然后它声明它是一个界面。就像定义一个结构一样,Go使用弯曲的框架(`{})来围绕界面的定义。

Stringer界面的情况下,唯一的行为是String()方法,该方法没有参数并返回一个字符串。

接下来,让我们看看一些具有fmt.Stringer行为的代码:

 1[label main.go]
 2package main
 3
 4import "fmt"
 5
 6type Article struct {
 7    Title string
 8    Author string
 9}
10
11func (a Article) String() string {
12    return fmt.Sprintf("The %q article was written by %s.", a.Title, a.Author)
13}
14
15func main() {
16    a := Article{
17    	Title: "Understanding Interfaces in Go",
18    	Author: "Sammy Shark",
19    }
20    fmt.Println(a.String())
21}

我们做的第一件事是创建一个名为文章的新类型,这个类型有一个标题和一个作者字段,两者都属于字符串 数据类型:

1[label main.go]
2...
3type Article struct {
4    Title string
5    Author string
6}
7...

接下来,我们在文章类型上定义一个名为字符串method

1[label main.go]
2...
3func (a Article) String() string {
4    return fmt.Sprintf("The %q article was written by %s.", a.Title, a.Author)
5}
6...

然后,在我们的主要函数(https://andsky.com/tech/tutorials/how-to-define-and-call-functions-in-go)中,我们创建一个文章类型的实例,并将其分配给名为a变量(https://andsky.com/tech/tutorials/how-to-use-variables-and-constants-in-go)。

1[label main.go]
2...
3a := Article{
4    Title: "Understanding Interfaces in Go",
5    Author: "Sammy Shark",
6}
7...

然后,我们通过调用fmt.Println来打印String方法的结果,并通过a.String()方法调用的结果:

1[label main.go]
2...
3fmt.Println(a.String())

启动程序后,您将看到以下输出:

1[secondary_label Output]
2The "Understanding Interfaces in Go" article was written by Sammy Shark.

到目前为止,我们还没有使用接口,但我们确实创建了一个具有行为的类型,这种行为与fmt.Stringer接口相匹配。

定义一个界面

现在我们已经确定了我们想要的行为的类型,我们可以看看如何使用这种行为。

然而,在我们这样做之前,让我们看看如果我们想在函数中从文章类型调用字符串方法,我们需要做些什么:

 1[label main.go]
 2package main
 3
 4import "fmt"
 5
 6type Article struct {
 7    Title string
 8    Author string
 9}
10
11func (a Article) String() string {
12    return fmt.Sprintf("The %q article was written by %s.", a.Title, a.Author)
13}
14
15func main() {
16    a := Article{
17    	Title: "Understanding Interfaces in Go",
18    	Author: "Sammy Shark",
19    }
20    Print(a)
21}
22
23func Print(a Article) {
24    fmt.Println(a.String())
25}

在此代码中,我们添加了一个名为打印的新函数,该函数以文章为参数。 请注意,打印函数所做的唯一事情就是调用字符串方法。

 1[label main.go]
 2package main
 3
 4import "fmt"
 5
 6type Article struct {
 7    Title string
 8    Author string
 9}
10
11func (a Article) String() string {
12    return fmt.Sprintf("The %q article was written by %s.", a.Title, a.Author)
13}
14
15type Stringer interface {
16    String() string
17}
18
19func main() {
20    a := Article{
21    	Title: "Understanding Interfaces in Go",
22    	Author: "Sammy Shark",
23    }
24    Print(a)
25}
26
27func Print(s Stringer) {
28    fmt.Println(s.String())
29}

在这里,我们创建了一个名为Stringer的界面:

1[label main.go]
2...
3type Stringer interface {
4    String() string
5}
6...

Stringer界面只有一个方法,名为String(),返回一个字符串。A method是一个专门的函数,在Go中范围到特定的类型。

然后,我们更新了打印方法的签名,以便采用字符串,而不是具体的文章类型,因为编译器知道字符串界面定义了字符串方法,所以它只会接受具有字符串方法的类型。

现在我们可以使用打印方法与任何符合Stringer界面的东西。

 1[label main.go]
 2package main
 3
 4import "fmt"
 5
 6type Article struct {
 7    Title string
 8    Author string
 9}
10
11func (a Article) String() string {
12    return fmt.Sprintf("The %q article was written by %s.", a.Title, a.Author)
13}
14
15type Book struct {
16    Title string
17    Author string
18    Pages int
19}
20
21func (b Book) String() string {
22    return fmt.Sprintf("The %q book was written by %s.", b.Title, b.Author)
23}
24
25type Stringer interface {
26    String() string
27}
28
29func main() {
30    a := Article{
31    	Title:  "Understanding Interfaces in Go",
32    	Author: "Sammy Shark",
33    }
34    Print(a)
35
36    b := Book{
37    	Title:  "All About Go",
38    	Author: "Jenny Dolphin",
39    	Pages:  25,
40    }
41    Print(b)
42}
43
44func Print(s Stringer) {
45    fmt.Println(s.String())
46}

现在我们添加了一种名为Book的第二种类型,它还定义了String方法,这意味着它也满足了Stringer界面,因此我们还可以将其发送到我们的打印函数:

1[secondary_label Output]
2The "Understanding Interfaces in Go" article was written by Sammy Shark.
3The "All About Go" book was written by Jenny Dolphin. It has 25 pages.

到目前为止,我们已经展示了如何使用单个界面,但是,一个界面可以有多个行为定义,接下来,我们将看到如何通过宣布更多的方法来使我们的界面更加多功能。

一个接口中的多种行为

编写Go代码的核心租户之一是写小、简要的类型,并将其组成成更大的、更复杂的类型。在编写接口时,同样是如此。 要看我们如何构建一个接口,我们首先要从只定义一个接口开始。 我们将定义两个形状,一个和一个方块,它们都将定义一种称为区域的方法。 这种方法将返回各自的形状的几何区域:

 1[label main.go]
 2package main
 3
 4import (
 5    "fmt"
 6    "math"
 7)
 8
 9type Circle struct {
10    Radius float64
11}
12
13func (c Circle) Area() float64 {
14    return math.Pi * math.Pow(c.Radius, 2)
15}
16
17type Square struct {
18    Width float64
19    Height float64
20}
21
22func (s Square) Area() float64 {
23    return s.Width * s.Height
24}
25
26type Sizer interface {
27    Area() float64
28}
29
30func main() {
31    c := Circle{Radius: 10}
32    s := Square{Height: 10, Width: 5}
33
34    l := Less(c, s)
35    fmt.Printf("%+v is the smallest\n", l)
36}
37
38func Less(s1, s2 Sizer) Sizer {
39    if s1.Area() < s2.Area() {
40    	return s1
41    }
42    return s2
43}

由于每个类型都声明了Area方法,所以我们可以创建一个界面来定义这种行为,我们创建了以下Sizer界面:

1[label main.go]
2...
3type Sizer interface {
4    Area() float64
5}
6...

然后我们定义一个名为Less的函数,该函数需要两个Sizer,并返回最小的函数:

1[label main.go]
2...
3func Less(s1, s2 Sizer) Sizer {
4    if s1.Area() < s2.Area() {
5    	return s1
6    }
7    return s2
8}
9...

请注意,我们不仅接受两个参数作为类型Sizer,而且还将结果返回为Sizer

最后,我们打印了最小的面积:

1[secondary_label Output]
2{Width:5 Height:10} is the smallest

接下来,我们将为每个类型添加另一个行为. 这次我们将添加返回字符串的 String() 方法. 这将满足 fmt.Stringer 界面:

 1[label main.go]
 2package main
 3
 4import (
 5    "fmt"
 6    "math"
 7)
 8
 9type Circle struct {
10    Radius float64
11}
12
13func (c Circle) Area() float64 {
14    return math.Pi * math.Pow(c.Radius, 2)
15}
16
17func (c Circle) String() string {
18    return fmt.Sprintf("Circle {Radius: %.2f}", c.Radius)
19}
20
21type Square struct {
22    Width float64
23    Height float64
24}
25
26func (s Square) Area() float64 {
27    return s.Width * s.Height
28}
29
30func (s Square) String() string {
31    return fmt.Sprintf("Square {Width: %.2f, Height: %.2f}", s.Width, s.Height)
32}
33
34type Sizer interface {
35    Area() float64
36}
37
38type Shaper interface {
39    Sizer
40    fmt.Stringer
41}
42
43func main() {
44    c := Circle{Radius: 10}
45    PrintArea(c)
46
47    s := Square{Height: 10, Width: 5}
48    PrintArea(s)
49
50    l := Less(c, s)
51    fmt.Printf("%v is the smallest\n", l)
52
53}
54
55func Less(s1, s2 Sizer) Sizer {
56    if s1.Area() < s2.Area() {
57    	return s1
58    }
59    return s2
60}
61
62func PrintArea(s Shaper) {
63    fmt.Printf("area of %s is %.2f\n", s.String(), s.Area())
64}

由于平方类型都执行区域字符串方法,我们现在可以创建另一个界面来描述更广泛的行为。

1[label main.go]
2...
3type Shaper interface {
4    Sizer
5    fmt.Stringer
6}
7...

<$>[注] ** 注:** 试图用er命名您的界面,例如fmt.Stringer,io.Writer,等等,这就是为什么我们命名我们的界面为Shaper,而不是Shape

现在我们可以创建一个名为PrintArea的函数,该函数以Shaper为参数。

1[label main.go]
2...
3func PrintArea(s Shaper) {
4    fmt.Printf("area of %s is %.2f\n", s.String(), s.Area())
5}

如果我们运行该程序,我们将收到以下输出:

1[secondary_label Output]
2area of Circle {Radius: 10.00} is 314.16
3area of Square {Width: 5.00, Height: 10.00} is 50.00
4Square {Width: 5.00, Height: 10.00} is the smallest

虽然我们可以从更大的界面开始,并将其传递给我们的所有功能,但它被认为是最好的做法,只将最小的界面发送到一个需要的功能。

例如,如果我们将Shaper转移到Less函数,我们可以假设它会调用AreaString方法,但是,由于我们只打算调用Area方法,它使Less函数清晰,因为我们知道我们只能调用给它传递的任何论点的Area方法。

结论

我们已经看到如何创建更小的接口,并将它们构建为更大的接口,使我们能够只分享我们需要的函数或方法,我们还了解到我们可以从其他接口组成我们的接口,包括从其他包中定义的接口,而不仅仅是我们的包。

如果您想了解更多关于 Go 编程语言的信息,请查看整个 How To Code in Go 系列

Published At
Categories with 技术
Tagged with
comments powered by Disqus