介绍
编写灵活、可重复使用和模块化代码对于开发多功能程序至关重要。以这种方式工作,确保代码更容易维护,避免在多个地方进行相同的更改。
开发人员还可以通过 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
函数,我们可以假设它会调用Area
和String
方法,但是,由于我们只打算调用Area
方法,它使Less
函数清晰,因为我们知道我们只能调用给它传递的任何论点的Area
方法。
结论
我们已经看到如何创建更小的接口,并将它们构建为更大的接口,使我们能够只分享我们需要的函数或方法,我们还了解到我们可以从其他接口组成我们的接口,包括从其他包中定义的接口,而不仅仅是我们的包。
如果您想了解更多关于 Go 编程语言的信息,请查看整个 How To Code in Go 系列。