www.zhblog.net

接口入门


在〈结构与继承〉的最后讨论到了多态,倘若现在需要有个函数,可以接受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 中,接口类型与行为是分开的,应该重视的只有行为本身,本质上与动态定型语言中只重行为而非类型相同,因此「如何知道某个接口的实现类型有哪些?」、「这个类型实现了哪些接口?」这类问题也就不重要了!


展开阅读全文

评论

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 心情