介绍
编写灵活的、可重用的和模块化的代码对于开发通用程序是至关重要的。以这种方式工作可以避免在多个位置进行相同的更改,从而确保代码更容易维护。如何做到这一点因语言而异。例如,继承是在 Java 、 c++ 、c#等语言中使用的一种常见方法。
开发人员还可以通过组合实现相同的设计目标。复合是将对象或数据类型组合成更复杂的类型的一种方法。这是 Go 用来促进代码重用、模块化和灵活性的方法。Go中的接口提供了一种组织复杂组合的方法,学习如何使用它们将允许您创建通用的、可重用的代码。
在本文中,我们将学习如何组合具有常见行为的自定义类型,这将允许我们重用代码。我们还将学习如何为我们实现自定义类型实现接口,并使用从另一个包中定义的接口。
定义一个接口行为
组合的核心实现之一是接口的使用。接口定义了类型的行为。Go标准库中最常用的接口之一是fmt.Stringer接口:
type Stringer interface {
String() string
}
第一行代码定义了一个名为Stringer的类型,然后声明它是一个接口。就像定义一个 结构体 一样,Go使用大括号({})来包围接口的定义。与定义结构相比,我们只定义了接口的行为:也就是“这种类型能做什么”。
对于Stringer接口,唯一的行为是String()方法。该方法不带参数,返回一个 字符串 。
接下来,让我们看一些具有fmt.Stringer的代码行为:
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: “Kobe Bryant”,
}
fmt.Println(a.String())
}
我们要做的第一件事是创建一个名为Article的新类型。该类型有一个Title和一个Author字段,两者都是字符串数据类型。
接下来,我们在Article类型上定义一个名为String的方法。String方法将返回一个表示Article类型的字符串。
然后,在我们的主函数中,我们创建了一个Article类型的实例,并将其赋值给名为a的变量。我们为Title字段提供“Understanding Interfaces in Go”的值,为Author字段提供“Kobe Bryant”的值。
最后,通过调用fmt输出String方法Println,并传入a.String()方法调用的结果,最终输出:
Output
The “Understanding Interfaces in Go” article was written by Kobe Bryant.
到目前为止,我们还没有使用接口,但是我们确实创建了一个具有行为的类型。该行为匹配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的接口:
…
type Stringer interface {
String() string
}
…
Stringer接口只有一个返回字符串的方法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: “Kobe Bryant”,
}
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函数:
Output
The “Understanding Interfaces in Go” article was written by Kobe Bryant.
The “All About Go” book was written by Jenny Dolphin. It has 25 pages.
到目前为止,我们已经演示了如何只使用一个接口。但是,一个接口可以定义多个行为。接下来,我们将看到如何通过声明更多的方法使接口更通用。
一个接口中的多种行为
编写Go代码的核心之一是编写小而简洁的类型,并将它们组合成更大、更复杂的类型。组合接口时也是如此。为了了解如何构建接口,我们首先只定义一个接口。我们将定义两个形状,圆形和方形,它们都将定义一个名为Area的方法。这个方法将返回它们各自形状的几何面积:
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 smallestn”, l)
}
func Less(s1, s2 Sizer) Sizer {
if s1.Area() < s2.Area() {
return s1
}
return s2
}
因为每种类型都声明了Area方法,所以我们可以创建一个定义该行为的接口。我们创建了以下Sizer接口:
…
type Sizer interface {
Area() float64
}
…
然后定义一个名为Less的函数,该函数接受两个size值,并返回最小的size值:
…
func Less(s1, s2 Sizer) Sizer {
if s1.Area() < s2.Area() {
return s1
}
return s2
}
…
请注意,我们不仅接受两个参数作为类型Sizer,而且还将结果作为接口Sizer返回。这意味着我们返回的不再是正方形或圆形,而是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 smallestn”, 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 %.2fn”, s.String(), s.Area())
}
因为Circle和Square类型都实现了Area和String方法,我们现在可以创建另一个接口来描述更广泛的行为集。为此,我们将创建一个名为Shaper的接口,把Sizer接口和fmt.Stringer接口组合在一起:
…
type Shaper interface {
Sizer
fmt.Stringer
}
…
现在我们可以创建一个名为PrintArea的函数,它接受Shaper作为参数。这意味着我们可以同时调用Area和String方法:
…
func PrintArea(s Shaper) {
fmt.Printf(“area of %s is %.2fn”, s.String(), s.Area())
}
输出结果:
Output
area 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方法,传递Sizer接口使得Less函数变得很清楚,因为我们知道,我们只能调用传递给它的任何参数的Area方法。
总结
我们已经了解了如何创建较小的接口并将其构建为较大的接口,从而共享函数或方法所需的内容。我们还了解到可以用其他接口组合我们的接口,包括那些从其他包定义的接口,而不仅仅是从我们自己定义的包。