接口也是 Go 语言中的一种类型,它能够出现在变量的定义、函数的入参和返回值中并对它们进行约束,不过 Go 语言中有两种略微不同的接口,一种是带有一组方法的接口,另一种是不带任何方法的 interface{}
:
图 4-7 Go 语言中的两种接口
Go 语言使用 iface
结构体表示第一种接口,使用 eface
结构体表示第二种空接口,两种接口虽然都使用 interface
声明,但是由于后者在 Go 语言中非常常见,所以在实现时使用了特殊的类型。
需要注意的是,与 C 语言中的 void *
不同,interface{}
类型不是任意类型,如果我们将类型转换成了 interface{}
类型,这边变量在运行期间的类型也发生了变化,获取变量类型时就会得到 interface{}
。
package main
func main() {
type Test struct{}
v := Test{}
Print(v)
}
func Print(v interface{}) {
println(v)
}
上述函数不接受任意类型的参数,只接受 interface{}
类型的值,在调用 Print
函数时会对参数 v
进行类型转换,将原来的 Test
类型转换成 interface{}
类型,我们会在本节后面介绍类型转换的过程和原理。
指针和接口
在 Go 语言中同时使用指针和接口时会发生一些让人困惑的问题,接口在定义一组方法时没有对实现的接收者做限制,所以我们会看到『一个类型』实现接口的两种方式:
图 4-8 结构体和指针实现接口
这是因为结构体类型和指针类型是完全不同的,就像我们不能向一个接受指针的函数传递结构体,在实现接口时这两种类型也不能划等号。但是上图中的两种实现不可以同时存在,Go 语言的编译器会在结构体类型和指针类型都实现一个方法时报错 —— method redeclared
。
对 Cat
结构体来说,它在实现接口时可以选择接受者的类型,即结构体或者结构体指针,在初始化时也可以初始化成结构体或者指针。下面的代码总结了如何使用结构体、结构体指针实现接口,以及如何使用结构体、结构体指针初始化变量。
type Cat struct {}
type Duck interface { ... }
func (c Cat) Quack {} // 使用结构体实现接口
func (c *Cat) Quack {} // 使用结构体指针实现接口
var d Duck = Cat{} // 使用结构体初始化变量
var d Duck = &Cat{} // 使用结构体指针初始化变量
实现接口的类型和初始化返回的类型两个维度组成了四种情况,这四种情况并不都能通过编译器的检查:
| 结构体实现接口 | 结构体指针实现接口 |
结构体初始化变量 | 通过 | 不通过 |
结构体指针初始化变量 | 通过 | 通过 |
四种中只有『使用指针实现接口,使用结构体初始化变量』无法通过编译,其他的三种情况都可以正常执行。当实现接口的类型和初始化变量时返回的类型时相同时,代码通过编译是理所应当的:
- 方法接受者和初始化类型都是结构体;
- 方法接受者和初始化类型都是结构体指针;
而剩下的两种方式为什么一种能够通过编译,另一种无法通过编译呢?我们先来看一下能够通过编译的情况,也就是方法的接受者是结构体,而初始化的变量是结构体指针:
type Cat struct{}
func (c Cat) Quack() {
fmt.Println("meow")
}
func main() {
var c Duck = &Cat{}
c.Quack()
}
作为指针的 &Cat{}
变量能够隐式地获取到指向的结构体,所以能在结构体上调用 Walk
和 Quack
方法。我们可以将这里的调用理解成 C 语言中的 d->Walk()
和 d->Speak()
,它们都会先获取指向的结构体再执行对应的方法。
但是如果我们将上述代码中方法的接受者和初始化的类型进行交换,代码就无法通过编译了:
type Duck interface {
Quack()
}
type Cat struct{}
func (c *Cat) Quack() {
fmt.Println("meow")
}
func main() {
var c Duck = Cat{}
c.Quack()
}
$ go build interface.go
./interface.go:20:6: cannot use Cat literal (type Cat) as type Duck in assignment:
Cat does not implement Duck (Quack method has pointer receiver)
编译器会提醒我们:Cat
类型没有实现 Duck
接口,Quack
方法的接受者是指针。这两个报错对于刚刚接触 Go 语言的开发者比较难以理解,如果我们想要搞清楚这个问题,首先要知道 Go 语言在传递参数时都是传值的。
图 4-9 实现接口的接受者类型
如上图所示,无论上述代码中初始化的变量 c
是 Cat{}
还是 &Cat{}
,使用 c.Quack()
调用方法时都会发生值拷贝:
- 如图 4-9 左侧,对于
&Cat{}
来说,这意味着拷贝一个新的&Cat{}
指针,这个指针与原来的指针指向一个相同并且唯一的结构体,所以编译器可以隐式的对变量解引用(dereference)获取指针指向的结构体; - 如图 4-9 右侧,对于
Cat{}
来说,这意味着Quack
方法会接受一个全新的Cat{}
,因为方法的参数是*Cat
,编译器不会无中生有创建一个新的指针;即使编译器可以创建新指针,这个指针指向的也不是最初调用该方法的结构体;
上面的分析解释了指针类型的现象,当我们使用指针实现接口时,只有指针类型的变量才会实现该接口;当我们使用结构体实现接口时,指针类型和结构体类型都会实现该接口。当然这并不意味着我们应该一律使用结构体实现接口,这个问题在实际工程中也没那么重要,在这里我们只想解释现象背后的原因。