Golang 语法之接口

2018-03-30 language golang

在 Go 语言的实际编程中,几乎所有的数据结构都围绕 Interface 展开,这是 GoLang 中所有数据结构的核心。

首先,Go 不是一种典型的 OO 语言,它在语法上不支持类和继承的概念,而通过 Interface 可以看到多态的影子。

简介

Interface 定义了方法集,只要某个类型实现了该接口的超集(实现了接口定义的所有方法,而且可能还有其它方法),那么就可以说这个类型实现了该接口。

这意味着,一个类型可能实现了多个接口,例如,所有的类型都实现了 interface {} 这个接口。

Interface 会在编译时检查,在运行时动态生成,如果有类似类型的报错,在编译阶段就可以发现,而不像 Python 会在运行时报错。

上面说的 interface {} 比较容易混淆,它定义的是空接口,所有的类型都实现了该接口,在函数传参定义类型时,可以认为是 C 语言中的 void *,可接收任意类型的参数。

简单示例

一般通过如下的规则判断一个类型或者指针是否实现了该接口:

  • 类型 *T 的对象可调用方法集含类型为 *TT 的所有方法集。
  • 类型 T 的对象可调用方法集含类型为 T 的所有方法集。

同时也可以得出一条推论:

  • 类型 T 的对象不能调用方法集类型为 *T 的方法集。
package main

import (
    "log"
)

type Notifier interface {
    Notify() error
}

type User struct {
    Name  string
    Email string
}

func (u *User) Notify() error {
    log.Printf("User: Sending User Email To %s<%s>\n", u.Name, u.Email)
    return nil
}

func SendNotification(notify Notifier) error {
    return notify.Notify()
}

func main() {
    user := User{
        Name:  "FooBar",
        Email: "foobar@example.com",
    }

    SendNotification(&user)
}

如果将 SendNotification(&user) 替换为 SendNotification(user) 将会报错。

理解接口

首先 Interface 是一种类型,可以参考上述示例的定义方法。

type Notifier interface {
    Notify() error
}

它的定义可以看出用了 type 关键字,更准确的说 interface 是一种具有一组方法的类型,这些方法定义了 interface 的行为。

允许不带任何方法的 interface ,称为 Empty Interface,也就是上述的 interface {}

在 go 中没有显式的关键字用来实现 interface,只需要实现 interface 包含的方法即可。

变量存储

interface 变量存储的是实现者的值

package main

import (
    "log"
)

// #1
type Notifier interface {
    Notify() error
}

type User struct {
    Name  string
    Email string
}

// #2
func (u *User) Notify() error {
    log.Printf("User: Sending User Email To %s<%s>\n", u.Name, u.Email)
    return nil
}

func SendNotification(notify Notifier) error {
    return notify.Notify()
}

func main() {
    user := User{
        Name:  "FooBar",
        Email: "foobar@example.com",
    }

    SendNotification(&user)
}

同样复制上述的示例,其中 #1 定义了接口,#2 实现了接口中定义的方法,也就是说 User 是接口 Notifier 的实现,接着通过 SendNotification(&user) 完成了一次对接口类型的使用。

其比较重要的用途就在 SendNotification() 函数中,如果有多种类型实现了这个接口,这些类型的值都可以直接使用接口的变量存储。

user := User{}
var n Notifier      // 声明一个接口对象
n = &user           // 将对象赋值到接口变量
SendNotification(n) // 调用该类型变量实现的接口

也就是说,接口变量中存储的是实现了该接口类型的对象值,这种能力称为 duck typing

在使用接口时不需要显式声明要实现哪个接口,只需要实现对应接口中的方法即可,go 会自动进行检查,并在运行时完成从其他类型到接口类型的自动转换。

即使实现了多个接口,go 也会在使用对应接口时实现自动转换,这就是接口的魔力所在。

类型判断

如上,如果有多种类型实现了 Notifier 这个接口,那么在调用接口时,如何判断接口变量保存的究竟时那种类型的实现。

此时可以使用 comma, ok 的形式做区分,也就是 value, ok := em.(T),其中 em 是接口类型的变量,T 代表要判断的类型,value 是接口变量存储的值,ok 返回是否类型 T 的实现。

例如,上述的示例可以修改为。

func SendNotification(notify Notifier) error {
    if n, ok := notify.(*User); ok {
        log.Printf("User implements Notifier %+v\n", n)
    }
    return notify.Notify()
}

