Go 语言
函数和方法
方法集合决定接口实现

方法集合决定的接口实现

Go 语言有一个创新的设计,它是通过方法集合来决定接口是否被实现的。接口的定义上不再关注类型是否是接口的实现,而是关注类型的方法集合是否满足接口的需求。这种设计可以让类型的实现与接口的定义解耦,互不影响。

方法集合是类型的一部分,它决定了类型的行为。如果类型 T 的方法集合包含接口 A 的所有方法,那么类型 T 被称为实现接口 A。这种设计可以让类型实现接口的时候无需显式声明,只要方法集合满足接口需求即可。

type A interface {
    Say()
}
 
type B struct {}
 
func (b B) Say() {
    fmt.Println("B")
}
 
func main() {
    var b B
    var a A = b
    a.Say()
    b.Say()
}
 
// 输出
// B
// B

在上面的代码中,类型 B 实现了接口 A,因为类型 B 的方法集合包含接口 A 的所有方法。所以类型 B 的实例可以赋值给接口 A 类型的变量。

方法集合

类型的方法集合是指可以被该类型的值调用的所有方法的集合。方法集合的类型有两种,分别是值接收者的方法集合和指针接收者的方法集合。

Go 语言有一个规范:对于非接口类型的自定义类型 T,其方法集合由所有 receiver 为 T 类型的方法组合;而类型 *T 的方法集合则包含所有 receiver 为 T 和 *T 类型的方法。

我们看一个例子:

type A interface {
    Say()
    Call()
}
 
type B struct {}
 
func (b B) Say() {
    fmt.Println("B")
}
 
func (b *B) Call() {
    fmt.Println("Call")
}
 
func main() {
    var b B
    var pb *B
    var a A = b // panic: cannot use b (type B) as type A in assignment: B does not implement A (Call method has pointer receiver)
    var pa A = pb
}

这里就触发了 panic, 因为 b 的方法集合只有 Say,而 A 需要 Call 方法。而 pb 的方法集合包含 SayCall 方法,所以可以赋值给 A 类型的变量。

类型嵌入

Go 支持用组合的思想来实现一些面向对象领域经典机制,比如继承。而且具体的方式就是利用类型嵌入(type embedding)。

与接口类型和结构体类型相关的类型嵌入有三种组合:

  • 在接口类型嵌入接口类型
  • 在结构体类型嵌入接口类型
  • 在结构体类型嵌入结构体类型

在接口类型嵌入接口类型

通过在接口类型中嵌入其他接口类型,可以将多个接口类型组合成一个接口类型。这种方式可以用于将一个大接口类型拆分成多个小接口类型,或者将多个小接口类型组合成一个大接口类型。

比如,io 包中的 io.ReadWriter 接口类型就是由 io.Reader 和 io.Writer 接口类型组合而成的。

type Reader interface {
    Read(p []byte) (n int, err error)
}
 
type Writer interface {
    Write(p []byte) (n int, err error)
}
 
type ReadWriter interface {
    Reader
    Writer
}

在 Go 1.14 版本之前,这种方式有一个约束,那就是被嵌入的接口类型的方法集合不能有交集,同时被嵌入的接口类型的方法集合中的方法不能与新接口中的其他方法同名。 自 Go 1.14 版本开始,这个约束驱动了。

在结构体类型嵌入接口类型

在结构体类型中嵌入接口类型后,该结构体类型的方法集合中将包含被嵌入接口类型的方法集合。

我们看一个例子:

type A interface {
    M1()
    M2()
}
 
type B struct {
    Interface
}
 
func (B) M3() {}

B 结构体类型中嵌入了接口类型 A,所以 B 结构体类型的方法集合中将包含 A 接口类型的方法集合。其方法集合为 M1、M2、M3

这里有一个问题需要注意:如果当结构体类型中嵌入多个接口类型,而且这些接口类型的方法集合存在交集时,怎么调用方法?

在 Go 语言中,嵌入了其他接口类型的结构体类型的实例在调用方法时,Go 选择方法的次序:

  • 优选选择结构体自身实现的方法
  • 如果结构体没有实现方法,那么将查找结构体中的嵌入接口类型的方法集合中是否有该方法,如果有,则提升为结构体方法。
  • 如果结果体嵌入了多个接口类型,而这些接口类型的方法集合存在交集,那么 Go 编译器就会报错,除非结构体自身实现了交集中的方法。

