介绍
编写灵活、可重用和模块化的代码对于开发通用程序至关重要。 以这种方式工作可以避免在多个地方进行相同的更改,从而确保代码更易于维护。 你如何做到这一点因语言而异。 例如,inheritance 是一种在 Java、C++、C# 等语言中使用的常用方法。
开发者也可以通过composition实现同样的设计目标。 组合是一种将对象或数据类型组合成更复杂的类型的方法。 这是 Go 用来促进代码重用、模块化和灵活性的方法。 Go 中的接口提供了一种组织复杂组合的方法,学习如何使用它们将使您能够创建通用的、可重用的代码。
在本文中,我们将学习如何组合具有共同行为的自定义类型,这将允许我们重用我们的代码。 我们还将学习如何为我们自己的自定义类型实现接口,以满足从另一个包定义的接口。
定义行为
组合的核心实现之一是接口的使用。 接口定义了一种类型的行为。 Go 标准库中最常用的接口之一是 fmt.Stringer 接口:
type Stringer interface { String() string }
第一行代码定义了一个名为 Stringer
的 type
。 然后它声明它是一个 interface
。 就像定义结构一样,Go 使用花括号 ({}
) 来包围接口的定义。 与定义结构体相比,我们只定义了接口的behavior; 即“这种类型能做什么”。
在 Stringer
接口的情况下,唯一的行为是 String()
方法。 该方法不接受任何参数并返回一个字符串。
接下来,让我们看一些具有 fmt.Stringer
行为的代码:
main.go
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", } fmt.Println(a.String()) }
我们要做的第一件事是创建一个名为 Article
的新类型。 该类型有一个 Title
和一个 Author
字段,并且都是字符串 数据类型:
main.go
... type Article struct { Title string Author string } ...
接下来,我们在 Article
类型上定义一个名为 String
的 方法。 String
方法将返回一个表示 Article
类型的字符串:
main.go
... func (a Article) String() string { return fmt.Sprintf("The %q article was written by %s.", a.Title, a.Author) } ...
然后,在我们的 main
函数 中,我们创建一个 Article
类型的实例并将其分配给 变量 ,称为 a
. 我们为 Title
字段提供 "Understanding Interfaces in Go"
的值,为 Author
字段提供 "Sammy Shark"
的值:
main.go
... a := Article{ Title: "Understanding Interfaces in Go", Author: "Sammy Shark", } ...
然后,我们通过调用 fmt.Println
打印出 String
方法的结果,并传入 a.String()
方法调用的结果:
main.go
... fmt.Println(a.String())
运行程序后,您将看到以下输出:
OutputThe "Understanding Interfaces in Go" article was written by Sammy Shark.
到目前为止,我们还没有使用接口,但我们确实创建了一个具有行为的类型。 该行为与 fmt.Stringer
接口相匹配。 接下来,让我们看看如何使用该行为来使我们的代码更具可重用性。
定义接口
现在我们已经定义了具有所需行为的类型,我们可以看看如何使用该行为。
然而,在我们这样做之前,让我们看看如果我们想从函数中的 Article
类型调用 String
方法需要做什么:
main.go
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
方法。 因此,我们可以改为定义一个接口来传递给函数:
main.go
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
的接口:
main.go
... type Stringer interface { String() string } ...
Stringer
接口只有一种方法,称为 String()
,它返回 string
。 method 是一个特殊的函数,作用域是 Go 中的特定类型。 与函数不同,方法只能从定义它的类型的实例中调用。
然后我们更新 Print
方法的签名以采用 Stringer
,而不是 Article
的具体类型。 因为编译器知道 Stringer
接口定义了 String
方法,所以它只接受也具有 String
方法的类型。
现在我们可以对任何满足 Stringer
接口的东西使用 Print
方法。 让我们创建另一种类型来证明这一点:
main.go
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
的方法。 此方法将返回它们各自形状的几何面积:
main.go
package main import ( "fmt" "math" ) type Circle struct { Radius float64 } func (c Circle) Area() float64 { return math.Pi * math.Pow(c.Radius, 2) } type Square struct { Width float64 Height float64 } func (s Square) Area() float64 { return s.Width * s.Height } type Sizer interface { Area() float64 } func main() { c := Circle{Radius: 10} s := Square{Height: 10, Width: 5} 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 }
因为每种类型都声明了 Area
方法,所以我们可以创建一个定义该行为的接口。 我们创建如下 Sizer
接口:
main.go
... type Sizer interface { Area() float64 } ...
然后我们定义一个名为 Less
的函数,它接受两个 Sizer
并返回最小的一个:
main.go
... func Less(s1, s2 Sizer) Sizer { if s1.Area() < s2.Area() { return s1 } return s2 } ...
请注意,我们不仅将两个参数都接受为 Sizer
类型,而且还将结果作为 Sizer
返回。 这意味着我们不再返回 Square
或 Circle
,而是返回 Sizer
的接口。
最后,我们打印出面积最小的部分:
Output{Width:5 Height:10} is the smallest
接下来,让我们为每种类型添加另一种行为。 这次我们将添加返回字符串的 String()
方法。 这将满足 fmt.Stringer
接口:
main.go
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
接口组成:
main.go
... type Shaper interface { Sizer fmt.Stringer } ...
注意: 尝试以er
结尾来命名你的界面被认为是惯用的,例如fmt.Stringer
、io.Writer
等。 这就是为什么我们将接口命名为 Shaper
,而不是 Shape
。
现在我们可以创建一个名为 PrintArea
的函数,它以 Shaper
作为参数。 这意味着我们可以对 Area
和 String
方法的传入值调用这两个方法:
main.go
... func PrintArea(s Shaper) { fmt.Printf("area of %s is %.2f\n", s.String(), s.Area()) }
如果我们运行程序,我们将收到以下输出:
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
我们现在已经看到了如何创建更小的接口并根据需要将它们构建成更大的接口。 虽然我们可以从较大的接口开始并将其传递给我们所有的函数,但最好的做法是只将最小的接口发送给所需的函数。 这通常会产生更清晰的代码,因为任何接受特定较小接口的东西都只打算使用该定义的行为。
例如,如果我们将 Shaper
传递给 Less
函数,我们可以假设它会同时调用 Area
和 String
方法。 但是,由于我们只打算调用 Area
方法,它使 Less
函数清楚,因为我们知道我们只能调用传递给它的任何参数的 Area
方法.
结论
我们已经看到如何创建更小的接口并将它们构建成更大的接口,这样我们就可以只将我们需要的东西分享给一个函数或方法。 我们还了解到,我们可以从其他接口组成我们的接口,包括从其他包定义的接口,而不仅仅是我们的包。
如果您想了解有关 Go 编程语言的更多信息,请查看整个 如何在 Go 中编写代码系列 。