引言
撰寫靈活、重用性高及模組化的代码對於開發多功能的程序至關重要。這種工作方式可以確保代码更易于維護,因為無需在多個地方做出相同的更改。不同語言之間完成這點的方式各有不同。例如,繼承是Java、C++、C#等語言中常用的方法。
開發者也可以通過 composition(組合)来实现同樣的設計目標。組合是一種將物件或數據類型結合成更複雜物件的方法。Golanguage就是通過這種方式來促進代碼重用、模組化與靈活性。Go中的接口提供了一种組織複雜組合的方法,學習如何使用它們將使你能夠創建常見的、可重用的代碼。
在本文中,我們將學習如何組合具有共同行為的自定義類型,這將讓我們能夠重用我們的代碼。我們還將學習如何為自定義類型實現接口,使其滿足來自另一個包中定義的接口。
定義行為
composition的核心實作之一是使用接口。接口定義了一種类型的行為。在Go標準庫中最常使用的接口之一是fmt.Stringer
接口:
代碼的第一行定義了一個type
稱為Stringer
。然後指出它是一個interface
。與定義struct一樣,Go使用大括號({}
)來围绕接口的定義。與定義struct相比,我們只定義接口的行為;也就是說,“這個類型能做什么”。
在Stringer
接口的情況下,唯一的行為是String()
方法。這個方法不接收任何參數並返回一個字符串。
接下來,讓我們看看具有fmt.Stringer
行為的代碼:
我們首先創建一個稱為Article
的新類型。這個類型有一個Title
和一個Author
字段,並且它們都是字符串數據類型:
接下來,在Article
類型上定義一個method
稱為String
。String
方法將返回一個代表Article
類型的字符串:
然後,在我们的 main
中的 function 中,我們創建了一个 Article
类型的实例並將它赋值給一個名為 a
的 variables 。我們為 Title
字段賦值了 "Understanding Interfaces in Go"
,并为 Author
字段賦值了 "Sammy Shark"
:
然後,我們通過调用 fmt.Println
并傳遞 a.String()
方法的結果來打印出結果:
執行程序後,您將會看到以下輸出:
OutputThe "Understanding Interfaces in Go" article was written by Sammy Shark.
到目前为止,我们还没有使用接口,但我们确实创建了一个具有所需行为的数据类型。该行为匹配了 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
作為 arguments。留意到這個 Print
function 只调用 the String
method。因為這樣,我們可以定義一個接口來傳遞給这个 function:
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
:
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
接口的类型一起使用。让我们创建另一个类型来演示这一点:
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
函数:
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.
以上内容我們已經介紹過如何使用一個接口。然而,接口可以定義多個行為。接下来,我們將看看如何讓我們的接口更為彈性,通過 Declaration more methods.
一个接口中的多个行为
编写Go代码的一个核心原则是编写小而简洁的类型,然后将它们组合成更大的、更复杂的类型。接口也是如此。首先,我们定义一个只有一个方法的接口。我们将定义两个形状,Circle
和Square
,它们都将定义一个名为Area
的方法。该方法将返回各自形状的几何面积:
因为每个类型都声明了Area
方法,我们可以创建一个定义此行为的接口。我们创建以下Sizer
接口:
然后我们定义一个函数Less
,它接受两个Sizer
并返回最小的那个:
注意,我们不仅接受两个参数为Sizer
类型,还返回一个Sizer
类型的结果。这意味着我们不再返回一个Square
或一个Circle
,而是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 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` 接口組成:
注: 一般认为以 `er` 结尾的接口名称是惯用语,例如 `fmt.Stringer`、`io.Writer` 等。这就是为什么我们称我们的接口为 `Shaper`,而不是 `Shape`。
現在,我们可以創建一個稱為 `PrintArea` 的函數,它接收一個 `Shaper` 作為參數。這意味著我們可以對傳入的值調用兩個方法,即 `Area` 和 `String` 方法:
如果我們運行程序,我們將收到以下輸出:
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
方法。
結論
我們已經看到,創建較小的接口並將它們組合成較大的接口,可以使我們只分享我們需要傳給函數或方法的内容。我們也學習到,我們可以從其他接口 compositing 我們的接口,包括那些從其他包定義的,不僅僅是我們的包。
如果您想更深入地學習 Go 程式設計語言,請查看完整的 How To Code in Go 系列。
Source:
https://www.digitalocean.com/community/tutorials/how-to-use-interfaces-in-go