我们看下面的例子:

type A interface {
    M1()
    M2()
}
 
type B interface {
    A
    M3()
}
 
type C struct {
    B
}
 
func (C) M1() {
    fmt.Println("C M1")
}
 
func (C) M3() {
    fmt.Println("C M3")
}
 
func main() {
    var c C
    c.M1()
    c.M2()
    c.M3()
}
 
// 输出
// C M1
// M2
// C M3

在上面的代码中,C 结构体类型中嵌入了 B 接口类型,而 B 接口类型中又嵌入了 A 接口类型。 C 结构体类型实现了 M1 和 M3 方法,所以调用 C 结构体类型的实例的 M1 和 M3 方法时,调用的是 C 结构体类型的方法。 而 C 结构体类型没有实现 M2 方法,所以调用 C 结构体类型的实例的 M2 方法时,调用的是 B 接口类型的 M2 方法。

在结构体类型嵌入结构体类型

在结构体类型嵌入结构体类型 Go 提供了一种实现”继承“的手段,外部的结构体类型 T 可以”继承“嵌入的结构体类型的所有方法的实现。 无论是 T 类型变量实例还是 *T 类型变量实例,都可以调用所有嵌入结构体类型的方法。

我们看一个例子:

type T1 struct {}
 
func (T1) T1M1() {
    println("T1M1")
}
 
func (T1) T1M2() {
    println("T1M2")
}
 
func (*T1) PT1M3() {
    println("PT1M3")
}
 
type T2 struct {}
 
func (T2) T2M1() {
    println("T2M1")
}
 
func (T2) T2M2() {
    println("T2M2")
}
 
func (*T2) PT2M3() {
    println("PT2M3")
}
 
type T struct {
    T1
    * T2
}
 
func main() {
    t := T{
        T1: T1{},
        T2: &T2{},
    }
    t.T1M1()
    t.T1M2()
    t.PT1M3()
    t.T2M1()
    t.T2M2()
    t.PT2M3()
 
    pt := &t
    pt.T1M1()
    pt.T1M2()
    pt.PT1M3()
    pt.T2M1()
    pt.T2M2()
    pt.PT2M3()
}
 
// 输出
// T1M1
// T1M2
// PT1M3
// T2M1
// T2M2
// PT2M3
// T1M1
// T1M2
// PT1M3
// T2M1
// T2M2
// PT2M3

通过输出可以看到,无论是 T 类型实例还是 *T 类型实例都可以调用所有方法,但是 T 和 *T 的方法集合是有差别的。

defined 类型的方法集合

Go 语言支持基于已有的类型创建新类型。已有的类型呗称为 underlying type,新类型被称为 defined type。 新定义的 defined 类型与原 underlying 类型是完全不同的类型,那么它们的方法集合有什么关系,我们看一个例子:

type T struct{}
 
func (T) M1() {}
funct (*T) M2() {}
 
type Interface interface {
    M1()
    M2()
}
 
type T1 t
type Interface1 Interface
 
func main() {
    var t T     // M1
    var pt *T   // M1 M2
    var t1 T1   // empty
    var pt1 *T1 // empty
    (Interface)(nil) // M1 M2
    (Interface1)(nil) // M1 M2
}

Go 对于分别基于接口类型和自定义类型非接口类型的创建的 defined 类型给出不一样的结果:

  • 基于接口类型创建的 defined 类型的方法集合与原接口类型的方法集合一致,如 InterfaceInterface1
  • 而基于自定义非接口类型创建的 defined 类型的方法集合为空,并没有“继承”原类型的方法,如 T1

方法集合决定接口实现。

类型别名的方法集合

类型别名与原类型几乎等价。Go 预定义标识符 rune 、 byte 就是通过类型别名语法定义的。

type byte = uint8
type rune = int32

注意类型别名别和 defined 类型的区别,语法上就有个 =

类型别名与原类型拥有完全相同的方法集合。无论原类型是接口类型还是非接口类型。