组合模式适合处理树形结构,如文件系统、AST等,核心是容器与叶子实现同一接口;Go中通过接口嵌入和结构体组合实现,需注意nil切片、循环引用和类型断言问题。

组合模式适合处理树形结构的场景
Go 语言没有类继承,但组合模式依然适用——它本质是让「容器」和「叶子」实现同一接口,从而统一处理。典型适用结构是天然具有父子层级、可递归遍历的数据,比如文件系统、DOM 节点、AST 抽象语法树、组织架构、菜单栏嵌套项。
关键判断依据:HasChildren() 是否有意义、Accept() 或 Render() 等操作能否在叶子与容器上保持语义一致。如果某节点既可能含子节点、又可能被当作终端值使用(如 JSON 中的 object 和 array),组合模式就比硬编码 if-else 分支更易维护。
Go 中用嵌入接口 + 结构体组合实现
Go 不靠继承,而是靠结构体字段嵌入(embedding)+ 接口约束来模拟组合。核心是定义一个公共接口(如 Component),再让 Leaf 和 Composite 都实现它;Composite 内部持有一个 []Component 切片,而不是具体类型切片。
-
Composite的方法里调用子节点的同名方法时,必须通过接口变量调用(不能直接调用具体结构体方法),否则无法多态 - 避免在
Composite中暴露children切片给外部修改,应提供Add(c Component)和Remove(c Component)方法封装 - 若需深度遍历,递归入口应始终基于接口类型(如
func (c *Composite) Traverse(fn func(Component))),而非*Composite或*Leaf
type Component interface {
GetName() string
GetSize() int
}
type File struct {
name string
size int
}
func (f *File) GetName() string { return f.name }
func (f *File) GetSize() int { return f.size }
type Folder struct {
name string
children []Component // 注意:不是 []*File 或 []Folder
}
func (f *Folder) GetName() string { return f.name }
func (f *Folder) GetSize() int {
total := 0
for _, c := range f.children {
total += c.GetSize()
}
return total
}
容易踩的坑:nil 指针、循环引用、接口断言失败
Go 的组合模式实操中,三个高频问题:
立即学习“go语言免费学习笔记(深入)”;
- 初始化
children切片为nil,后续append不报错但遍历时 panic —— 应显式初始化为[]Component{} - 误将父节点加入自身
children(如配置错误或动态拼接逻辑缺陷),导致递归遍历时栈溢出 —— 可在Add中加简单环检测(例如记录已访问地址) - 需要获取具体类型做特殊处理时(如只对
*File计算哈希),直接c.(*File)会 panic;应改用类型断言if f, ok := c.(*File); ok { ... }
不适合组合模式的层级关系
不是所有有“层级”的结构都适合。以下情况建议绕开组合模式,改用更直白的结构或函数式处理:
- 层级固定且极浅(如只有两级:Category → Product),强行抽象反而增加间接层
- 不同层级语义差异极大(如“用户”和“订单”虽有关联,但不构成容器/元素关系),此时用关联字段(
UserID uint)或查询函数更自然 - 需要频繁按路径随机访问某个节点(如
/a/b/c),而组合模式通常只支持深度/广度优先遍历,查路径得自己写查找逻辑,不如用 map[string]Component 做索引
组合模式的价值不在“能建模”,而在“让新增节点类型、扩展遍历行为时不改已有调用方代码”。如果项目里几乎不会加新节点类型,或遍历逻辑总在变,那它很可能只是增加了理解成本。










