Skip to content

迭代器与组合模式

本章讲两个结构型模式:迭代器(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 迭代器的优势:

  • 天然支持无限序列(斐波那契、素数)。
  • 天然支持并发(生产消费分离)。
  • 可以用 rangeselect 配合。
  • 可以用 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 特有的惯用模式和需要警惕的反模式。