介面入門


在〈結構與繼承〉的最後討論到了多型,倘若現在需要有個函式,可以接受 AccountCheckingAccount 實例,或者是有個陣列或 slice,可以收集 AccountCheckingAccount實例,那該怎麼辦呢?

介面定義行為

在 Go 語言中,可以使用 interface 定義行為,舉例來說,若現在想要定義儲蓄的行為,可以如下:

type Savings interface {
    Deposit(amount float64)
    Withdraw(amount float64) error
}

注意,不必使用 func 關鍵字,也不用宣告接受者型態,只需要定義行為的名稱、參數與傳回值。接著該怎麼實現這個介面呢?實際上,就〈結構與繼承〉,已經實現了這個介面,也就是說,結構上不用任何關鍵字,只要有函式實現這兩個行為就可以了。

因此,現在可以寫個函式,同時接受 AccountCheckingAccount 實例,在提款後顯示餘額:

package main

import (
    "errors"
    "fmt"
)

type Savings interface {
    Deposit(amount float64)
    Withdraw(amount float64) error
}

type Account struct {
    id      string
    name    string
    balance float64
}

func (ac *Account) Deposit(amount float64) {
    if amount <= 0 {
        panic("必須存入正數")
    }
    ac.balance += amount
}

func (ac *Account) Withdraw(amount float64) error {
    if amount > ac.balance {
        return errors.New("餘額不足")
    }
    ac.balance -= amount
    return nil
}

type CheckingAccount struct {
    Account
    overdraftlimit float64
}

func (ac *CheckingAccount) Withdraw(amount float64) error {
    if amount > ac.balance+ac.overdraftlimit {
        return errors.New("超出信用額度")
    }
    ac.balance -= amount
    return nil
}

func Withdraw(savings Savings) {
    if err := savings.Withdraw(500); err != nil {
        fmt.Println(err)
    } else {
        fmt.Println(savings)
    }
}

func main() {
    account1 := Account{"1234-5678", "Justin Lin", 1000}
    account2 := CheckingAccount{Account{"1234-5678", "Justin Lin", 1000}, 30000}
    Withdraw(&account1) // 顯示 &{1234-5678 Justin Lin 500}
    Withdraw(&account2) // 顯示 &{{1234-5678 Justin Lin 500} 30000}
}

雖然沒有定義接收者為 *CheckingAccountDeposit 方法,然而,作為內部型態的 Account 有定義 Deposit(並且沒有使用到 CheckingAccount 定義的值域),這個實現被提昇至外部型態,也就滿足了 Savings 要求的行為規範。

注意!由於在實作 WithdrawDeposit 方法時,都是用指標 (ac *Account)(ac *CheckingAccount) 宣告了接受者型態,因此傳遞實例給 func Withdraw(savings Savings) 時,也就必須傳遞指標。

如果在實作WithdrawDeposit 方法時,是使用 (ac Account)(ac CheckingAccount) 宣告了接受者型態,那麼傳遞實例給接受 Savings 的函式時,就可以不用取指標,例如:

package main

import (
    "errors"
    "fmt"
)

type Savings interface {
    Deposit(amount float64)
    Withdraw(amount float64) error
}

type Account struct {
    id      string
    name    string
    balance float64
}

func (ac Account) Deposit(amount float64) {
    if amount <= 0 {
        panic("必須存入正數")
    }
    ac.balance += amount
}

func (ac Account) Withdraw(amount float64) error {
    if amount > ac.balance {
        return errors.New("餘額不足")
    }
    ac.balance -= amount
    return nil
}

type CheckingAccount struct {
    Account
    overdraftlimit float64
}

func (ac CheckingAccount) Withdraw(amount float64) error {
    if amount > ac.balance+ac.overdraftlimit {
        return errors.New("超出信用額度")
    }
    ac.balance -= amount
    return nil
}

func Withdraw(savings Savings) {
    if err := savings.Withdraw(500); err != nil {
        fmt.Println(err)
    } else {
        fmt.Println(savings)
    }
}

func main() {
    account1 := Account{"1234-5678", "Justin Lin", 1000}
    account2 := CheckingAccount{Account{"1234-5678", "Justin Lin", 1000}, 30000}
    Withdraw(account1) // 顯示 {1234-5678 Justin Lin 1000}
    Withdraw(account2) // 顯示 {{1234-5678 Justin Lin 1000} 30000}
}

