在计算机的世界里,同一类工具不同的实现所体现出背后的哲理也是不一样的,例如 Linux 与 windows,都是操作系统,但是如果用使用windows的习惯去操作Linux是玩不转的。编程语言也一样,对于面向对象不同的语言也同过不同的方式来实现。java不支持类的多重继承,但是可以通过接口的多重继承来弥补。Python干脆在语言层面上就不提供接口这样的特性,所以要想实现接口的效果可以采用继承只有方法空实现的父类并重写父类方法来达到目的。而Go语言提供了更加灵活和抽象的接口特性。
C++、Java使用的是“侵入式”的接口,即实现类需要显式的声明自己所实现的接口,而Go采用的是“非侵入式”的接口,从语言角度看,接口是一种类型,它指定一个方法集。只要类型T的公开方法完全满足接口I的要求,就可以把类型T的对象用在需要接口I的地方,所谓类型T的公开方法完全满足接口I的要求,也就是如果一个接口里的所有方法都被类型T实现了,那么我们就说该类型实现了该接口。这种做法的学名叫做Structural Typing,有人也把它看作是一种静态的Duck Typing。
接口
定义一个接口如下
1 | type Driver interface{ |
我们定义了一个接口Driver,该接口中声明了一个方法Drive,接下来我们看如何定义一个struct来实现该接口。
1 | type Person sturct{ |
定义了一个方法,该方法的Receiver是Person,并且该方法完全符合接口Driver的要求所以Person就实现了该接口。
下面我们定义一个方法用来接受任意一个实现Driver接口的类型的值或者指针。
1 | func main() { |
诶?为什么编译器不考虑值是实现该接口的类型?这就涉及到Go语言规范里的一些规则:
- 类型
*T
的可调用方法集包含接受者为*T
或T
的所有方法集
这条规则是说如果用来调用特定接口方法的接口变量是一个指针类型,那么方法的接受者可以是值类型也可以是指针类型。显然刚才的例子不符合该规则,因为我们传入 DriveCar
函数的接口变量是一个值类型。
- 类型
T
的可调用方法集包含接受者为T
的所有方法
这条规则是说如果用来调用特定接口方法的接口变量是一个值类型,那么方法的接受者必须也是值类型该方法才可以被调用。显然上面的例子也不符合这条规则,因为我们 Drive
方法的接受者是一个指针类型。
由此可以得出以下结论:
- 类型
T
的可调用方法集不包含接受者为*T
的方法
所以我们只需将Person的地址传给DriveCar即可
1 | func main() { |
类型的指针同样可以调用接受者是值的方法。Go调整和解引用指针使得调用可以被执行。注意,当接受者不是一个指针时,该方法操作对应接受者的值的副本(意思就是即使你使用了指针调用函数,但是函数的接受者是值类型,所以函数内部操作还是对副本的操作,而不是指针操作)
内嵌类型
struct中的一个属性为另一个struct即为内嵌,定义一个新类型然后潜入Person:
1 | type Manager struct { |
这种方式是组合而不是集成,现在修改调用方法:
1 | func main() { |
解释如下
当我们嵌入一个类型,这个类型的方法就变成了外部类型的方法,但是当它被调用时,方法的接受者是内部类型(嵌入类型),而非外部类型。— Effective Go
我们可以用 Manager
类型的一个指针来调用 DriveCar
函数。现在 Manager
类型也通过来自嵌入的 Person
类型的方法提升实现了该接口。
Go 语言中内部类型方法集提升的规则有如下三条:
给定一个结构体类型S
和一个命名为T
的类型,方法提升像下面规定的这样被包含在结构体方法集中:
- 如果
S
包含一个匿名字段T
,S
和*S
的方法集都包含接受者为T的方法提升。
意思是当嵌入一个类型时嵌入类型的接受者为值类型的方法将被提升,可以被外部类型的值和指针调用。
- 对于
*S
类型的方法集包含接受者为*T
的方法提升
这条规则说的是当外部类型使用指针调用内部类型的方法时,只有接受者为指针类型的内部类型方法集将被提升。
- 如果
S
包含一个匿名字段*T
,S
和*S
的方法集都包含接受者为T
或者*T
的方法提升
这条规则说的是嵌入类型的接受者为值类型或指针类型的方法将被提升,可以被外部类型的值或者指针调用。
所以如果外部类型包含了符合要求的接口实现,它将会被使用。否则,通过方法提升,任何内部类型的接口实现可以直接被外部类型使用。