結構組合


結構本身用來組織相關資料,可以將處理結構的相關函式定義為方法,類似物件導向程式語言中,使用類別定義值域與方法,那麼繼承呢?Go 語言並非以物件導向為主要典範的語言,沒有繼承的概念,不過可以使用組合代替繼承。

在組告之前

在〈結構與方法〉中使用 struct 定義了 Account,如果今天你想定義一個支票帳戶,方式之一是…

type CheckingAccount struct {
    id string
    name string
    balance float64
    overdraftlimit float64
}

這是個很尋常的作法,也許你想將 idnamebalance 組織在一起:

package main

import "fmt"

type CheckingAccount struct {
    account struct {
        id      string
        name    string
        balance float64
    }
    overdraftlimit float64
}

func main() {
    checking := CheckingAccount{}
    checking.account = struct {
        id      string
        name    string
        balance float64
    }{"1234-5678", "Justin Lin", 1000}
    checking.overdraftlimit = 30000

    fmt.Println(checking)                // {{1234-5678 Justin Lin 1000} 30000}
    fmt.Println(checking.account)        // {1234-5678 Justin Lin 1000}
    fmt.Println(checking.account.name)   // Justin Lin
    fmt.Println(checking.overdraftlimit) // 30000
}

這是一種方式,不過使用起來麻煩,或許你可以這麼做:

package main

import "fmt"

type Account struct {
    id      string
    name    string
    balance float64
}

type CheckingAccount struct {
    account        Account
    overdraftlimit float64
}

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

    fmt.Println(checking)                // {{1234-5678 Justin Lin 1000} 30000}
    fmt.Println(checking.account)        // {1234-5678 Justin Lin 1000}
    fmt.Println(checking.account.name)   // Justin Lin
    fmt.Println(checking.overdraftlimit) // 300000
}

看來還不錯,不過,如果想要 fmt.Println(checking.name) 就能取得名稱的話,這種寫法行不通!

結構值域的查找

在定義結構時,可以將另一已定義的結構直接內嵌:

package main

import "fmt"

type Account struct {
    id      string
    name    string
    balance float64
}

type CheckingAccount struct {
    Account
    overdraftlimit float64
}

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

    fmt.Println(account)                // {{1234-5678 Justin Lin 1000} 30000}
    fmt.Println(account.id)             // 1234-5678
    fmt.Println(account.name)           // Justin
    fmt.Println(account.balance)        // 1000
    fmt.Println(account.overdraftlimit) // 30000
}

這稱為型態內嵌(type embedding),Account 被稱為 CheckingAccount 的內部型態,反之,CheckingAccountAccount 的外部型態,雖然是透過 account.idaccount.nameaccount.balance 來存取,不過內部型態提昇,令內部型態定義的值域為可見。

那麼,如果想要明確地透過 Account 的結構來存取呢?也是可以的:

package main

import "fmt"

type Account struct {
    id      string
    name    string
    balance float64
}

type CheckingAccount struct {
    Account
    overdraftlimit float64
}

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

    fmt.Println(account)                 // {{1234-5678 Justin Lin 1000} 30000}
    fmt.Println(account.Account.id)      // 1234-5678
    fmt.Println(account.Account.name)    // Justin
    fmt.Println(account.Account.balance) // 1000
    fmt.Println(account.overdraftlimit)  // 30000
}

雖然內部型態會提昇,然而,若外部型態中定義了同名值域,就會直接取得外部型態的值域,因此,如果 CheckingAccount 定義了相同的值域 balance,如果透過 account.balance,結果會是找到 CheckingAccount 定義的 balance,如果想明確找到 Accountbalance,可以指定 Account 作為前置:

package main

import "fmt"

type Account struct {
    id      string
    name    string
    balance float64
}

type CheckingAccount struct {
    Account
    balance        float64
    overdraftlimit float64
}

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

    fmt.Println(account.balance)         // 2000
    fmt.Println(account.Account.balance) // 1000
}

無論是結構值域或是方法,若來自兩個結構的值域或方法產生了同名衝突,Go 會有 ambiguous selector 的錯誤提示,此時你必須明確指定結構名稱,指定使用來自哪個結構的值域或方法。

方法的查找

如果內部型態原本定義了方法,這些方法也是查找時的對象:

package main

import (
    "errors"
    "fmt"
)

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 main() {
    account := CheckingAccount{Account{"1234-5678", "Justin Lin", 1000}, 30000}
    account.Deposit(2000)
    account.Withdraw(500)
    fmt.Println(account) // {{1234-5678 Justin Lin 2500} 30000}
}

類似地,若外部型態中定義了同名的方法,那麼就會使用該方法,這類似重新定義(Override)的概念:

package main

import (
    "errors"
    "fmt"
)

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() {
    account := CheckingAccount{Account{"1234-5678", "Justin Lin", 1000}, 30000}
    account.Deposit(2000)
    if err := account.Withdraw(50000); err != nil {
        fmt.Println(err)
    } else {
        fmt.Println(account)
    }
}

在上面的範例中,會顯示「超出信用額度」的訊息,拿掉 func (account *CheckingAccount) Withdraw(amount float64) 該函式的定義,則會顯示「餘額不足」的訊息。

如果想指定使用 AccountWithdraw 函式,也還是可以的:

func main() {
    account := CheckingAccount{Account{"1234-5678", "Justin Lin", 1000}, 30000}
    account.Deposit(2000)
    if err := account.Account.Withdraw(50000); err != nil {
        fmt.Println(err)
    } else {
        fmt.Println(account)
    }
}

雖然可以實現方法重新定義的概念,不過,單純只是如上定義的話,並不支援多型的概念,因為一開始這麼指定就會出錯了:

// cannot use CheckingAccount literal (type CheckingAccount) as type Account in assignment
var account Account = CheckingAccount{Account{"1234-5678", "Justin Lin", 1000}, 30000}

若想實作出多型的概念,必須使用 interface,這在之後的文件會加以說明。