當然,就這個例子來說,結果並不是正確的,就算改成 Withdraw(&account1)&Withdraw(account2),也不會是正確的結果,因為就 WithdrawDeposit 的接收者來說,會是複製結構的值域,而不是修改原結構實例的值域,這純綷只是示範。

介面實例的型態與值

如果你定義了一個變數:

var savings Savings

那麼 savings 變數儲存了什麼?技術上來說,savings 變數儲存兩個資訊:型態與值。就方才的savings 被指定為 nil 來說,代表著 savings 在底層儲存的型態為 nil,而值沒有指定,這樣的介面實例稱為 nil interface,因為沒有型態資訊,也就不能透過 nil interface 呼叫方法。

如果接收者是定義為 (ac *Account),而且有底下的程式,那麼 savings 底層儲存的型態會 *Account,而值是 Account 結構實例的位址值:

var savings Savings = &Account{"1234-5678", "Justin Lin", 1000}

當接收者是指標時,透過介面比對是否為 nil 時要留意,例如以下會是 true,這是因為 savings 在底層儲存的型態為 nil,而值沒有指定,介面宣告的變數只有在這個情況下,跟 nil 直接相等比較才會是 true

var savings Savings = nil
fmt.Println(savings == nil)

然而以下會是 false,這是因為 savings 在底層儲存的型態為 *Account,而值是 nil
這時透過 savings 是可以呼叫方法的,接收者會是 nil,就看你要不要在方法中處理 nil 了):

var acct *Account = nil
var savings Savings = acct
fmt.Println(savings == nil)

這是個 FAQ 了,在〈Why is my nil error value not equal to nil?〉就提到了個例子:

func returnsError() error {
    var p *MyError = nil
    if bad() {
        p = ErrBad
    }
    return p
}

如果對 returnsError 傳回值進行 nil 比較,結果會是 false

fmt.Println(returnsError() == nil) // false

因此如果傳回型態是個介面,值會是 nil,請記得直接傳 nil

func returnsError() error {
    if bad() {
        return ErrBad
    }
    return nil // 直接傳 nil
}

如果接收者是定義為 (ac Account),而你有底下的程式:

var savings Savings = Account{"1234-5678", "Justin Lin", 1000}

這時 savings 在底層會儲存型態 Account,而值為結構實例,這時透過 Savings 來進行實例的指定時,底層也會是結構實例的指定,因此會發生複製:

var savings1 Savings = Account{"1234-5678", "Justin Lin", 1000}
var savings2 Savings = savings1

savings2.name = "Monica Huang"
fmt.Println(savings.name) // Justin Lin

異質陣列或 slice

Go 語言會檢查類型的實例,是否實現了介面中規範的行為,若是的話,就可以使用介面型態來接受不同型態實例的指定,因此,若要建立一個異質陣列或 slice,也是可以的:

package main

import (
    "errors"
    "fmt"
)

type Savings interface {
    Deposit(amount float64)
    Withdraw(amount float64) error
}

type Account struct {
    id      string
    name    string
    balance float64
}

func (ac *Account) Deposit(amount float64) {
    if amount <= 0 {
        panic("必須存入正數")
    }
    ac.balance += amount
}

func (ac *Account) Withdraw(amount float64) error {
    if amount > ac.balance {
        return errors.New("餘額不足")
    }
    ac.balance -= amount
    return nil
}

type CheckingAccount struct {
    Account
    overdraftlimit float64
}

func (ac *CheckingAccount) Withdraw(amount float64) error {
    if amount > ac.balance+ac.overdraftlimit {
        return errors.New("超出信用額度")
    }
    ac.balance -= amount
    return nil
}

func main() {
    savingsArray := [...]Savings{
        &Account{"1234-5678", "Justin Lin", 1000},
        &CheckingAccount{Account{"1234-5678", "Justin Lin", 1000}, 30000},
    }

    for _, savings := range savingsArray {
        fmt.Println(savings)
    }

    savingsSlice := []Savings{
        &Account{"1234-5678", "Justin Lin", 1000},
        &CheckingAccount{Account{"1234-5678", "Justin Lin", 1000}, 30000},
    }

    for _, savings := range savingsSlice {
        fmt.Println(savings)
    }
}

