1. 接口的定义
接口是和调用方的一种约定,它是一个高度抽象的类型,不用和具体的实现细节绑定在一起。接口要做的是定义好约定,告诉调用方自己可以做什么,但不用知道它的内部实现,这和我们见到的具体的类型如 int
、map
、slice
等不一样。
接口的定义和结构体稍微有些差别,虽然都以 type
关键字开始,但接口的关键字是 interface
,表示自定义的类型是一个接口。
也就是说 person
是一个接口,它有两个方法 sayName() string
和 sayAge() int
,整体如下面的代码所示:
type person interface {
sayName() string
sayAge() int
}
}
针对 person
接口来说,它会告诉调用者可以通过它的 sayName()
方法获取姓名字符串,通过它的 sayAge()
方法获取年龄,这就是接口的约定。至于这个字符串怎么获得的,长什么样,接口不关心,调用者也不用关心,因为这些是由接口实现者来做的。
2. 接口的实现
接口的实现者必须是一个具体的类型,以 student
结构体为例,
type student struct {
name string
age int
}
让它来实现 person
接口,如下代码所示:
func (s student) sayName() string {
fmt.Printf("name is %v\n", s.name)
return s.name
}
func (s student) sayAge() int {
fmt.Printf("name is %v\n", s.age)
return s.age
}
给结构体类型 student
定义一个方法,这个方法和接口里方法的签名(名称、参数和返回值)一样,这样结构体 student
就实现了 person
接口。
注意:如果一个接口有多个方法,那么需要实现接口的每个方法才算是实现了这个接口。
3. 接口赋值
接口赋值在 Go
语言中分为如下两种情况:
- 将对象实例赋值给接口;
- 将一个接口赋值给另一个接口。
3.1 对象实例赋值给接口
先讨论将某种类型的对象实例赋值给接口,这要求该对象实例实现了接口要求的所有方法,如下:
type Integer int
func (a Integer) Less(b Integer) bool {
return a < b
}
func (a *Integer) Add(b Integer) {
*a += b
}
相应地,我们定义接口 LessAdder
,如下:
type LessAdder interface {
Less(b Integer) bool
Add(b Integer)
}
现在有个问题:假设我们定义一个 Integer
类型的对象实例,怎么将其赋值给 LessAdder
接口呢?
应该用下面的语句(1),还是语句(2)呢?
var a Integer = 1
var b LessAdder = &a ... (1)
var b LessAdder = a ... (2)
答案是应该用语句(1)。原因在于,Go
语言可以根据下面的函数:
func (a Integer) Less(b Integer) bool {
return a < b
}
自动生成一个新的 Less()
方法:
func (a *Integer) Less(b Integer) bool {
return (*a).Less(b)
}
这样,类型 *Integer
就既存在 Less()
方法,也存在 Add()
方法,满足 LessAdder
接口。
而从另一方面来说,根据
func (a *Integer) Add(b Integer)
这个函数无法自动生成以下这个成员方法:
func (a Integer) Add(b Integer) {
(&a).Add(b)
}
因为 (&a).Add()
改变的只是函数参数a,对外部实际要操作的对象并无影响,这不符合用
户的预期。
所以,Go
语言不会自动为其生成该函数。因此,类型 Integer
只存在 Less()
方法,缺少 Add()
方法,不满足 LessAdder
接口,故此上面的语句(2)不能赋值。
为了进一步证明以上的推理,我们不妨再定义一个 Lesser
接口,如下:
type Lesser interface {
Less(b Integer) bool
}
然后定义一个 Integer
类型的对象实例,将其赋值给 Lesser
接口:
var a Integer = 1
var b1 Lesser = &a ... (1)
var b2 Lesser = a ... (2)
正如我们所料的那样,语句(1)和语句(2)均可以编译通过。
3.2 将一个接口赋值给另一个接口
在 Go
语言中,只要两个接口拥有相同的方法列表(次序不同不要紧),那么它们就是等同的,可以相互赋值。
4. 接口调用
实现了 person
接口后就可以使用了。首先我先来定义一个可以打印 person
接口的函数,如下所示:
func printPersonInfo(p person) {
fmt.Printf("name is %v\n", p.sayName())
fmt.Printf("age is %v\n", p.sayAge())
}
这个被定义的函数 printPersonInfo
,它接收一个 person
接口类型的参数,然后打印出 person
接口的 sayName()
方法返回的字符串,sayAge()
方法返回年龄
printPersonInfo
这个函数的优势就在于它是面向接口编程的,只要一个类型实现了 Stringer
接口,都可以打印出对应的字符串,而不用管具体的类型实现。
因为 student
实现了 person
接口,所以变量 stu
可以作为函数 printPersonInfo
的参数,可以用如下方式打印:
printPersonInfo(stu)
输出
name is wohu
age is 20
现在让结构体 teacher
也实现 person
接口,如下面的代码所示:
type teacher struct {
name string
age int
}
func (t teacher) sayName() string {
return t.name
}
func (t teacher) sayAge() int {
return t.age
}
因为结构体 teacher
也实现了 person
接口,所以 printPersonInfo
函数不用做任何改变,可以直接被使用,如下所示:
printPersonInfo(t)
输出结果:
name is Jack
age is 40
这就是面向接口的好处,只要定义和调用双方满足约定,就可以使用,而不用管具体实现。接口的实现者也可以更好的升级重构,而不会有任何影响,因为接口约定没有变。
5. 值接收者和指针接收者
我们已经知道,如果要实现一个接口,必须实现这个接口提供的所有方法,而且在上节课讲解方法的时候,定义一个方法,有值类型接收者和指针类型接收者两种。二者都可以调用方法,因为 Go
语言编译器自动做了转换,所以值类型接收者和指针类型接收者是等价的。但是在接口的实现中,值类型接收者和指针类型接收者不一样,下面我会详细分析二者的区别。
上面已经验证了结构体类型实现了 person
接口,那么结构体对应的指针是否也实现了该接口呢?我通过下面这个代码进行测试:
printPersonInfo(&s)
输出结果:
name is wohu
age is 20
测试后会发现,把变量 t
的指针作为实参传给 printPersonInfo
函数也是可以的,编译运行都正常。这就证明了以值类型接收者实现接口的时候,不管是类型本身,还是该类型的指针类型,都实现了该接口。
示例中值接收者(s student
)实现了 person
接口,那么类型 student
和它的指针类型 *student
就都实现了 person
接口。
现在,我把接收者改成指针类型,如下代码所示:
func (s *student) sayName() string {
return s.name
}
func (s *student) sayAge() int {
return s.age
}
修改成指针类型接收者后会发现,示例中这行 printPersonInfo(stu)
代码编译不通过,提示如下错误:
demo.go:46:17: cannot use stu (type student) as type person in argument to printPersonInfo:
student does not implement person (sayAge method has pointer receiver)
意思就是类型 student
没有实现 person
接口。这就证明了以指针类型接收者实现接口的时候,只有对应的指针类型才被认为实现了该接口。
总结:
-
当值类型作为接收者时,
student
类型和*student
类型都实现了该接口; -
当指针类型作为接收者时,只有
*student
类型实现了该接口;
可以发现,实现接口的类型都有 *student
,这也表明指针类型比较万能,不管哪一种接收者,它都能实现该接口。
package main
import "fmt"
type student struct {
name string
age int
}
type teacher struct {
name string
age int
}
type person interface {
sayName() string
sayAge() int
}
func (t teacher) sayName() string {
return t.name
}
func (t teacher) sayAge() int {
return t.age
}
func (s *student) sayName() string {
// fmt.Printf("name is %v\n", s.name)
return s.name
}
func (s *student) sayAge() int {
// fmt.Printf("age is %v\n", s.age)
return s.age
}
func printPersonInfo(p person) {
fmt.Printf("name is %v\n", p.sayName())
fmt.Printf("age is %v\n", p.sayAge())
}
func main() {
stu := student{
name: "wohu", age: 20}
// t := teacher{name: "Jack", age: 40}
printPersonInfo(stu)
// printPersonInfo(t)
}