Golang中嵌入聚合与组合的详细解析
Golang中嵌入聚合与组合的详细解析 大家好,
今天在回顾一些用Go实现的设计模式并尝试将其转换为经典的UML类图时,我感到有些困惑。
我对这门语言的理解是,它没有继承,只有嵌入。一个嵌入的结构体/接口可以使用另一个“基础”结构体/接口的方法和字段。
有些人将嵌入称为聚合。因此我做了一些研究,这让我想起了很久以前在Java中遇到的关于聚合与组合的困惑。 在我看来,区分两者更务实的答案是:对于组合,如果对A的某个特定实例不再存在引用,那么它的B实例就会被销毁,而聚合则不是这样。
嵌入是通过引用结构体类型而不为字段分配名称来实现的。在内部,使用的是结构体类型的名称,如下所示:
type Car struct {
Engine
}
如果我添加一个字段名,那么它就不再是嵌入。
所以,在我看来,它要么是组合,要么是聚合。
如果Engine结构体是通过值复制使用的,那么它就是组合,因为一旦Car实例被销毁,该实例也会被销毁。
如果Engine结构体是一个指针,那么这取决于Engine类型是否在其他地方被使用。
我的理解对吗,还是我遗漏了什么?
type Car struct {
e Engine
}
type Car struct {
e *Engine
}
提前感谢您的帮助。
更多关于Golang中嵌入聚合与组合的详细解析的实战教程也可以访问 https://www.itying.com/category-94-b0.html
BigBoulard: 我的理解是对的吗,还是我遗漏了什么?
在我看来,你对聚合和组合的理解似乎是正确的。
更多关于Golang中嵌入聚合与组合的详细解析的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
感谢 @skillian。我现在更明白了。
祝好。
你好 Jeff,
感谢你对此的反馈。我想补充一点,切片、映射以及通道都是“按指针传递”(或者说是“按引用传递”),但它们并非像在 C++ 中那样以经典方式创建。
由于Go是一门按值传递的语言,你需要将对象的指针传递给函数,以便能够在函数中修改该对象。因此,我发现自己使用对象指针的频率远高于直接使用对象本身。
纯粹主义者会说应该通过通道传递对象本身,这样做也有其道理,尤其是当你打算编写支持并发的程序时。
但是,如果我不是为了并发而编程,我就直接使用对象的指针。
这可能是任何一种情况;在Go语言中,这种关系不是通过语法来传达的。嵌入一个结构体值(而不是指向结构体的指针)几乎肯定意味着组合。嵌入一个指针可能意味着关联、组合或聚合。Go语言使用文档和清晰的代码来传达结构体字段之间的关系。
type SinglyLinkedListNode struct {
Value interface{}
Next *SinglyLinkedListNode
}
type File struct {
*Dir
Name string
}
就我个人而言,我认为这两个结构体都相当清晰地传达了它们分别与 SinglyLinkedListNode 和 Dir 的关系。在我假设的包中,我可以将 File 重构为更像这样的形式:
package paths
type elem struct {
// Dir 拥有此文件系统元素。
// 除根目录 Dir 外,所有元素都包含在一个目录中。
*Dir
// 此文件系统元素的名称。
Name string
}
// File 是文件系统上的文件。
type File struct {
elem
}
// Dir 是文件系统中包含文件和其他目录的目录。
type Dir struct {
elem
}
在这里,我们使用了符合Go语言惯例的写法和注释,向开发者和用户传达哪些关系是关联、组合或聚合。
关于Go中嵌入与组合/聚合关系的专业解析
你的理解基本正确,但需要更精确地区分Go的嵌入机制与传统的UML关系。让我们通过代码示例来详细说明。
1. Go嵌入的本质
Go的嵌入(Embedding)是一种语法糖,它允许被嵌入类型的方法和字段提升到外层类型。但这不等于传统的继承或组合关系。
// 嵌入示例
type Engine struct {
Power int
}
func (e Engine) Start() {
fmt.Println("Engine started")
}
// 嵌入 - 方法提升
type Car struct {
Engine // 嵌入,不是字段名
}
func main() {
c := Car{Engine{Power: 150}}
c.Start() // 可以直接调用Engine的方法
fmt.Println(c.Power) // 可以直接访问Engine的字段
}
2. 值类型嵌入 vs 指针类型嵌入
值类型嵌入(类似组合)
type Car struct {
Engine // 值类型嵌入
}
func main() {
c := Car{Engine{Power: 150}}
// Engine是Car的一部分,生命周期与Car绑定
// 当Car被销毁时,这个Engine实例也被销毁
// 这符合组合的定义:部分不能独立于整体存在
}
指针类型嵌入(可能是聚合)
type Car struct {
*Engine // 指针类型嵌入
}
func main() {
engine := &Engine{Power: 200}
c1 := Car{Engine: engine}
c2 := Car{Engine: engine} // 多个Car共享同一个Engine
// 这种情况下,Engine的生命周期不依赖于单个Car
// 更符合聚合的定义:部分可以独立于整体存在
}
3. 明确字段名的情况
// 这不是嵌入,只是普通的字段
type Car struct {
e Engine // 有字段名,不是嵌入
}
func main() {
c := Car{e: Engine{Power: 150}}
// c.Start() // 错误:不能直接调用
c.e.Start() // 必须通过字段名访问
}
4. 实际的内存布局证明
type Engine struct {
Power int
}
type Car1 struct {
Engine // 嵌入
}
type Car2 struct {
e Engine // 字段
}
func main() {
c1 := Car1{Engine{100}}
c2 := Car2{e: Engine{100}}
// 内存布局相同,但方法集不同
fmt.Printf("Size of Car1: %d\n", unsafe.Sizeof(c1)) // 8
fmt.Printf("Size of Car2: %d\n", unsafe.Sizeof(c2)) // 8
// 方法集验证
var _ interface{} = c1
// Car1的方法集包含Engine的方法
// Car2的方法集不包含Engine的方法
}
5. 接口嵌入的特殊情况
type Reader interface {
Read([]byte) (int, error)
}
type Writer interface {
Write([]byte) (int, error)
}
// 接口嵌入
type ReadWriter interface {
Reader
Writer
}
// 这纯粹是接口组合,不涉及生命周期问题
关键区别总结:
- 语法层面:嵌入使用类型名而不指定字段名
- 方法集:嵌入会提升方法,有字段名则不会
- 生命周期:
- 值类型嵌入:生命周期绑定,类似组合
- 指针类型嵌入:生命周期可能独立,类似聚合
- UML映射:Go没有直接的UML对应关系,需要根据具体使用场景判断
你的理解方向正确,但需要记住:Go的嵌入主要是语法特性,而组合/聚合是设计概念。在实际设计中,应该根据业务逻辑和生命周期需求来选择使用值类型还是指针类型,而不是单纯依赖语法特性。


