Golang - Go语言中的嵌入【第一部分】:结构体嵌入结构体

Go 语言并不支持传统意义上的继承,相反,它提倡通过组合来扩展类型的功能。这并不是 Go 所特有的概念,继承之上的组合是 OOP 的一个众所周知的原则,在《设计模式》一书的第一章就有介绍。Embedding(嵌入)是 Go 语言一个重要的特性,有了它使得组合更加方便、更有用。虽然 Go 力求简单,但某种程度上嵌入增加了语言的复杂度,如果使用不当会导致 bug 出现。在这一系列文章中,我将介绍 Go 支持的不同种类的嵌入,并提供真实的代码示例(大部分来自 Go 语言的标准库)。

Go 语言里面有三种不同类型的嵌入:

结构体嵌入结构体

我们将从一个简单的示例开始,演示将一个结构体嵌入另一个结构体:

1
2
3
4
5
6
7
type Base struct {
  b int
}
type Container struct {     // Container 是嵌入结构体
  Base                      // Base 是被嵌入的结构体
  c string
}

【译者注】为了便于理解,被嵌入的结构体我们称为内部类型;嵌入结构体称为外部类型。

Container 的实例现在也会有 b 字段,在 Go 语言规范中,它被称为一个提升(promoted)字段,我们可以像访问成员 c 那样访问它。

1
2
3
4
5
co := Container{}
co.b = 1
co.c = "string"
fmt.Printf("co -> {b: %v, c: %v}\n"co.bco.c)

然而,当使用结构体字面量时,我们需要将被嵌入的结构体整体初始化,而不是单单对其字段初始化。

1
2
3
co := Container{Base: Base{b10}, c: "foo"}
fmt.Printf("co -> {b: %v, c: %v}\n", co.b, co.c)

我们可以这样 co.Base.b 访问 b,co.b 是一种简介的访问方式,

方法

上面的机制同样适用于带有方法的结构体。假设 Base 有一个可用的方法:

1
2
3
4
func (base Base) Describe() string {
  return fmt.Sprintf("base %d belongs to us"base.b)
}

我们仍然可以通过 Container 实例调用该方法,就好像 Container 拥有该方法一样:

1
2
fmt.Println(cc.Describe())

为了更好地理解这个调用的机制,我们可以想象 Container 有一个明确的 Base 类型的字段和一个明确的 Describe 方法来转发这个调用。

1
2
3
4
5
6
7
type Container struct {
  base Base
  c string
}
func (cont Container) Describe() string {
  return cont.base.Describe()
}

这种方式与之前的调用方式是同样的效果。

被嵌入的字段被覆盖

当内部类型和外部类型都有一个字段 x,嵌入时会发生什么呢?这种情况下,通过嵌入结构访问 x 时,我们访问到的是外部类型的 x,而不是内部类型的 x:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type Base struct {
  b   int
  tag string
}
func (base Base) DescribeTag() string {
  return fmt.Sprintf("Base tag is %s"base.tag)
}
type Container struct {
  Base
  c   string
  tag string
}
func (co Container) DescribeTag() string {
  return fmt.Sprintf("Container tag is %s"co.tag)
}

像下面这样使用:

1
2
3
4
b := Base{b10, tag: "b's tag"}
co := Container{Base: b, c: "foo", tag: "co's tag"}
fmt.Println(b.DescribeTag())
fmt.Println(co.DescribeTag())

输出:

1
2
3
Base tag is b's tag
Container tag is co's tag

注意,当我们访问 co.tag 时,我们访问的是 Container 的 tag 字段,而不是 base 的 tag 字段。我们可以通过 co.Base.tag 方式访问 base 的 tag 字段。

下面几个例子都来自 Go 语言的标准库。

示例:sync.Mutex

Go 语言里结构体嵌入的一个经典例子是 sync.Mutex,比如 crypto/tls/common.go 文件中的 lruSessionCache:

1
2
3
4
5
6
7
type lruSessionCache struct {
  sync.Mutex
  m        map[string]*list.Element
  q        *list.List
  capacity int
}

注意这里的嵌入结构体 sync.Mutex,如果 cache 是 lruSessionCache 的实例,我们可以直接调用方法 cache.Lock() 和 cache.Unlock(),这在一些场景下非常有用。如果 lock 是结构体的公共 API 的一部分,嵌入 mutex 会很方便,而且不需要额外地转发方法。

不过,也有可能是该结构体的内部方法中使用该锁,并没有对外公开。在这种情况下,我不会嵌入 sync.Mutex,而是让它成为一个未导出的字段(如 mu sync.Mutex)。

示例:elf.FileHeader

结构体中嵌入 sync.Mutex 是一个很好的示例,外部类型可以获得新的行为能力,比如加锁、解锁。这里有一个不同的例子,关于数据嵌入。在 debug/elf/file.go中,我们找到描述 ELF 文件的结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// A FileHeader represents an ELF file header.
type FileHeader struct {
  Class      Class
  Data       Data
  Version    Version
  OSABI      OSABI
  ABIVersion uint8
  ByteOrder  binary.ByteOrder
  Type       Type
  Machine    Machine
  Entry      uint64
}
// A File represents an open ELF file.
type File struct {
  FileHeader
  Sections  []*Section
  Progs     []*Prog
  closer    io.Closer
  gnuNeed   []verneed
  gnuVersym []byte
}

elf 包的开发者直接在 File 结构体中列出了所有的文件头字段,但是将这些字段归纳在 FileHeader 结构体中,这是一个很好的数据分区的示例。写代码的人可能会想要单独初始化或者操作文件头,而这种嵌入式的结构设计可以很好地做到这点。

我们可以在文件 compress/gzip/gunzip.go 中找到另一个相同的例子,gzip.Reader 嵌入了 gzip.Header 结构体,这是一个非常好的嵌入数据重用的例子,因为 gzip.Writer 也嵌入了 gzip.Header,所以这有助于避免重复拷贝。

示例:bufio.ReadWriter

由于嵌入结构体(外部类型)“继承”了被嵌入结构体的方法,所以嵌入结构体可以成为实现接口的一个有用工具。

我们可以看看 bufio 包,里面有一个 bufio.Reader 类型,*bufio.Reader 类型实现了 io.Reader 接口,同样地 *bufio.Writer 实现了 io.Writer 接口。那我们该如何创建一个实现了 io.ReadWriter 接口的类型呢?

通过嵌入可以很容易实现:

1
2
3
4
5
type ReadWriter struct {
  *Reader
  *Writer
}

类型“继承”了 *bufio.Reader*bufio.Writer 的所有方法,所以实现了 io.ReadWriter 接口,不需要额外地命名其他字段,也不需要明确地转发方法,就能轻松地实现。

另一个类似的例子,context 包里面的 timerCtx 结构体:

1
2
3
4
5
type timerCtx struct {
  cancelCtx
  timer *time.Timer
  deadline time.Time
}

为了实现 Context 接口,timerCtx 结构体嵌入了 cancelCtx,它已经实现了 4 个方法中的 3 个方法:Done()、Err() 和 Value(),所以 timerCtx 只需要实现属于自己的 Deadline() 方法即可。

via: https://eli.thegreenplace.net/2020/embedding-in-go-part-1-structs-in-structs/
作者:Eli Bendersky


Golang - Go语言中的嵌入【第一部分】:结构体嵌入结构体
https://www.boer.xyz/2023/03/15/golang-structs-in-structs/
作者
boer
发布于
2023年3月15日
许可协议