类型嵌入指的就是在一个类型的定义中嵌入了其他类型。Go 语言支持两种类型嵌入:

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

接口类型只能嵌入接口类型,而结构体类型对嵌入类型的要求就比较宽泛了,可以是任意自定义类型或接口类型。

1,接口类型的类型嵌入

接口类型声明了由一个方法集合代表的接口,比如下面接口类型 E:

type E interface {
    M1()
    M2()
}

如果某个类型实现了方法 M1 和 M2,我们就说这个类型实现了 E 所代表的接口。

再定义另外一个接口类型 I,它的方法集合中包含了三个方法 M1、M2 和 M3,如下面代码:

type I interface {
    M1()
    M2()
    M3()
}

我们看到接口类型 I 方法集合中的 M1 和 M2,与接口类型 E 的方法集合中的方法完全相同。在这种情况下,我们可以用接口类型 E 替代上面接口类型 I 定义中 M1 和 M2,如下面代码:

type I interface {
    E
    M3()
}

像这种在一个接口类型(I)定义中,嵌入另外一个接口类型(E)的方式,就是我们说的接口类型的类型嵌入

而且,这个带有类型嵌入的接口类型 I 的定义与上面那个包含 M1、M2 和 M3 的接口类型 I 的定义,是等价的。

通过在接口类型中嵌入其他接口类型可以实现接口的组合,这也是 Go 语言中基于已有接口类型构建新接口类型的惯用法。

接口类型的类型嵌入比较简单,我们只要把握好它的语义,也就是“方法集合并入”就可以了。

2,结构体类型的类型嵌入

常规的结构体类型:

type S struct {
    A int
    b string
    c T
    p *P
    _ [10]int8
    F func()
}

结构体类型 S 中的每个字段(field)都有唯一的名字与对应的类型,即便是使用空标识符占位的字段,它的类型也是明确的。

带有嵌入字段的结构体定义,看下面代码中的 S1

type T1 int
type t2 struct{
    n int
    m int
}

type I interface {
    M1()
}

type S1 struct {
    T1
    *t2
    I            
    a int
    b string
}
  • 标识符 T1 表示字段名为 T1,它的类型为自定义类型 T1;
  • 标识符 t2 表示字段名为 t2,它的类型为自定义结构体类型 t2 的指针类型;
  • 标识符 I 表示字段名为 I,它的类型为接口类型 I。
  • 如果嵌入类型的名字是首字母大写的,那么也就说明这个嵌入字段是可导出的。
  • 如果结构体使用从其他包导入的类型作为嵌入字段,比如 pkg.T,那么这个嵌入字段的字段名就是 T,代表的类型为 pkg.T。

这种以某个类型名、类型的指针类型名或接口类型名,直接作为结构体字段的方式就叫做结构体的类型嵌入,这些字段也被叫做嵌入字段

嵌入字段的使用的确可以帮我们在 Go 中实现方法的“继承”。

类型嵌入这种看似“继承”的机制,实际上是一种组合的思想。更具体点,它是一种组合中的代理(delegate)模式,如下图所示:

在这里插入图片描述

结构体类型的方法集合,包含嵌入的接口类型的方法集合。 也就是说,当结构体类型中嵌入了接口类型时,接口类型中的方法都会并入到结构体类型中。

在结构体类型中嵌入结构体类型,为 Gopher 们提供了一种“实现继承”的手段,外部的结构体类型 T 可以“继承”嵌入的结构体类型的所有方法的实现。

当通过结构体类型 S 的变量 s 调用 Read 方法时,Go 发现结构体类型 S 自身并没有定义 Read 方法,于是 Go 会查看 S 的嵌入字段对应的类型是否定义了 Read 方法。这个时候,Reader 字段就被找了出来,之后 s.Read 的调用就被转换为 s.Reader.Read 调用。

这样一来,嵌入字段 Reader 的 Read 方法就被提升为 S 的方法,放入了类型 S 的方法集合。

当结构体嵌入的多个接口类型的方法集合存在交集时,Go 编译器会报错。

嵌入了其他类型的结构体类型本身是一个代理,在调用其实例所代理的方法时,Go 会首先查看结构体自身是否实现了该方法。

  • 如果实现了,Go 就会优先使用结构体自己实现的方法。
  • 如果没有实现,那么 Go 就会查找结构体中的嵌入字段的方法集合中,是否包含了这个方法。
    • 如果多个嵌入字段的方法集合中都包含这个方法,那么我们就说方法集合存在交集。
    • 这个时候,Go 编译器就会因无法确定究竟使用哪个方法而报错。(有点类似 C++ 多重继承出现的问题)

一个示例:

  type E1 interface {
      M1()
      M2()
      M3()
  }
  
  type E2 interface {
     M1()
     M2()
     M4()
 }
 
 type T struct {
     E1
     E2
 }
 
 func main() {
     t := T{}
     t.M1()
     t.M2()
 }

输出如下:

main.go:22:3: ambiguous selector t.M1
main.go:23:3: ambiguous selector t.M2

这个问题有两种解决办法

  • 一是,我们可以消除 E1 和 E2 方法集合存在交集的情况。
  • 二是为 T 增加 M1 和 M2 方法的实现,这样的话,编译器便会直接选择 T 自己实现的 M1 和 M2,不会陷入两难境地。

如下:

... ...
type T struct {
    E1
    E2
}

func (T) M1() { println("T's M1") }
func (T) M2() { println("T's M2") }

func main() {
    t := T{}
    t.M1() // T's M1
    t.M2() // T's M2
}

3,type 定义新类型时的方法集合

分两种情况:

  • type NewT OldT:定义新的类型
    • 如果 OldT 是接口类型,NewT 的方法集合与 OldT 的方法集合是一致的
    • 如果 OldT 是非接口类型,那么 NewT 不会继承 OldT 的任意一个方法,NewT 的方法集合是空集合
  • type NewT = OldT:类型别名
    • 无论 OldT 是接口类型还是非接口类型,NewT 都与 OldT 拥有完全相同的方法集合
    • 实际上 NewT 与 OldT 是完全等价的

4,一个思考题

下面带有类型嵌入的结构体 S1 与不带类型嵌入的结构体 S2 是否是等价的,如不等价,区别在哪里?

type T1 int
type t2 struct{
    n int
    m int
}

type I interface {
    M1()
}

type S1 struct {
    T1
    *t2
    I
    a int
    b string
}

type S2 struct { 
    T1 T1
    t2 *t2
    I  I
    a  int
    b  string
}
  • S1 是类型嵌入的,拥有“继承”能力
  • S2 并不是类型嵌入的,只是普通的结构体组合,所以没有对应接口的方法集合(并不存在“继承”的能力)

(完。)