函数与方法
24 方法集合决定接口实现
方法集合
Go 语言的一个创新是,自定义类型与接口之间的实现关系是松耦合的:如果某个自定义类型 T 的方法集合是某个接口类型的方法集合的超集,那么就说类型 T 实现了该接口,并且类型 T 的变量可以被赋值给该接口类型的变量,即我们说的方法集合决定接口实现。
方法集合是 Go 语言中一个重要的概念,在为接口类型变量赋值、使用结构体嵌入/接口嵌入、类型别名和方法表达式等时都会用到方法集合,它像胶水一样将自定义类型与接口隐式地黏结在一起。
Go 语言规范:对于非接口类型的自定义类型 T,其方法集合由所有 receiver 为 T 类型的方法组成。而类型 *T 的方法集合则包含所有 receiver 为 T 和 *T 类型的方法。
所以,在为 receiver 选择类型时需要考虑的第三点因素:是否支持将 T 类型实例赋值给某个接口类型变量。如果需要支持,我们就要实现 receiver 为 T 类型的接口类型方法集合中的所有方法。
类型嵌入
类型嵌入是用组合的思想来实现面向对象领域经典的继承机制。
与接口类型和结构体类型相关的类型嵌入有三种组合:
- 
在接口类型中嵌入接口类型:(如 io 包中的 ReadWriter、ReadWriteCloser 等);
 - 
在结构体类型中嵌入接口类型:结构体类型在嵌入某接口类型的同时,也实现了这个接口。当嵌入其它接口类型的结构体类型的实例在调用方法时,Go 选择方法的次序:
- 优先选择结构体自身的方法;
 - 如果结构体自身并未实现,那么将查找结构体中的嵌入接口类型的方法集合中是否有该方法,如果有,则提升为结构体的方法;
 - 如果结构体嵌入了多个接口类型且这些接口类型的方法集合存在交集,那么 Go 编译器将报错,除非结构体自己实现了交集中的所有方法;(所以尽量避免在结构体类型中嵌入方法集合有交集的多个接口类型)
 
 - 
在结构体类型中嵌入结构体类型:通过下面的代码输出结果没有报错可以看出,无论通过 T 类型变量实例还是 *T 类型变量实例都可以调用所有“继承”的方法(这也是 Go 语法糖),但是 T 和 *T 类型的方法集合是有差别的:
- T 类型的方法集合 = T1 的方法集合 + *T2 的方法集合
 - *T 类型的方法集合 = *T1 的方法集合 + *T2 的方法集合
 
type T1 struct{} func (T1) T1M1() { println("T1's M1") } func (T1) T1M2() { println("T1's M2") } func (*T1) PT1M3() { println("PT1's M3") } type T2 struct{} func (T2) T2M1() { println("T2's M1") } func (T2) T2M2() { println("T2's M2") } func (*T2) PT2M3() { println("PT2's M3") } type T struct { T1 *T2 } func main() { t := T{ T1: T1{}, T2: &T2{}, } println("call method through t:") t.T1M1() t.T1M2() t.PT1M3() t.T2M1() t.T2M2() t.PT2M3() println("\ncall method through pt:") pt := &t pt.T1M1() pt.T1M2() pt.PT1M3() pt.T2M1() pt.T2M2() pt.PT2M3() } 
defined 类型的方法集合
Go 语言支持基于已有的类型创建新类型,如type newType originalType。
已有的类型被称为 underlying 类型,而新类型被称为 defined 类型。新定义的 defined 类型与原 underlying 类型是完全不同的类型。然而,Go 对于分别基于接口类型和自定义非接口类型创建的 defined 类型的方法集合是不一致的:
- 基于接口类型创建的 defined 类型与原接口类型的方法集合是一致的;
 - 基于自定义非接口类型创建的 defined 类型并没有“继承”原类型的方法集合,新的 defined 类型的方法集合是空的。
 
方法集合决定接口实现。基于自定义非接口类型的 defined 类型的方法集合为空,这决定了即便原类型实现了某些接口,基于其创建的 defined 类型也没有“继承”这一隐式关联。新 defined 类型要想实现那些接口,仍需重新实现接口的所有方法。
类型别名的方法集合
Go 语言支持为已有类型定义别名,如type byte = uint8和type rune = int32。
类型别名与原类型拥有完全相同的方法集合,无论原类型是接口类型还是非接口类型。
往期回顾
- 「读书笔记」在 init 函数中检查包级变量的初始状态
 - 「读书笔记」让自己习惯于函数是“一等公民”
 - 「读书笔记」使用 defer 让函数更简洁、更健壮
 - 「读书笔记」理解方法的本质以选择正确的 receiver 类型
 
关注我
参考
《Go 语言精进之路:从新手到高手的编程思想、方法和技巧》——白明