在這邊雖然是以 AccountCheckingAccount 為例,不過,只要實現了 Savings 的行為,就算是一隻鴨子,也是可以的:

package main

import "fmt"

type Savings interface {
    Deposit(amount float64)
    Withdraw(amount float64) error
}

type Duck struct{}

func (d *Duck) Deposit(amount float64) {
    fmt.Println("我是一隻鴨子,我沒帳戶")
}

func (d *Duck) Withdraw(amount float64) error {
    fmt.Println("我是一隻鴨子,我沒錢")
    return nil
}

func main() {
    duckArray := [...]Savings{
        &Duck{},
        &Duck{},
    }

    for _, duck := range duckArray {
        duck.Deposit(1000)
    }

    duckSlice := []Savings{
        &Duck{},
        &Duck{},
    }

    for _, duck := range duckSlice {
        duck.Withdraw(500)
    }
}

空介面

那麼,如果想要建立一個實例容器,可以收集各種類型的實例,要怎麼做呢?答案就是透過空介面,也就是沒有定義任何行為的 interface {}

package main

import "fmt"

type Duck struct{}

func main() {
    instances := [](interface{}){
        &Duck{},
        [...]int{1, 2, 3, 4, 5},
        map[string]int{"caterpillar": 123456, "monica": 54321},
    }

    for _, instance := range instances {
        fmt.Println(instance)
    }
}

如果你查看 fmt.Println 的文件說明,可以發現,它的參數類型就是 interface {}

func Print(a ...interface{}) (n int, err error)
func Printf(format string, a ...interface{}) (n int, err error)
func Println(a ...interface{}) (n int, err error)

順便一提的是,就目前來說,在使用 fmt.Println 顯示結構時,都是使用預設的字串格式,如果想自訂字串格式,必須實現 Stringer 這個介面,這定義在 fmt 的 print.go 之中:

type Stringer interface {
        String() string
}

在需要字串的場合中,會呼叫 String() 方法。例如,若你想要帳號顯示時,可以出現 Account 或 CheckingAccount 字樣的話,可以如下實作:

package main

import "fmt"

type Account struct {
    id      string
    name    string
    balance float64
}

func (ac *Account) String() string {
    return fmt.Sprintf("Account(id = %s, name = %s, balance = %.2f)",
        ac.id, ac.name, ac.balance)
}

type CheckingAccount struct {
    Account
    overdraftlimit float64
}

func (ac *CheckingAccount) String() string {
    return fmt.Sprintf("CheckingAccount(id = %s, name = %s, balance = %.2f, overdraftlimit = %.2f)",
        ac.id, ac.name, ac.balance, ac.overdraftlimit)
}

func main() {
    account1 := Account{"1234-5678", "Justin Lin", 1000}
    account2 := CheckingAccount{Account{"1234-5678", "Justin Lin", 1000}, 30000}

    // 顯示 Account(id = 1234-5678, name = Justin Lin, balance = 1000.00)
    fmt.Println(&account1)

    // 顯示 CheckingAccount(id = 1234-5678, name = Justin Lin, balance = 1000.00, overdraftlimit = 30000.00)
    fmt.Println(&account2)
}

實作某介面的型態有哪些?

來自 Java 之類語言的開發者,在認識 Go 的 interface 後可能會有些疑問,像是「如何知道某個介面的實現型態有哪些?」、「這個型態實現了哪些介面?」…並且會想在文件上尋找這類資訊,因為 Java 的文件中,會記錄某介面的實現類別有哪些。

這是因為 Java 中,介面型態與行為是結合在一起的。

在 Go 中不需要記錄這些,當開發者看到某 API 上定義可以接收某介面型態的值時,應該看看該介面定義了哪些行為,接著看看要傳入的值是否有實作這些行為,這樣就可以了,因為 Go 的介面重點是「行為」,不管 API 上定義的介面型態是什麼,只要行為符合都可以傳入。

也就是說 Go 中,介面型態與行為是分開的,應該重視的只有行為本身,本質上與動態定型語言中只重行為而非型態相同,因此「如何知道某個介面的實現型態有哪些?」、「這個型態實現了哪些介面?」這類問題也就不重要了!