Go嵌套:结构体嵌套结构体

创建于 2023年12月15日修改于 2024年5月5日
GoEmbedding

Go嵌套


原文:https://eli.thegreenplace.net/2020/embedding-in-go-part-1-structs-in-structs
翻译:literank.cn

Go 不支持传统意义上的继承;相反,它鼓励使用组合的方式来扩展类型的功能。这不是 Go 特有的概念。组合优于继承是面向对象编程的常见原则,并且在《设计模式》书中的第一章就有所体现。

嵌套(Embedding)是 Go 中一个重要特性,它使得组合更易实现。

结构体中嵌套结构体

让我们从一个简单例子开始,将一个结构体嵌套到另一个结构体中:

type Base struct {
  b int
}

type Container struct {     // Container 是嵌套结构体
  Base                      // Base 是被嵌套的结构体
  c string
}

Container 的实例现在也将拥有字段 b。在Go 规范中,它被称为提升字段(promoted field)。可以像访问字段 c 一样访问它:

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

不过,当使用结构字面量时,我们必须将嵌套的结构体作为整体进行初始化,而不是初始化其字段。在组合后的结构体里用字面量初始化时,提升字段不能直接用作字段名称:

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

注意,直接访问 co.b 是一种语法糖;我们也可以显式地使用 co.Base.b

方法

嵌套结构体也为方法的组合复用带来了方便。假设 Base 有以下方法:

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

我们现在可以在 Container 的实例上调用它,就好像它也有这个方法一样:

fmt.Println(co.Describe())

为了更好地理解这个调用的机制,可以将 Container 当做具有 Base 类型的显式字段和一个调用转发的显式 Describe 方法:

type Container struct {
  base Base
  c string
}

func (cont Container) Describe() string {
  return cont.base.Describe()
}

在这个写法的 Container 上调用 Describe 与前面使用嵌套的 Container 的调用效果相同。

这个例子还展示了嵌套字段的方法“继承”行为的一个微妙之处;当调用 BaseDescribe 时,不管通过哪个嵌套结构体调用它,它都传递了一个 Base 接收者到方法。 这与其他语言(如 Python 和 C++ )中的继承方式不同,它们继承的方法会得到一个发起调用的子类的引用。这是 Go 中嵌套“继承”与传统继承区别的关键点。

嵌套字段的遮蔽

如果嵌套结构体有一个字段 x,被嵌套的结构体也有一个字段 x,那么外层的字段会遮蔽内层的字段。

如下所示:

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)
}

如下使用时:

b := Base{b: 10, tag: "b's tag"}
co := Container{Base: b, c: "foo", tag: "co's tag"}

fmt.Println(b.DescribeTag())
fmt.Println(co.DescribeTag())

输出如下:

Base tag is b's tag
Container tag is co's tag

如果想访问内层的字段,可以使用 co.Base.tag 显式访问。

以下示例都来自Go标准库。

示例:sync.Mutex

Go 中嵌套结构体的一个经典例子是 sync.Mutex

crypto/tls/common.go 中有 lruSessionCache

type lruSessionCache struct {
  sync.Mutex
  m        map[string]*list.Element
  q        *list.List
  capacity int
}

该结构体嵌套了 sync.Mutex;现在,如果 cache 是一个 lruSessionCache 类型的对象,我们可以简单地调用 cache.Lock()cache.Unlock() 来加解锁。 如果加解锁是该结构体公共 API 的一部分,那么嵌入 Mutex 是最方便的。

然而,有些情况下我们希望锁只在结构体的方法里内部使用,不想暴露给其用户。此情况下,可将其作为私有字段(如 mu sync.Mutex)。

示例:elf.FileHeader

嵌套 sync.Mutex 的示例很好地演示了通过嵌套实现新行为的结构体嵌套。本示例主要涉及嵌套实现数据复用。在 debug/elf/file.go 中,我们找到描述ELF文件的结构体:

// 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 中列出所有头字段,但将其放在一个单独的结构体中是一个更好的做法。用户代码可能希望分开文件和文件头的初始化操作,嵌套设计使这个操作变得自然顺畅。

compress/gzip/gunzip.go 中也可以找到类似的例子,其中 gzip.Reader 嵌套 gzip.Header。这是一个很好的嵌套用于数据重用的例子,因为 gzip.Writer 也嵌套 gzip.Header,这可以避免重复代码。

示例:bufio.ReadWriter

因为嵌套结构体“继承”(如上所述,非传统意义上的继承)被嵌套结构体的方法,所以嵌套也可以是实现接口的得力工具。

比如 bufio 包,其中有类型 bufio.Reader。指向这个类型的指针实现了 io.Reader 接口。对应的 bufio.Writer 指针也是如此,它实现了 io.Writer 接口。我们如何创建一个 bufio 类型,让它实现 io.ReadWriter 接口呢?

通过嵌套很容易实现:

type ReadWriter struct {
  *Reader
  *Writer
}

这个类型继承了 bufio.Readerbufio.Writer 的方法,因此实现了 io.ReadWriter。这种嵌套省去了大量的重复代码,胶水代码。

示例:context.timerCtx

context 包中还有一个更精妙复杂的例子:timerCtx

type timerCtx struct {
  cancelCtx
  timer *time.Timer
  deadline time.Time
}

为了实现 Context 接口,timerCtx 嵌套了 cancelCtx,后者实现了所需的4个方法中的3个(DoneErrValue)。然后,它只需在自身上实现第四个方法 - Deadline