第十六章:介面

Zioyi發表於2021-11-28

image

本篇翻譯自《Practical Go Lessons》 Chapter 16: Interfaces

1 你將在本章學到什麼?

  • 什麼是型別介面?
  • 如何定義介面。
  • “實現一個介面”是什麼意思?
  • 介面的優點

2 涵蓋的技術概念

  • 介面 interface
  • 具體實現 concrete implementation
  • 實現一個介面
  • 介面的方法集

3 介紹

剛開始程式設計時,介面似乎很難理解。通常,新手程式設計師並不能完全理解介面的潛力。本節旨在解釋什麼是介面,它的有趣之處在哪裡,以及如何建立介面。

4 介面的基本定義

  • 介面是定義一組行為的契約
  • 介面是一個純粹的設計物件,它們只是定義了一組行為(即方法),而沒有給出這些行為的任何實現
  • 介面是一種型別,它定義了一組方法而不實現它們

“實現” = “編寫方法的程式碼”,這是一個示例介面型別(來自標準包 io):

type Reader interface {
    Read(p []byte) (n int, err error)
}

這裡我們有一個名為 Reader 的介面型別,它指定了一種名為 Read 的方法。該方法沒有具體實現,唯一指定的是方法名稱及其簽名(引數型別和結果型別)。

4.0.0.1 介面型別的零值

介面型別的零值為 nil,例子:

var r io.Reader
log.Println(r)
// 2021/11/28 12:27:52 <nil>

5 基本示例

type Human struct {
    Firstname string
    Lastname string
    Age int
    Country string
}

type DomesticAnimal interface {
    ReceiveAffection(from Human)
    GiveAffection(to Human)
}
  • 首先,我們宣告一個名為 Human 的型別
  • 我們宣告瞭一個名為 DomesticAnimal 的新型別介面
  • 這種型別的介面有一個由兩個方法組成的方法集:ReceiveAffectionGiveAffect

DomesticAnimal 是一個契約。

  • 它告訴開發者,要成為 DomesticAnimal,我們至少需要有兩種行為:ReceiveAffectionGiveAffection

讓我們建立兩個型別:

type Cat struct {
    Name string
}

type Dog struct {
    Name string
}

我們有兩種新型別。為了讓他們遵守我們的介面 DomesticAnimal 的契約,
我們必須為每種型別定義介面指定的方法。

我們從 Cat 型別開始:

func (c Cat) ReceiveAffection(from Human) {
    fmt.Printf("The cat named %s has received affection from Human named %s\n", c.Name, from.Firstname)
}

func (c Cat) GiveAffection(to Human) {
    fmt.Printf("The cat named %s has given affection to Human named %s\n", c.Name, to.Firstname)
}

現在 Cat 型別實現了 DomesticAnimal 介面。我們現在對 Dog 型別做同樣的事情:

func (d Dog) ReceiveAffection(from Human) {
    fmt.Printf("The dog named %s has received affection from Human named %s\n", d.Name, from.Firstname)
}

func (d Dog) GiveAffection(to Human) {
    fmt.Printf("The dog named %s has given affection to Human named %s\n", d.Name, to.Firstname)
}

我們的 Dog 型別現在正確地實現了 DomesticAnimal 介面。現在我們可以建立一個函式,它接受一個帶有引數的介面:

func Pet(animal DomesticAnimal, human Human) {
    animal.GiveAffection(human)
    animal.ReceiveAffection(human)
}

Pet 函式將 DomesticAnimal 型別的介面作為第一個引數,將 Human 作為第二個引數。

在函式內部,我們呼叫了介面的兩個函式。

讓我們使用這個函式:

func main() {

    // Create the Human
    var john Human
    john.Firstname = "John"


    // Create a Cat
    var c Cat
    c.Name = "Maru"

    // then a dog
    var d Dog
    d.Name = "Medor"

    Pet(c, john)
    Pet(d,john)
}
  • DogCat 型別實現了介面 DomesticAnimal 的方法
  • 也就是說 DogCat 型別的任何變數都可以看作DomesticAnimal

