Appearance
迭代器与组合模式
本章讲两个结构型模式:迭代器(Iterator)和组合(Composite)。迭代器模式在 Go 里有特殊地位——range 关键字是内建的迭代器,多数场景根本不需要手写。组合模式用于处理树形结构(文件系统、UI 组件树、组织架构),在 Go 里用接口 + 递归组合实现得很自然。我们还会看 Go 1.18+ 泛型如何让迭代器类型安全。
一、迭代器模式
1. 意图
迭代器模式:提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露该对象的内部表示。
2. 经典场景
- 遍历集合(数组、链表、树)而不暴露其内部结构。
- 支持多种遍历方式(前序、中序、后序)。
- 统一不同集合的遍历接口。
3. Go 的 range:内置迭代器
Go 的 range 关键字是语言层面支持的迭代器,覆盖了数组、切片、map、字符串、channel:
go
package main
import "fmt"
func main() {
// 切片
for i, v := range []int{10, 20, 30} {
fmt.Printf("slice[%d]=%d\n", i, v)
}
// map
for k, v := range map[string]int{"a": 1, "b": 2} {
fmt.Printf("map[%s]=%d\n", k, v)
}
// 字符串(按 rune 迭代)
for i, r := range "Go语言" {
fmt.Printf("string[%d]=%c\n", i, r)
}
// channel
ch := make(chan int, 3)
ch <- 1
ch <- 2
close(ch)
for v := range ch {
fmt.Printf("channel=%d\n", v)
}
}正因为 range 如此强大,Go 里很少需要手写迭代器。只有在以下场景才有必要:
- 自定义集合需要特殊遍历方式(如树的多种遍历)。
- 需要懒计算(流式处理)。
- 需要提供多种迭代器(正向、反向、过滤)。
4. Go 实现:Iterator 接口(Next/Value)
经典的迭代器接口:
go
package main
import "fmt"
// 迭代器接口
type Iterator[T any] interface {
HasNext() bool
Next() T
}
// 具体迭代器:遍历切片
type SliceIterator[T any] struct {
data []T
pos int
}
func NewSliceIterator[T any](data []T) *SliceIterator[T] {
return &SliceIterator[T]{data: data}
}
func (it *SliceIterator[T]) HasNext() bool {
return it.pos < len(it.data)
}
func (it *SliceIterator[T]) Next() T {
v := it.data[it.pos]
it.pos++
return v
}
// 容器接口
type Container[T any] interface {
GetIterator() Iterator[T]
}
type List[T any] struct {
items []T
}
func (l *List[T]) Add(v T) {
l.items = append(l.items, v)
}
func (l *List[T]) GetIterator() Iterator[T] {
return NewSliceIterator(l.items)
}
func main() {
list := &List[int]{}
list.Add(1)
list.Add(2)
list.Add(3)
it := list.GetIterator()
for it.HasNext() {
fmt.Println(it.Next())
}
}泛型让迭代器类型安全,不再需要 interface{} 和类型断言。这是 Go 1.18+ 相比老版本的显著进步。
5. channel 作为迭代器
Go 特有的写法:用 channel 当迭代器,发布者往 channel 写,消费者用 range 读。这种「生成器」模式特别适合懒计算:
go
package main
import "fmt"
// 用 channel 实现斐波那契数列的「无限迭代器」
func Fibonacci() <-chan int {
ch := make(chan int)
go func() {
a, b := 0, 1
for {
ch <- a
a, b = b, a+b
}
}()
return ch
}
func main() {
fib := Fibonacci()
for i := 0; i < 10; i++ {
fmt.Println(<-fib)
}
}channel 迭代器的优势:
- 天然支持无限序列(斐波那契、素数)。
- 天然支持并发(生产消费分离)。
- 可以用
range和select配合。 - 可以用
close通知结束。
反向遍历的 channel 迭代器:
go
package main
import "fmt"
func Reverse(data []int) <-chan int {
ch := make(chan int)
go func() {
for i := len(data) - 1; i >= 0; i-- {
ch <- data[i]
}
close(ch)
}()
return ch
}
func main() {
data := []int{1, 2, 3, 4, 5}
for v := range Reverse(data) {
fmt.Println(v)
}
}6. Go 1.23+ 的迭代器函数
Go 1.23 引入了 range over function 特性,让函数本身可以作为 range 的对象。一个标准的迭代函数签名是 func(yield func(V) bool):
go
package main
import "fmt"
// Go 1.23+ 的迭代器函数
func IntRange(start, end int) func(yield func(int) bool) {
return func(yield func(int) bool) {
for i := start; i < end; i++ {
if !yield(i) { // yield 返回 false 表示提前停止
return
}
}
}
}
func main() {
for v := range IntRange(1, 6) {
fmt.Println(v)
}
}这是 Go 官方推荐的迭代器写法(在 1.23+),比 Iterator 接口或 channel 都更轻量,且能直接用 range。如果你的项目用 1.23+,优先考虑这种写法。
7. 实战示例:自定义集合的迭代器
下面实现一个二叉搜索树,提供中序遍历迭代器:
go
package main
import "fmt"
type TreeNode struct {
Value int
Left *TreeNode
Right *TreeNode
}
type BST struct {
root *TreeNode
}
func (b *BST) Insert(v int) {
b.root = insert(b.root, v)
}
func insert(node *TreeNode, v int) *TreeNode {
if node == nil {
return &TreeNode{Value: v}
}
if v < node.Value {
node.Left = insert(node.Left, v)
} else {
node.Right = insert(node.Right, v)
}
return node
}
// 中序遍历迭代器(用 channel)
func (b *BST) InOrder() <-chan int {
ch := make(chan int)
go func() {
inOrder(b.root, ch)
close(ch)
}()
return ch
}
func inOrder(node *TreeNode, ch chan<- int) {
if node == nil {
return
}
inOrder(node.Left, ch)
ch <- node.Value
inOrder(node.Right, ch)
}
func main() {
tree := &BST{}
for _, v := range []int{5, 3, 7, 1, 4, 6, 8} {
tree.Insert(v)
}
fmt.Println("中序遍历(升序):")
for v := range tree.InOrder() {
fmt.Print(v, " ")
}
fmt.Println()
}BST 的中序遍历天然得到升序序列,用 channel 实现迭代器让调用方可以用 for range 优雅遍历,不需要知道树的内部结构。
二、组合模式
1. 意图
组合模式:将对象组合成树形结构以表示「整体-部分」的层次结构。组合模式让客户端可以统一地处理单个对象和组合对象。
2. 经典场景
- 文件系统:目录包含文件和子目录,统一处理。
- UI 组件树:容器包含子组件,递归渲染。
- 组织架构:部门包含子部门和员工。
- 算术表达式:表达式由子表达式组成。
3. Go 实现:统一接口 + 递归组合
组合模式的核心:定义一个统一接口,叶子和容器都实现它,容器内部递归持有子节点。
go
package main
import "fmt"
// 统一组件接口
type Component interface {
Name() string
Size() int
Display(indent string)
}
// 叶子:文件
type File struct {
name string
size int
}
func NewFile(name string, size int) *File {
return &File{name: name, size: size}
}
func (f *File) Name() string { return f.name }
func (f *File) Size() int { return f.size }
func (f *File) Display(indent string) {
fmt.Printf("%s📄 %s (%d bytes)\n", indent, f.name, f.size)
}
// 容器:目录
type Directory struct {
name string
children []Component
}
func NewDirectory(name string) *Directory {
return &Directory{name: name}
}
func (d *Directory) Name() string { return d.name }
func (d *Directory) Add(c Component) {
d.children = append(d.children, c)
}
// 目录的大小是所有子项大小之和
func (d *Directory) Size() int {
total := 0
for _, c := range d.children {
total += c.Size()
}
return total
}
func (d *Directory) Display(indent string) {
fmt.Printf("%s📁 %s/ (%d bytes)\n", indent, d.name, d.Size())
for _, c := range d.children {
c.Display(indent + " ")
}
}
func main() {
root := NewDirectory("root")
src := NewDirectory("src")
src.Add(NewFile("main.go", 1024))
src.Add(NewFile("util.go", 512))
docs := NewDirectory("docs")
docs.Add(NewFile("readme.md", 256))
root.Add(src)
root.Add(docs)
root.Add(NewFile("go.mod", 64))
root.Display("")
fmt.Printf("总大小: %d bytes\n", root.Size())
}关键点:
File(叶子)和Directory(容器)都实现Component接口。Directory.Size()递归调用子节点的Size(),自动汇总。Display也是递归的,统一处理叶子和容器。- 客户端
main只跟Component打交道,不区分文件还是目录。
4. 透明组合 vs 安全组合
组合模式有两种风格:
- 透明组合:叶子也实现「Add/Remove」等方法,但调用时报错。客户端不区分叶子/容器,但可能误操作。
- 安全组合:只有容器有「Add/Remove」方法,叶子没有。客户端需要类型断言才能 Add,但不会误操作。
上面的例子是「安全组合」——Add 只在 Directory 上,File 没有。Go 社区普遍推荐安全组合,因为:
- Go 没有抽象基类,无法让叶子提供「默认报错的 Add」。
- Go 的接口是隐式的,让
File也实现Add会污染接口。 - 安全组合更符合「显式优于隐式」的 Go 哲学。
如果一定要透明组合,可以把 Add 放进接口,叶子实现成 no-op:
go
package main
import "fmt"
type Component interface {
Name() string
Size() int
Add(Component) // 叶子也会实现,但 no-op
}
type File struct {
name string
size int
}
func (f *File) Name() string { return f.name }
func (f *File) Size() int { return f.size }
func (f *File) Add(Component) {
fmt.Println("文件不能 Add")
}
type Directory struct {
name string
children []Component
}
func (d *Directory) Name() string { return d.name }
func (d *Directory) Size() int {
total := 0
for _, c := range d.children {
total += c.Size()
}
return total
}
func (d *Directory) Add(c Component) {
d.children = append(d.children, c)
}
func main() {
root := &Directory{name: "root"}
root.Add(&File{name: "a.txt", size: 10})
file := &File{name: "b.txt", size: 20}
file.Add(&File{name: "c.txt", size: 5}) // 透明但无效
fmt.Println("root size:", root.Size())
}5. 实战示例:UI 组件树
go
package main
import "fmt"
type UIComponent interface {
Render() string
Children() []UIComponent
}
// 叶子:按钮
type Button struct {
label string
}
func (b *Button) Render() string { return fmt.Sprintf("[Button: %s]", b.label) }
func (b *Button) Children() []UIComponent { return nil }
// 叶子:文本
type Text struct {
content string
}
func (t *Text) Render() string { return fmt.Sprintf("[Text: %s]", t.content) }
func (t *Text) Children() []UIComponent { return nil }
// 容器:面板
type Panel struct {
name string
children []UIComponent
}
func (p *Panel) Add(c UIComponent) {
p.children = append(p.children, c)
}
func (p *Panel) Render() string {
return fmt.Sprintf("[Panel: %s]", p.name)
}
func (p *Panel) Children() []UIComponent {
return p.children
}
// 递归渲染
func renderTree(c UIComponent, indent string) {
fmt.Println(indent + c.Render())
for _, child := range c.Children() {
renderTree(child, indent+" ")
}
}
func main() {
root := &Panel{name: "Window"}
header := &Panel{name: "Header"}
header.Add(&Text{content: "标题"})
header.Add(&Button{label: "关闭"})
body := &Panel{name: "Body"}
body.Add(&Text{content: "内容区域"})
body.Add(&Button{label: "提交"})
body.Add(&Button{label: "取消"})
root.Add(header)
root.Add(body)
renderTree(root, "")
}UI 组件树是组合模式的招牌应用——容器可以嵌套任意层,叶子是原子组件,渲染时统一递归处理。前端框架(React、Vue)的组件树本质上就是这个结构。
6. 与访问者模式的关系
组合模式常和访问者模式(Visitor)搭配:在不变组件接口的前提下,给整棵树增加新操作。但 Go 对访问者模式支持不好(没有方法重载,双分派要手写 type switch),所以实践中很少用。需要新操作时,通常直接在组件接口加方法,或者用 type switch 分发。
三、迭代器与组合的结合
迭代器和组合经常一起用:对组合结构做遍历,就是迭代器。下面演示对文件树做深度优先遍历:
go
package main
import "fmt"
type Component interface {
Name() string
}
type File struct{ name string }
func (f *File) Name() string { return f.name }
type Directory struct {
name string
children []Component
}
func (d *Directory) Name() string { return d.name }
func (d *Directory) Add(c Component) { d.children = append(d.children, c) }
func (d *Directory) Children() []Component { return d.children }
// 用 channel 实现深度优先迭代器
func DFS(root Component) <-chan Component {
ch := make(chan Component)
go func() {
var walk func(c Component)
walk = func(c Component) {
ch <- c
if dir, ok := c.(*Directory); ok {
for _, child := range dir.Children() {
walk(child)
}
}
}
walk(root)
close(ch)
}()
return ch
}
func main() {
root := &Directory{name: "root"}
src := &Directory{name: "src"}
src.Add(&File{name: "main.go"})
src.Add(&File{name: "util.go"})
docs := &Directory{name: "docs"}
docs.Add(&File{name: "readme.md"})
root.Add(src)
root.Add(docs)
root.Add(&File{name: "go.mod"})
fmt.Println("深度优先遍历:")
for c := range DFS(root) {
fmt.Println(" -", c.Name())
}
}迭代器(DFS)把树「拍平」成一个序列,调用方可以统一处理所有节点,不关心树的层级。这是组合 + 迭代器的经典组合。
四、标准库中的应用
1. container/list 的迭代
container/list 是双向链表,没有实现 Iterator 接口,而是提供 Front()/Next() 让你手动遍历:
go
package main
import (
"container/list"
"fmt"
)
func main() {
l := list.New()
l.PushBack(1)
l.PushBack(2)
l.PushBack(3)
for e := l.Front(); e != nil; e = e.Next() {
fmt.Println(e.Value)
}
}这是「外部迭代器」的原始形态——没有 HasNext/Next 接口,直接用元素的 Next 方法。Go 标准库倾向这种轻量做法,不为每个集合造一个 Iterator 类型。
2. filepath.Walk 对目录树的遍历
filepath.Walk 是组合 + 迭代器的标准库实例,它递归遍历目录树,对每个节点调用回调:
go
package main
import (
"fmt"
"io/fs"
"path/filepath"
)
func main() {
count := 0
// 注意:实际运行需要一个真实目录,这里用 "." 演示
_ = filepath.Walk(".", func(path string, info fs.FileInfo, err error) error {
if err != nil {
return nil
}
count++
if count <= 3 {
fmt.Println(path, info.IsDir())
}
return nil
})
fmt.Println("总节点数:", count)
}filepath.Walk 把「文件系统这棵组合树」抽象成「一个回调序列」,调用方不需要关心递归细节——这就是组合 + 迭代器的威力。
五、小结
- 迭代器模式提供统一遍历接口,不暴露集合内部结构。Go 的
range是内建迭代器,覆盖切片、map、字符串、channel,多数场景不需要手写。 - 手写迭代器有三种方式:Iterator 接口(HasNext/Next,可用泛型类型安全)、channel(天然支持无限序列和并发)、Go 1.23+ 的迭代器函数(
func(yield func(V) bool),可直接range)。 - channel 迭代器是 Go 特色:天然懒计算、天然支持并发、可与
select/context组合。 - Go 1.18+ 泛型让迭代器摆脱
interface{},类型安全;Go 1.23+ 的 range over function 是官方推荐的现代写法。 - 组合模式把对象组织成树形结构,统一处理叶子和容器。Go 用「接口 + 递归组合」实现,容器递归持有子节点。
- 组合分「透明组合」(叶子也有 Add,但 no-op)和「安全组合」(只有容器有 Add)。Go 推荐安全组合,符合「显式优于隐式」。
- 迭代器和组合常结合:对组合树做深度/广度优先遍历,把树拍平成序列。
filepath.Walk是标准库中的典型例子。 - 标准库
container/list用「外部指针 + Next 方法」实现轻量迭代,filepath.Walk用回调遍历目录树,都是值得学习的范例。
下一篇是本系列最后一篇,总结 Go 特有的惯用模式和需要警惕的反模式。