Go 语言中的接口

Go 语言中设计最精妙的部分是 interface。

有人甚至将 interface 翻译为“界面”而非“接口”,可见 Go 语言中的接口独树一帜,与 C++ 颇不一样。

据说,Go语言的诞生,是三个有很强个性的设计师共同完成的。Go语言的定位,就象三维坐标系中的一个点,在强类型、动态和并发这三个特性维度上,分别代表了Ken、Robert和Rob三人的创造思维的投影。

与 C++、Java 一样,Go 的变量类型是安全高效的强静态类型,“强类型”这一点容易理解;“并发”是 Go 的一大特性以后再说;何来“动态”呢?在 Python、Ruby 这类动态语言中有动态类型,对象可以根据提供的方法被处理,而忽略它们的实际类型,Go 也有这种功力吗?

《Go 语言中的面向对象》一文中讲过,Go 没有明显的继承,而是用 Has-A 替代 Is-A 的实现的。

如果一只鸟走起来像鸭子,叫起来像鸭子,那它就是鸭子 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package main

import "fmt"

type IDuck interface {
Quack()
Walk()
}

func DuckDance(duck IDuck) {
for i := 1; i <= 3; i++ {
duck.Quack()
duck.Walk()
}
}

type Bird struct {
// ...
}

func (b *Bird) Quack() {
fmt.Println("I am quacking!")
}

func (b *Bird) Walk() {
fmt.Println("I am walking!")
}

func main() {
b := new(Bird)
DuckDance(b)
}

遵循这个理念,Go 使用接口规避了传统面向对象的继承(似乎传统继承有不少缺点,关于继承和组合有颇多讨论),使得这种静态语言表现出非常灵活的动态性。

具体来看,接口是一组 method 签名的组合,我们通过接口来定义对象的一组行动。比较方便的是,任何提供了接口方法实现代码的类型都隐式地实现了该接口,而不用显式地声明。同时,任何类型都实现了空接口(interface{}),因为空接口不包含任何 method。

1
2
3
4
5
6
7
// 定义a为空接口
var a interface{}
var i int = 5
s := "Hello world"
// a可以存储任意类型的数值
a = i
a = s

顺着这个思路,如果一个函数把interface{}作为参数,那么他可以接受任意类型的值作为参数,如果一个函数返回interface{},那么也就可以返回任意类型的值。

当然,非空接口也可作为函数参数,试看一例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main
import (
"fmt"
"strconv"
)

type Human struct {
name string
age int
phone string
}

// 通过这个方法 Human 实现了 fmt.Stringer
func (h Human) String() string {
return "❰"+h.name+" - "+strconv.Itoa(h.age)+" years - ✆ " +h.phone+"❱"
}

func main() {
Bob := Human{"Bob", 39, "000-7777-XXX"}
fmt.Println("This Human is : ", Bob)
}

fmt.Println 可以直接输出 Bob,这是因为 Bob 实现了 String() 也就实现了接口 fmt.Stringer

1
2
3
type Stringer interface {
String() string
}

而 fmt.Println 的函数参数正是 Stringer 接口!

不仅如此,Go 的接口还有一个吸引人的地方,它支持匿名字段。就像 struct 的匿名字段一样,带有匿名字段的接口竟包含了另一个接口,那么这个接口同时包含了另一个接口的方法!看源码会发现有大量这样的例子,比如

1
2
3
4
5
// io.ReadWriter
type ReadWriter interface {
Reader
Writer
}

io 包下面的 io.ReadWriter 包含了 io 包里的 Reader 和 Writer 两个接口。我们发现,这种嵌入接口的机制使得语言又灵活了不少。

在灵活的同时,Go 还能保证减少编译出错,因为我们可以反向知道接口变量里实际保存了哪些类型,这便是 Go 的 Comma-ok 断言和 switch 测试。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
for index, element := range list {
if value, ok := element.(int); ok {
fmt.Printf("list[%d] is an int and its value is %d\n", index, value)
} else if value, ok := element.(string); ok {
fmt.Printf("list[%d] is a string and its value is %s\n", index, value)
} else if value, ok := element.(Person); ok {
fmt.Printf("list[%d] is a Person and its value is %s\n", index, value)
} else {
fmt.Printf("list[%d] is of a different type\n", index)
}
}

for index, element := range list{
switch value := element.(type) {
case int:
fmt.Printf("list[%d] is an int and its value is %d\n", index, value)
case string:
fmt.Printf("list[%d] is a string and its value is %s\n", index, value)
case Person:
fmt.Printf("list[%d] is a Person and its value is %s\n", index, value)
default:
fmt.Println("list[%d] is of a different type", index)
}
}

并且,Go 语言有反射机制,可以检查程序在运行时的状态。这简直是在说,一个聪明灵活的人,还非常善于反省。

1
2
3
4
5
6
7
8
9
func show(i interface{}) {
switch t:= i.(type) {
case *Person:
t := reflect.TypeOf(i)
v := reflect.ValueOf(i)
tag := t.Elem().Field(0).Tag
name := t.Elem().Field(0).String()
}
}