只要 Cat 實現的方法的函式簽名與介面定義一致就可以,不強制要求完全相同變數名和返回名。所以我們將函式 func (c Cat) ReceiveAffection(from Human) {...} 改成 func (c Cat) ReceiveAffection(f Human) {...} 也是可以的

6 編譯器在看著你!

遵守型別 T 的介面契約意味著實現介面的所有方法。讓我們試著欺騙編譯器看看會發生什麼:

// ...
// let's create a concrete type Snake
type Snake struct {
    Name string
}
// we do not implement the methods ReceiveAffection and GiveAffection intentionally
//...


func main(){

    var snake Snake
    snake.Name = "Joe"

    Pet(snake, john)
}
  • 我們建立了一個新型別的 Snake
  • 該型別沒有實現 DomesticAnimal 動物的任何方法
  • 在主函式中,我們建立了一個新的 Snake 型別的變數
  • 然後我們用這個變數作為第一個引數呼叫 Pet 函式

結果是編譯失敗:

./main.go:70:5: cannot use snake (type Snake) as type DomesticAnimal in argument to Pet:
    Snake does not implement DomesticAnimal (missing GiveAffection method)

編譯器在未實現的按字母順序排列的第一個方法處檢查停止。

7 例子:database/sql/driver.Driver

我們來看看 Driver 介面(來自包database/sql/driver

type Driver interface {
    Open(name string) (Conn, error)
}
  • 存在不同種類的 SQL 資料庫,因此 Open 方法有多種實現。
  • 為什麼?因為你不會使用相同的程式碼來啟動到 MySQL 資料庫和 Oracle 資料庫的連線。
  • 通過構建介面,你可以定義一個可供多個實現使用的契約。

8 介面嵌入

你可以將介面嵌入到其他介面中。讓我們舉個例子:

// the Stringer type interface from the standard library
type Stringer interface {
    String() string
}
// A homemade interface
type DomesticAnimal interface {
    ReceiveAffection(from Human)
    GiveAffection(to Human)
    // embed the interface Stringer into the DomesticAnimal interface
    Stringer
}

在上面的程式碼中,我們將介面 Stringer 嵌入到介面 DomesticAnimal 中。
因此,已經實現了 DomesticAnimal 的其他型別必須實現 Stringer 介面的方法。

  • 通過介面嵌入,你可以在不重複的情況下向介面新增功能。
  • 這也是有代價的,如果你從另一個模組嵌入一個介面,你的程式碼將與其耦合
    • 其他模組介面的更改將迫使你重寫程式碼。
    • 請注意,如果依賴模組遵循語義版本控制方案,則這種危險會得到緩和
    • 你可以毫無畏懼地使用標準庫中的介面

9 來自標準庫的一些有用(和著名)的介面

9.1 Error 介面

type error interface {
    Error() string
}

這個介面型別被大量使用,用於當函式或方法執行失敗是返會error型別介面:

func (c *Communicator) SendEmailAsynchronously(email *Email) error {
    //...
}

要建立一個 error ,我們通常呼叫: fmt.Errorf() 返回一個 error 型別的結果,或者使用 errors.New()函式。
當然,你也可以建立實現error介面的型別。

9.2 fmt.Stringer 介面

type Stringer interface {
    String() string
}

使用 Stringer 介面,你可以定義在呼叫列印方法時如何將型別列印為字串(fmt.Errorf(),fmt.Println, fmt.Printf, fmt.Sprintf...)

這有一個示例實現

type Human struct {
    Firstname string
    Lastname string
    Age int
    Country string
}

func (h Human) String() string {
    return fmt.Sprintf("human named %s %s of age %d living in %s",h.Firstname,h.Lastname,h.Age,h.Country)
}

Human 現在實現了 Stringer 介面:

package main

func main() {
    var john Human
    john.Firstname = "John"
    john.Lastname = "Doe"
    john.Country = "USA"
    john.Age = 45

    fmt.Println(john)
}

輸出:

human named John Doe of age 45 living in the USA

9.3 sort.Interface 介面

type Interface interface {
    Len() int
    Less(i, j int) bool
    Swap(i, j int)
}

通過在一個型別上實現 sort.Interface 介面,可以對一個型別的元素進行排序(通常,底層型別是一個切片)。

這是一個示例用法(來源:sort/example_interface_test.go):

type Person struct {
    Age int
}
// ByAge implements sort.Interface for []Person based on
// the Age field.
type ByAge []Person

func (a ByAge) Len() int           { return len(a) }
func (a ByAge) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
func (a ByAge) Less(i, j int) bool { return a[i].Age < a[j].Age }
  • ByAge 型別實現了 sort.Interface
    • 底層型別是 Person 的一個切片
  • 介面由三個方法組成:
    • Len() int:返回集合內的元素數
    • Less(i, j int) bool:如果索引 i 處的元素應該排在索引 j 處的元素之前,則返回 true
    • Swap(i, j int):交換索引 i & j 處的元素;換句話說,我們應該將位於索引 j 的元素放在索引 i 處,而位於索引 i 的元素應該放在索引 j 處。
      然後我們可以使用 sort.Sort 函式對 ByAge 型別的變數進行排序
func main() {
    people := []Person{
        {"Bob", 31},
        {"John", 42},
        {"Michael", 17},
        {"Jenny", 26},
    }

    sort.Sort(ByAge(people))
}

10 隱式實現

介面是隱式實現的。當你宣告一個型別時,你不必指定它實現了哪些介面。

11 PHP 和 JAVA

在其他語言中,你必須指定介面實現。
這是 Java 中的一個示例:

// JAVA
public class Cat implements DomesticAnimal{
    public void receiveAffection(){
        //...
    }
    public void giveAffection(){
        //..
    }
}

這是 PHP 中的另一個示例:

//PHP
<?php

class Cat implements DomesticAnimal {
    public function receiveAffection():void {
        // ...
    }
    public function giveAffection():void {
        // ...
    }
}
?>

你可以看到,在宣告實現介面的類時,必須新增關鍵字"implements"

你可能會問 Go 執行時如何處理這些隱式介面實現。我們將後面解釋介面值的機制。

12 空介面

Go 的空介面是你可以編寫的最簡單、體積更小的介面。它的方法集正好由 0 個方法組成。

interface{}

也就是說,每種型別都實現了空介面。你可能會問為什麼需要這麼無聊的空介面。根據定義,空介面值可以儲存任何型別的值。如果你想構建一個接受任何型別的方法,它會很有用。
讓我們從標準庫中舉一些例子。

  • log 包中,你有一個 Fatal 方法,可以將任何型別的輸入變數作為輸入:
func (l *Logger) Fatal(v ...interface{}) { }
  • fmt 包中,我們還有許多方法將空介面作為輸入。例如 Printf 函式:
func Printf(format string, a ...interface{}) (n int, err error) { }

12.1 型別轉換

接受空介面作為引數的函式通常需要知道其輸入引數的有效型別。
為此,該函式可以使用“型別開關”,這是一個 switch case 將比較型別而不是值。
這是從標準庫(檔案 runtime/error.go,包 runtime)中獲取的示例:

// printany prints an argument passed to panic.
// If panic is called with a value that has a String or Error method,
// it has already been converted into a string by preprintpanics.
func printany(i interface{}) {
    switch v := i.(type) {
    case nil:
        print("nil")
    case bool:
        print(v)
    case int:
        print(v)
    case int8:
        print(v)
    case int16:
        print(v)
    case int32:
        print(v)
    case int64:
        print(v)
    case uint:
        print(v)
    case uint8:
        print(v)
    case uint16:
        print(v)
    case uint32:
        print(v)
    case uint64:
        print(v)
    case uintptr:
        print(v)
    case float32:
        print(v)
    case float64:
        print(v)
    case complex64:
        print(v)
    case complex128:
        print(v)
    case string:
        print(v)
    default:
        printanycustomtype(i)
    }
}

image

12.2 關於空介面的使用

  • 你應該非常小心地使用空介面。
  • 當你別無選擇時,請使用空介面。
  • 空介面不會向將使用你的函式或方法的人提供任何資訊,因此他們將不得不參考文件,這可能會令人沮喪。

你更喜歡哪種方法?

func (c Cart) ApplyCoupon(coupon Coupon) error  {
    //...
}

func (c Cart) ApplyCoupon2(coupon interface{}) (interface{},interface{}) {
    //...
}

ApplyCoupon 方法嚴格指定它將接受和返回的型別。而 ApplyCoupon2 沒有在輸入和輸出中指定它的型別。作為呼叫方,ApplyCoupon2 的使用難度比 ApplyCoupon 大。

13 實際應用:購物車儲存

13.1 規則說明

你建立了一個電子商務網站;你必須儲存和檢索客戶購物車。必須支援以下兩種行為:

  1. 通過 ID 獲取購物車
  2. 將購物車資料放入資料庫

為這兩種行為提出一個介面。還要建立一個實現這兩個介面的型別(不要實現方法中的邏輯)。

13.2 答案

這是一個設計的介面:

type CartStore interface {
    GetById(ID string) (*cart.Cart, error)
    Put(cart *cart.Cart) (*cart.Cart, error)
}

實現介面的型別:

type CartStoreMySQL struct{}

func (c *CartStoreMySQL) GetById(ID string) (*cart.Cart, error) {
    // implement me
}

func (c *CartStoreMySQL) Put(cart *cart.Cart) (*cart.Cart, error) {
    // implement me
}

另一種實現介面的型別:

type CartStorePostgres struct{}

func (c *CartStorePostgres) GetById(ID string) (*cart.Cart, error) {
    // implement me
}

func (c *CartStorePostgres) Put(cart *cart.Cart) (*cart.Cart, error) {
    // implement me
}
  • 你可以為你使用的每個資料庫模型建立一個特定的實現
  • 新增對新資料庫引擎的支援很容易!你只需要建立一個實現介面的新型別。

14 為什麼要使用介面?

14.1 易於升級

當你在方法或函式中使用介面作為輸入時,你將程式設計為易於升級的。未來的開發人員(或未來的你)可以在不更改大部分程式碼的情況下建立新的實現。

假設你構建了一個執行資料庫讀取、插入和更新的應用程式。你可以使用兩種設計方法:

  1. 建立與你現在使用的資料庫引擎密切相關的型別和方法。
  2. 建立一個介面,列出資料庫引擎的所有操作和具體實現。
  • 在第一種方法中,你建立將特定實現作為引數的方法。
  • 通過這樣做,你將程式限制到一個實現。
  • 在第二種方法中,你建立接受介面的方法。
  • 改變實現就像建立一個實現介面的新型別一樣簡單。

14.2 提高團隊合作

團隊也可以從介面中受益。
在構建功能時,通常需要多個開發人員來完成這項工作。如果工作需要兩個團隊編寫的程式碼進行互動,他們可以就一個或多個介面達成一致。
然後,兩組開發人員可以處理他們的程式碼並使用商定的介面。他們甚至可以 mock 其他團隊的返回結果。通過這樣做,團隊不會被阻塞。

14.3 Benefit from a set of routines

在自定義型別上實現介面時,你可以不需要開發就使用的附加功能。讓我們從標準庫中舉一個例子:sort 包。這並不奇怪。這個包是用來進行排序的。這是 go 原始碼的摘錄:

// go v.1.10.1
package sort
//..

type Interface interface {
    // Len is the number of elements in the collection.
    Len() int
    // Less reports whether the element with
    // index i should sort before the element with index j.
    Less(i, j int) bool
    // Swap swaps the elements with indexes i and j.
    Swap(i, j int)
}

// Sort sorts data.
// It makes one call to data.Len to determine n, and O(n*log(n)) calls to
// data.Less and data.Swap. The sort is not guaranteed to be stable.
func Sort(data Interface) {
    n := data.Len()
    quickSort(data, 0, n, maxDepth(n))
}

在第一行,我們宣告當前包:sort。在接下來的幾行中,程式設計師宣告瞭一個名為 Interface 的介面。這個介面 Interface 指定了三個方法:Len、Less、Swap

在接下來的幾行中,函式 Sort 被宣告。它將介面型別 data 作為引數。這是一個非常有用的函式,可以對給定的資料進行排序。

我們如何在我們的一種型別上使用這個函式?實現介面

假設你有一個 User 型別:

type User struct {
    firstname string
    lastname string
    totalTurnover float64
}

還有一個型別 Users ,它是 User 型別切片:

type Users []User

讓我們建立一個 Users 例項並用三個 User 型別的變數填充它:

user0 := User{firstname:"John", lastname:"Doe", totalTurnover:1000}
user1 := User{firstname:"Dany", lastname:"Boyu", totalTurnover:20000}
user2 := User{firstname:"Elisa", lastname:"Smith Brown", totalTurnover:70}

users := make([]Users,3)
users[0] = user0
users[1] = user1
users[2] = user2

如果我們想按營業額排序怎麼辦?我們可以從頭開始開發符合我們規範的排序演算法。或者我們可以只實現使用 sort 包中的內建函式.Sort 所需的介面。我們開始吧:

// Compute the length of the array. Easy...
func (users Users) Len() int {
  return len(users)
}

// decide which instance is bigger than the other one
func (users Users) Less(i, j int) bool {
  return users[i].totalTurnover < users[j].totalTurnover
}

// swap two elements of the array
func (users Users) Swap(i, j int) {
    users[i], users[j] = users[j], users[i]
}

通過宣告這些函式,我們可以簡單地使用 Sort 函式:

sort.Sort(users)
fmt.Println(users)
// will output :
[{Elisa Smith Brown 70} {John Doe 1000} {Dany Boyu 20000}]

15 一點建議

  1. 儘量使用標準庫提供的介面
  2. 方法太多的介面很難實現(因為它需要編寫很多方法)。

16 隨堂測試

16.1 問題

  1. 舉一個介面嵌入另一個介面的例子。
  2. 判斷真假。嵌入介面中指定的方法不是介面方法集的一部分。
  3. 說出使用介面的兩個優點。
  4. 介面型別的零值是多少?

16.2 答案

  1. 舉一個介面嵌入另一個介面的例子。
type ReadWriter interface {
    Reader
    Writer
}
  1. 判斷真假。嵌入介面中指定的方法不是介面方法集的一部分。
    錯。介面的方法集是由兩個部分組成:
    1. 直接指定到介面中的方法
    2. 來自嵌入介面的方法
  2. 說出使用介面的兩個優點。
    1. 輕鬆地在開發人員之間拆分工作:
      1.定義介面型別
      2.一個人開發介面的實現
      3.另一個人可以在其功能中使用介面型別
      4.兩個人可以互不干擾地工作。
    2. 易於升級
      1.當你建立一個介面時,你就建立了一個契約
      2.不同的實現可以履行這個契約。
      3.在一個專案的開始,通常有一個實現
      4.隨著時間的推移,可能需要另一種實現方式。
  3. 介面型別的零值是多少?
    nil

17 關鍵要點

  • 介面就是契約
  • 它指定方法(行為)而不實現它們。
type Cart interface {
    GetById(ID string) (*cart.Cart, error)
    Put(cart *cart.Cart) (*cart.Cart, error)
}
  • 介面是一種型別(就像structs, arrays, maps,等)
  • 我們將介面中指定的方法稱為介面的方法集。
  • 一個型別可以實現多個介面。
  • 無需明確型別實現了哪個介面
    • 與其他需要宣告它的語言(PHP、Java 等)相反
  • 一個介面可能嵌入到另一個介面中;在這種情況下,嵌入的介面方法被新增到介面中。
  • 介面型別可以像任何其他型別一樣使用
  • 介面型別的零值為 nil
  • 任何型別實現空介面 interface{}
  • 空介面指定了 0 個方法
  • 要獲取空介面的具體型別,您可以使用 type switch:
switch v := i.(type) {
    case nil:
        print("nil")
    case bool:
        print(v)
    case int:
        print(v)
}
  • 當我們可以通過各種方式實現一個行為時,我們或許可以建立一個介面。
    • 例如:儲存(我們可以使用 MySQL、Postgres、DynamoDB、Redis 資料庫來儲存相同的資料)

相關文章