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
的调用效果相同。
这个例子还展示了嵌套字段的方法“继承”行为的一个微妙之处;当调用 Base
的 Describe
时,不管通过哪个嵌套结构体调用它,它都传递了一个 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.Reader
和 bufio.Writer
的方法,因此实现了 io.ReadWriter
。这种嵌套省去了大量的重复代码,胶水代码。
示例:context.timerCtx
在 context
包中还有一个更精妙复杂的例子:timerCtx
:
type timerCtx struct {
cancelCtx
timer *time.Timer
deadline time.Time
}
为了实现 Context
接口,timerCtx
嵌套了 cancelCtx
,后者实现了所需的4个方法中的3个(Done
、Err
和 Value
)。然后,它只需在自身上实现第四个方法 - Deadline
。