Go语言interface的介绍

Interface 是Go语言里很优秀的一个设计,它本身是仅仅是一个结构体,但是通过interface我们可以实现面向对象的很多特性,比如多态、动态联编等。interface 在Go 语言里使用起来的感觉,相比于其它语言也要优雅得多。

底层实现

Interface 的底层只是一个简单的由c语言实现的数据结构:

1
2
3
4
5
struct Eface
{
Type* type;
void* data;
};

type 表示它的类型,data 里存储了它的具体信息。对于Go语言,任何数据类型都是可以由interface{}来表示的。当我们使用时可以通过反射把里面的信息取出来。

方法实现

我们先定义一个简单的接口,类似于Java 中的那样。

1
2
3
4
type Animal interface {
Run()
Roar()
}

定义一个结构体 Dog ,直接定义Run 和 Roar 方法,即可实现接口。

1
2
3
4
5
6
7
8
9
10
11
12
type Dog struct {
Name string
Size string
}
func (d *Dog)Run() {
fmt.Printf("Dog %s run!\n", d.Name)
}
func (d *Dog)Roar() {
fmt.Printf("A %s dog roar!\n", d.Size)
}

声明一个Dog 类型并初始化。

1
2
3
4
5
6
7
8
9
10
11
func main () {
d := Dog{
Name: "Jim",
Size: "big",
}
d.Run()
d.Roar()
}
// output:
// Dog Jim run!
// A big dog roar!

这样实现一个接口的方法很简单,只需要在定义某一类型的时候,定义一个同名的方法即可。

多态

我们可以直接声明一个该接口的变量,再让这个变量指向实现其方法类型的实例的地址,这个变量就可以调用此实例的方法。

1
2
3
4
5
6
7
8
9
10
func main() {
...
var a Animal
a = &d
a.Run()
a.Roar()
}
// output:
// Dog Jim run!
// A big dog roar!

同样定义另外一个实现该接口的类型。

1
2
3
4
5
6
7
8
9
10
11
12
type Cat struct {
Name string
Color string
}
func (c *Cat)Run() {
fmt.Printf("Cat %s run!\n", c.Name)
}
func (c *Cat)Roar() {
fmt.Printf("A %s cat roar!\n", c.Color)
}

再让上面声明的接口变量指向这个类型的实例的地址,实际上调用方法的对象则变成后者。

1
2
3
4
5
6
7
8
9
10
11
12
13
func main() {
...
c := Cat{
Name: "Kitty",
Color: "yello",
}
a = &c
c.Roar()
c.Run()
}
//output:
// A yello cat roar!
// Cat Kitty run!

Go语言里面向对象的特性通过interface 很轻易地体现了出来。本质上上述声明的Animal 变量a 是一个指针,它底层的内存结构包含两个字段:

  • receiver,a可以指向任意变量,只需要这个变量的类型实现了该接口,此时receiver 存储该变量的地址,实际上此地址指向的其实是该变量的一个副本,因为Go 会在堆上申请一段空间用以存储变量的副本。如果该变量不大于一个字,则该变量直接存储在receiver 中。
  • method table ptr,类似于C++ 里的虚函数表,当a 指向某实例变量后,该字段存储其方法的入口地址。

类型转换

一个空的 interface ,不包含任何方法,因此它能够存储任意类型的值。

1
2
3
4
5
6
7
8
9
10
11
12
func main() {
var i interface{}
a := 1
i = a
fmt.Println(i)
b := true
i = b
fmt.Println(i)
}
//output:
// 1
// true

我们可以借此实现一个函数,它的参数可以接收任意类型的值。这时,interface 的用法就比较像Java里的Object 对象。同样,我们使用这个参数前需要类型转换,在Java里可以通过运算符instanceof 来判断一个对象的实例是否为某一类型,并可以通过显示类型转换来使用这个实例。 在Go语言里,我们通过断言一样可以做到这些。先看看断言的用法:

1
2
3
4
5
6
7
func testFunc(animal interface{}) {
if c, ok := animal.(Cat); ok {
fmt.Println(c)
} else {
fmt.println("It's not a cat!")
}
}

格式为:变量名.(类型名)。返回两个值,第一个值为转换类型后的实例,第二个值为布尔变量,如果传入的参数为Cat 类型就返回 true, 否则返回 false 。 调用testFunc 时,在函数里判断其传入的参数是否为Cat 类型,如果是就打印出它的值。

也可以直接使用断言返回的第一个值。用法如c := animal.(Cat),直接忽略判断的结果。下面我们为testFunc 加入更多类型的判断,这时,通过结合switch的使用,使代码可读性更加友好。

1
2
3
4
5
6
7
8
9
10
func testFunc(animal interface{}) {
switch animal.(type) {
case Dog:
fmt.Println(animal)
case Cat:
fmt.Println(animal)
default:
fmt.Println("It's nothing!")
}
}