oktrue 表明 notify 存储的是 *User 类型的值,false 则不是,这种区分能力叫 Type assertions (类型断言)。

如果需要区分多种类型,可以使用 switch 语句,如下,其中 Foo 未定义会报错。

func SendNotification(notify Notifier) error {
    switch n := notify.(type) {
        case *User:
            log.Printf("notify store *User, %+v", n)
        case *Foo:
            log.Printf("notify store *Foo, %+v", n)
    }
    return notify.Notify()
}

空接口

interface{} 是一个空的接口类型,如前所述,可以认为所有的类型都实现了 interface{},那么如果定义一个函数参数是 interface{} 类型,这个函数可以接受任何类型作为它的参数。

其它

如果实现的类型不一致,那么在如下的调用时同样会报错。

package main

import (
    "fmt"
)

type Animal interface {
    Speak() string
}

type Dog struct {}
// #2 func (d Dog) Speak() string {
func (d *Dog) Speak() string {
    return "Woof!"
}

type Cat struct {}
// #2 func (c Cat) Speak() string {
func (c *Cat) Speak() string {
    return "Meow!"
}

func main() {
    // #2 animals := []Animal{Dog{}, Cat{}}
    animals := []Animal{&Dog{}, &Cat{}}
    for _, animal := range animals {
        fmt.Println(animal.Speak())
    }
}

可以保持现状,或者都修改为 #2 的方式,但是如果有不一致的,将会报错。

Receiver 类型

如果将上述 SendNotification(&user) 改为 SendNotification(user),执行时会报如下的错。

cannot use user (type User) as type Notifier in argument to SendNotification:
        User does not implement Notifier (Notify method has pointer receiver)

上述报错的大致意思是说,User 没有实现 Notifier ,这里的关键是 UserNotify() 方法的 Receiver 是个指针 *User

接口的定义并没有严格规定实现者的方法 Receiver 是个 Value Receiver 还是 Pointer Receiver,不过如果定义为 Pointer 而使用 Value ,那么会导致报错。

与之相关可以参考 Pointers vs. Values ,关键信息为:

The rule about pointers vs. values for receivers is that value methods can be invoked on pointers and values, but pointer methods can only be invoked on pointers.

This rule arises because pointer methods can modify the receiver; invoking them on a value would cause the method to receive a copy of the value, so any modifications would be discarded. The language therefore disallows this mistake.

也就是说,value method 可以被 pointer 或者 value 对象调用,而 pointer method 只能被 pointer 对象调用。

示例

那么,反过来会怎样,如果 Receiver 是 Value,函数用 Pointer 的形式调用?

package main

import (
    "log"
)

type User struct {
    Name  string
    Email string
}

func (u User) Notify() error {
    log.Printf("User: Sending User Email To %s<%s>\n", u.Name, u.Email)
    return nil
}

type Notifier interface {
    Notify() error
}

func SendNotification(notify Notifier) error {
    return notify.Notify()
}

func main() {
    user := User{
        Name:  "AriesDevil",
        Email: "ariesdevil@xxoo.com",
    }

    SendNotification(user)
    SendNotification(&user)
}

从执行代码可以看到无论是 Pointer 还是 Value 都可以正确执行。

原因

在调用某个对象的函数时,都会复制一份,包括了指针以及指,如果在函数中有修改对象中保存的值,那么指针对应的值会同步修改,而值因为是复制了一份,那么实际修改的是复制的值,并不会修改原来的值。

传值会很容易出错,而且非常难排查,当然,这可以作为语言特性的一部分,但是为了防止出错,将这部分功能作为异常。

比较

何时使用 Pointer

比较常见的有几个场景:

  1. 修改接收器中的成员,值传递会复制一份数据,如果修改实际操作的是复制后的对象;
  2. 如果结构体比较大,那么复制过程的成本会比较高。

何时使用 Value

  • 不需要编辑接收器值;
  • 值接收器是并发安全的,而指针接收器不是并发安全的。

如果某个接收器已经存在了指针,为了统一,最好是统一使用指针。

其它

  1. 如果是 map func chan 则不需要指针,而切片只有在需要修改切片时再使用指针;
  2. 当接收器中存在类似 sync.Mutex 同步字段时,需要使用指针;

参考