如何在 Go 使用 interface

简述

编写灵活的、可重复使用的、模块化的代码对于开发多功能的程序至关重要。以这种方式开发,可以避免在多个地方做同样的修改,从而确保代码更容易维护。如何完成这个目标,不同语言有不同的实现方法来完成这个目标。例如,继承是一种常见的方法,在 Java、C++、C#等语言中都有使用。

开发者们也可以通过组合实现这个设计目标。组合是一个将多个对象和数据类型组合到一个复杂的结构体中的方式。这个是 Go 用来促进代码复用,模块化和灵活性的方法。在 Go 中 intrerface 提供了一个方法用于构建复杂的组合,学习使用它们,将会使你创建通用的可重复使用的代码。

在这篇文章中,我们将会学习如何构建那些有相同行为的自定义类型,用于复用代码。 我们还将学习如何为我们自己的自定义类型实现 interface,以满足在另一个包中定义的接口。

定义一个行为

组合实现的核心之一是使用 interface。一个 interface 定义一个类型的行为。Go 标准库中最常用的 interface 之一是fmt.Stringer 接口:

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

第一行代码定义一个typeStringer。然后表明它是一个interface。就好像定义一个结构体,Go 使用大括号({})来囊括 interface 的定义。跟结构体的定义相比,我们只定义interface行为,就是“这个类型可以做什么”

对这个Stringer接口的例子来说,唯一的行为就是String()这个方法。这个方法没有参数。

接着,让我们看一些代码,这些代码有fmt.Stringer的行为:

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

第一件事是我们创建了一个新的类型叫做Article。这个类型有一个Title和一个Author字段,两个都是 string 的 数据类型:

  1. ...
  2. type Article struct {
  3. Title string
  4. Author string
  5. }
  6. ...

接着,我们为 Article 类型定义了一个叫做 String 的 方法String方法将会返回一个用于表示Article类型的字符串:

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

然后,在我们的mainfunction里,我们创建一个Article类型的实例,并且将它赋值给一个变量a。我们给Title字段设置了一个值,为"理解Go中的Interfaces",给Author字段赋值"Sammy Shark"

  1. ...
  2. a := Article{
  3. Title: "Understanding Interfaces in Go",
  4. Author: "Sammy Shark",
  5. }
  6. ...

紧接着,我们通过调用fmt.Println并传入调用a.String()后的结果,打印出String方法的结果:

  1. ...
  2. fmt.Println(a.String())

随后运行程序,你会发现如下输出:

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

至此,我们还没有使用 interface,但是我们创建了一个具备一个行为的类型。这个行为匹配fmt.Stringer接口。随后,让我们看看如何利用这种行为来使我们的代码更容易重复使用。

定义一个 interface

现在,我们已经用所需的行为定义了我们的类型,我们可以看看如何使用该行为。

然而,在这之前,让我们看看如果我们想在一个函数中从Article类型中调用String方法,我们需要做什么:

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

这段代码中,我们添加了一个名为Print的新函数,该函数接收一个Article作为参数。请注意,Print函数唯一做的事情是调用String方法。正因为如此,我们则可以定义一个接口来传递给函数。

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

这里我们创建了一个 interface 叫做Stringer

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

Stringerinterface 只有一个方法,叫做String(),返回一个stringmethod是一个特殊的函数,在 Go 中被限定于一个特殊类型。不像函数,一个方法只能从它所定义的类型的实例中被调用。

然后我们更新Print方法的签名来接收一个Stringer,而不是一个Article的具体类型。因为编译器知道Stringer接口定义了String方法,所以它只接收也有String方法的类型。

现在我们可以对任何满足Stringer接口的东西使用Print方法。让我们创建另一个类型来证明这一点:

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

现在,我们添加了第二个类型叫Book。它同样也有定义String方法。这表示它也满足Stringer接口。因此,我们也可以传递它到Print函数:

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

到目前为止,我们已经演示了如何只使用一个 interface。然而,一个 interface 可以有不止一个行为的定义。接下来,我们将看到如何通过声明更多的方法来使我们的 interface 更加通用。

多行为 interface

编写 Go 代码的核心原则之一是编写小而简洁的类型,并将它们组成更大,更复杂的类型。组合 interface 也是一样的。为了了解我们是如何建立一个 interface 的,我们先从只定义一个 interface 开始。我们将会定义 2 个形状,一个Circle和一个Square,然后他们都会定义一个方法叫Area。这个方法会返回它们对应形状的几何面积:

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

因为每个类型都定义了Area方法,我们可以创建一个 interface 来定义这个行为。我们创建如下的Sizerinterface:

  1. ...
  2. type Sizer interface {
  3. Area() float64
  4. }
  5. ...

然后定义一个函数叫做Less,传入 2 个Sizer并返回最小的那一个:

  1. ...
  2. func Less(s1, s2 Sizer) Sizer {
  3. if s1.Area() < s2.Area() {
  4. return s1
  5. }
  6. return s2
  7. }
  8. ...

注意到我们不仅接收 2 个都为Sizer的类型,而且返回的结果也用Sizer。这意味着我们不再返回一个Square或者一个Circle,而是Sizerinterface。

最后,我们打印出哪一个是最小的面积:

  1. Output
  2. {Width:5 Height:10} is the smallest

接着,让我们给每个类型添加另一个行为。这次我们添加String()方法,返回一个 string。这个满足fmt.Stringerinterface:

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

因为CircleSquare类型都同时实现了AreaString方法,我们现在可以创建另一个 interface 来描述这些更广泛的行为。为了实现这个,我们创建了一个 interface 叫做Shaper。这个Shaper将由Sizerinterface 和fmt.Stringerinterface 组成:

  1. ...
  2. type Shaper interface {
  3. Sizer
  4. fmt.Stringer
  5. }
  6. ...

**注意:**基于习惯,尝试以er结尾来给你的 interface 命名,例如fmt.Stringerio.Writer等等。这也是为什么我们用Shaper来命名我们的 interface,而不是Shape

现在我们可以创建一个名为PrintArea的函数,该函数以Shaper为参数。这意味着我们可以对传入的值调用AreaString这两个方法:

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

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

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

我们现在已经看到了我们如何创建较小的 interface,并根据需要将它们建立成较大的 interface。虽然我们可以从较大的 interface 开始,并将其传递给我们所有的函数,但最好的做法是只将最小的 interface 发送给需要的函数。这通常会使代码更加清晰,因为任何接收特定的较小的 interface 的东西都只打算执行其定义的行为。

例如,如果我们将Shaper传递给Less函数,我们可能会认为它要同时调用AreaString方法。然而,由于我们只打算调用Area方法,这使得Less函数很清楚,因为我们知道我们只能调用传递给它的任何参数的Area方法。

总结

我们已经看到,创建较小的 interface 并将其构建为较大的 interface,可以让我们只分享我们需要的函数或方法。我们还了解到,我们可以从其他 interface 中组成我们的 interface,包括从其他包中定义的 interface,而不仅仅是我们的包。