如何在 Go 使用 interface


簡述

編寫靈活的、可重複使用的、模塊化的代碼對於開發多功能的程序至關重要。以這種方式開發,可以避免在多個地方做同樣的修改,從而確保代碼更容易維護。如何完成這個目標,不同語言有不同的實現方法來完成這個目標。例如,繼承 [1] 是一種常見的方法,在 Java、C++、C# 等語言中都有使用。

開發者們也可以通過組合 [2] 實現這個設計目標。組合是一個將多個對象和數據類型組合到一個複雜的結構體中的方式。這個是 Go 用來促進代碼複用,模塊化和靈活性的方法。在 Go 中 intrerface 提供了一個方法用於構建複雜的組合,學習使用它們,將會使你創建通用的可重複使用的代碼。

在這篇文章中,我們將會學習如何構建那些有相同行爲的自定義類型,用於複用代碼。我們還將學習如何爲我們自己的自定義類型實現 interface,以滿足在另一個包中定義的接口。

定義一個行爲

組合實現的核心之一是使用 interface。一個 interface 定義一個類型的行爲。Go 標準庫中最常用的 interface 之一是fmt.Stringer 接口:

type Stringer interface {
    String() string
}

第一行代碼定義一個typeStringer。然後表明它是一個interface。就好像定義一個結構體,Go 使用大括號 ({}) 來囊括 interface 的定義。跟結構體的定義相比,我們只定義interface的_行爲_,就是 “這個類型可以做什麼”

對這個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: "Sammy Shark",
 }
 fmt.Println(a.String())
}

第一件事是我們創建了一個新的類型叫做Article。這個類型有一個Title和一個Author字段,兩個都是 string 的 數據類型 (點擊跳轉查看):

...
type Article struct {
 Title string
 Author string
}
...

接着,我們爲 Article 類型定義了一個叫做 String 的 方法(點擊跳轉查看)String方法將會返回一個用於表示Article類型的字符串:

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

然後,在我們的main function_(點擊跳轉查看)裏,我們創建一個Article類型的實例,並且將它賦值給一個 變量(點擊跳轉查看)_叫a。我們給Title字段設置了一個值,爲"理解Go中的Interfaces",給Author字段賦值"Sammy Shark"

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

緊接着,我們通過調用fmt.Println並傳入調用a.String()後的結果,打印出String方法的結果:

...
fmt.Println(a.String())

隨後運行程序,你會發現如下輸出:

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

至此,我們還沒有使用 interface,但是我們創建了一個具備一個行爲的類型。這個行爲匹配fmt.Stringer接口。

隨後,讓我們看看如何利用這種行爲來使我們的代碼更容易重複使用。

定義一個 interface

現在,我們已經用所需的行爲定義了我們的類型,我們可以看看如何使用該行爲。

然而,在這之前,讓我們看看如果我們想在一個函數中從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())
}

這裏我們創建了一個 interface 叫做Stringer

...
type Stringer interface {
 String() string
}
...

Stringerinterface 只有一個方法,叫做String(),返回一個stringmethod_(點擊跳轉查看)_是一個特殊的函數,在 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: "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函數:

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.

到目前爲止,我們已經演示瞭如何只使用一個 interface。然而,一個 interface 可以有不止一個行爲的定義。

接下來,我們將看到如何通過聲明更多的方法來使我們的 interface 更加通用。

多行爲 interface

編寫 Go 代碼的核心原則之一是編寫小而簡潔的類型,並將它們組成更大,更復雜的類型。組合 interface 也是一樣的。

爲了瞭解我們是如何建立一個 interface 的,我們先從只定義一個 interface 開始。我們將會定義 2 個形狀,一個Circle和一個Square,然後他們都會定義一個方法叫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 smallest\n", l)
}

func Less(s1, s2 Sizer) Sizer {
 if s1.Area() < s2.Area() {
  return s1
 }
 return s2
}

因爲每個類型都定義了Area方法,我們可以創建一個 interface 來定義這個行爲。

我們創建如下的Sizerinterface:

...
type Sizer interface {
 Area() float64
}
...

然後定義一個函數叫做Less,傳入 2 個Sizer並返回最小的那一個:

...
func Less(s1, s2 Sizer) Sizer {
 if s1.Area() < s2.Area() {
  return s1
 }
 return s2
}
...

注意到我們不僅接收 2 個都爲Sizer的類型,而且返回的結果也用Sizer。這意味着我們不再返回一個Square或者一個Circle,而是Sizerinterface。

最後,我們打印出哪一個是最小的面積:

Output
{Width:5 Height:10} is the smallest

接着,讓我們給每個類型添加另一個行爲。這次我們添加String()方法,返回一個 string。

這個滿足fmt.Stringerinterface:

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())
}

因爲CircleSquare類型都同時實現了AreaString方法,我們現在可以創建另一個 interface 來描述這些更廣泛的行爲。

爲了實現這個,我們創建了一個 interface 叫做Shaper。這個Shaper將由Sizerinterface 和fmt.Stringerinterface 組成:

...
type Shaper interface {
 Sizer
 fmt.Stringer
}
...

注意:

基於習慣,嘗試以er結尾來給你的 interface 命名,例如fmt.Stringerio.Writer等等。這也是爲什麼我們用Shaper來命名我們的 interface,而不是Shape

現在我們可以創建一個名爲PrintArea的函數,該函數以Shaper爲參數。這意味着我們可以對傳入的值調用AreaString這兩個方法:

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

我們現在已經看到了我們如何創建較小的 interface,並根據需要將它們建立成較大的 interface。

雖然我們可以從較大的 interface 開始,並將其傳遞給我們所有的函數,但最好的做法是隻將最小的 interface 發送給需要的函數。這通常會使代碼更加清晰,因爲任何接收特定的較小的 interface 的東西都只打算執行其定義的行爲。

例如,如果我們將Shaper傳遞給Less函數,我們可能會認爲它要同時調用AreaString方法。然而,由於我們只打算調用Area方法,這使得Less函數很清楚,因爲我們知道我們只能調用傳遞給它的任何參數的Area方法。

總結

我們已經看到,創建較小的 interface 並將其構建爲較大的 interface,可以讓我們只分享我們需要的函數或方法。我們還了解到,可以從其他 interface 中組成我們的 interface,包括從其他包中定義的 interface,而不僅僅是我們的包。

相關鏈接:

[1]https://en.wikipedia.org/wiki/Inheritance_(object-oriented_programming)

[2]https://en.wikipedia.org/wiki/Object_composition

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/BSLwPZ_yw7rWjSBPo_m-Jg