如何在Go中使用接口

引言

撰寫靈活、重用性高及模組化的代码對於開發多功能的程序至關重要。這種工作方式可以確保代码更易于維護,因為無需在多個地方做出相同的更改。不同語言之間完成這點的方式各有不同。例如,繼承是Java、C++、C#等語言中常用的方法。

開發者也可以通過 composition(組合)来实现同樣的設計目標。組合是一種將物件或數據類型結合成更複雜物件的方法。Golanguage就是通過這種方式來促進代碼重用、模組化與靈活性。Go中的接口提供了一种組織複雜組合的方法,學習如何使用它們將使你能夠創建常見的、可重用的代碼。

在本文中,我們將學習如何組合具有共同行為的自定義類型,這將讓我們能夠重用我們的代碼。我們還將學習如何為自定義類型實現接口,使其滿足來自另一個包中定義的接口。

定義行為

composition的核心實作之一是使用接口。接口定義了一種类型的行為。在Go標準庫中最常使用的接口之一是fmt.Stringer接口:

type Stringer interface {
    String() string
}

代碼的第一行定義了一個type稱為Stringer。然後指出它是一個interface。與定義struct一樣,Go使用大括號({})來围绕接口的定義。與定義struct相比,我們只定義接口的行為;也就是說,“這個類型能做什么”。

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類型上定義一個method稱為StringString方法將返回一個代表Article類型的字符串:

main.go
...
func (a Article) String() string {
	return fmt.Sprintf("The %q article was written by %s.", a.Title, a.Author)
}
...

然後,在我们的 main 中的 function 中,我們創建了一个 Article 类型的实例並將它赋值給一個名為 avariables 。我們為 Title 字段賦值了 "Understanding Interfaces in Go" ,并为 Author 字段賦值了 "Sammy Shark"

main.go
...
a := Article{
	Title: "Understanding Interfaces in Go",
	Author: "Sammy Shark",
}
...

然後,我們通過调用 fmt.Println 并傳遞 a.String() 方法的結果來打印出結果:

main.go
...
fmt.Println(a.String())

執行程序後,您將會看到以下輸出:

Output
The "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 作為 arguments。留意到這個 Print function 只调用 the String method。因為這樣,我們可以定義一個接口來傳遞給这个 function:

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
}
...

The Stringer interface has only one method, called String() that returns a string. A method is a special function that is scoped to a specific type in Go. Unlike a function, a method can only be called from the instance of the type it was defined on.

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

现在我们可以使用 Print 方法与任何满足 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)
}

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 接口。因为这个原因,我们也可以将 Book 发送给我们的 Print 函数:

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

以上内容我們已經介紹過如何使用一個接口。然而,接口可以定義多個行為。接下来,我們將看看如何讓我們的接口更為彈性,通過 Declaration more methods.

一个接口中的多个行为

编写Go代码的一个核心原则是编写小而简洁的类型,然后将它们组合成更大的、更复杂的类型。接口也是如此。首先,我们定义一个只有一个方法的接口。我们将定义两个形状,CircleSquare,它们都将定义一个名为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` 的接口。我們將 composition 這個接口由 `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())
}

如果我們運行程序,我們將收到以下輸出:

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 函數,我們可能會假設它會調用 AreaString 方法。然而,因為我們只打算調用 Area 方法,這使得 Less 函數變得清晰,因為我們知道我們只能調用傳給它的任何參數的 Area 方法。

結論

我們已經看到,創建較小的接口並將它們組合成較大的接口,可以使我們只分享我們需要傳給函數或方法的内容。我們也學習到,我們可以從其他接口 compositing 我們的接口,包括那些從其他包定義的,不僅僅是我們的包。

如果您想更深入地學習 Go 程式設計語言,請查看完整的 How To Code in Go 系列

Source:
https://www.digitalocean.com/community/tutorials/how-to-use-interfaces-in-go