理解 Go 语言的 Embedding 特性

2020-11-19 ⏳2.4分钟(0.9千字) 🕸️

Go 语言没有类,也就没有继承,只能通过 embedding 特性(为行文方便,下面统一称之为嵌入)组合各种功能。

最简单的就是 interface 嵌入 interface。比如我们已经定义了 Reader 和 Writer 接口,

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

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

我们就可以把它们组合起来定义一个新的 ReadWriter 接口

type ReadWriter interface {
    Reader // 嵌入 Reader
    Writer // 嵌入 Writer
}

这样的写法跟下面的写法是等效的,但可以避免重复定义(Don’t repeat yourself)。

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

Go 语言还支持在 struct 中嵌入 interface,但这就需要一点理解成本了。

假如我们定义一个叫 ReadWriter 的 struct,我们希望它实现 Reader 和 Writer 接口,而且我们也已经有对应的 reader 和 writer 的实例。如果没有嵌入功能,我们可能需成这个样子:

type ReadWriter struct {
    reader *Reader
    writer *Writer
}
func (rw *ReadWriter) Read(p []byte) (n int, err error) {
    return rw.reader.Read(p)
}
func (rw *ReadWriter) Write(p []byte) (n int, err error) {
    return rw.writer.Write(p)
}

我们给 ReadWriter 定义了 Read 和 Write 方法,但又没做具体的事,只是将功能委托给了 reader 和 writer 对象处理。显然,重复写这样的代码没什么意思。我们可以利用 go 语言的 embedding 来消除这种重复,于就就有了这样的代码

type ReadWriter struct {
    *Reader
    *Writer
}

大家有看出区别吗?对,就是把前面的字段名 reader 和 writer 去掉。这个时候编译器就知道你是想把 Reader 和 Writer 指针嵌入到 ReadWriter 结构。如果你现在有一个 rw *ReaderWriter 指针,你就可以直接调用 rw.Read()rw.Write()方法,就像是 ReadWriter 自己定义了这些方法一样。

但是,没有字段名怎么初始化 ReadWriter 结构呢?这是初学者很容易迷惑的地方。对于嵌入,go 语言约定其字段名等同于类型名(去掉包名),不需要显示指定。

也就是说,ReadWrite 会有一个叫 Reader 一个叫 Writer 的属性,我们可以通过它们初始化 ReadeWriter,例如

rw := &ReadWriter{Reader: r, Writer: w}

我们有两种方式来调用嵌入方法

rw.Reade(p)
rw.Reader.Read(p)

第一种可以通过 rw 直接调用,第二种是可以通过约定的 rw.Reader 来调用。一般来说,使用嵌入都是希望采用第一种调用方式。

但是,如果 Reader 接口有一个方法也就 Reader 呢?这个时候就会产生命名冲突

rw.Reader() // 调用 rw.Reader 的 Reader 方法
rw.Reader   // 读取 rw.Reader 实例

为了消除这种冲突,go 语言规定外层 struct 的字段和方法优先,原文是这样写的

First, a field or method X hides any other item X in a more deeply nested part of the type.

也就说,如果 Reader 接口有一个方法也就 Reader,那么 rw.Reader 指的是 Reader 这个实例,你不能通过 rw.Reader() 来调用对应的 Reader 方法。

就是因为早期对这一规则理解不足,我们的 sniper 框架 一直不支持在 service 中定义同名 method,现在终于修好了

其实还有一种冲突。ReadWriter 嵌入 Reader 后会拥一个 Reader 的字段。但如果 ReadWriter 自也定义了一个名为 Reader 的字段会有什么问题呢?

Second, if the same name appears at the same nesting level, it is usually an error; However, if the duplicate name is never mentioned in the program outside the type definition, it is OK.

只要不引用它们,就不会报错。不过我还是建议大家不要炫技,别写这种难以理解的代码。

另外,官方对于 embedding 特性的权威说明在这里,网上还有一篇Type embedding in Go也非常不错。

好了,行文到此也就差不多了。希望能给大家带来一点启发