8

Go 学习笔记7-Go继承:类型嵌入

 1 year ago
source link: https://codeshellme.github.io/2022/06/go7/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

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

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

1,接口类型的类型嵌入

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

type E interface {

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

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

type I interface {

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

type I interface {

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

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

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

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

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

常规的结构体类型:

type S struct {
b string
_ [10]int8
F func()

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

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

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

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK