Go 设计模式学习笔记(23 种 GoF 模式 × Go 惯用实现)

本笔记基于 Refactoring Guru(中文站) 的经典 23 种 GoF 设计模式目录,用 Go 视角重新梳理。每个模式按「模式介绍 → 设计逻辑 → 应用场景 → 生产示例 → Go 简化 / 变体」展开,重点不是照搬经典类图,而是理解这些模式在 Go 中如何被简化、变体或替代,并学习 Go 社区基于语言特性(接口、组合、channel、函数一等公民)的惯用写法。所有代码均为可编译、可运行的独立示例,只用 Go 标准库,中文注释。

一、为什么 Go 需要重新审视设计模式

经典 GoF 模式诞生于 Java / C++ 时代,依赖继承、抽象类、protected、方法重载这些 OOP 机制。Go 刻意去掉了这些,换来的是更简单的语言:

经典 OOP 依赖 Go 的替代
继承(extends) 结构体嵌入 + 接口组合
抽象类 / 虚方法 接口(隐式实现)+ 函数字段
方法重载 函数式选项 / 变长参数 / 泛型
异常(exception) 返回值 error + 错误包装
观察者、事件 channel + goroutine
迭代器(Iterator) 原生 range + Go 1.23 range over func

因此同样一个模式,在 Go 里可能是原生支持(几乎不用写)、简化(一两行函数搞定)、变体(换一种更 Go 的形态),甚至反模式 / 低价值(社区明确不推荐)。本笔记的核心任务,就是给每个模式贴上这个「Go 形态」标签。

二、23 种 GoF 模式总览(Go 形态分类)

Go 形态标签

  • 🟢 原生 — Go 语言特性直接提供,几乎无需实现
  • 🟡 简化 — 用更少的代码 / 更简单的结构表达
  • 🟠 变体 — 换成 Go 特有的形态(函数值、channel、钩子字段等)
  • 🔴 反模式 / 低价值 — 社区明确不建议在 Go 中直接使用

创建型模式(1–5)

# 模式 Go 形态 一句话要点
1 单例 Singleton 🔴 反模式 包级变量即可,sync.Once 只用于懒加载;社区更推崇依赖注入
2 工厂方法 Factory Method 🟡 简化 退化为「返回接口的 NewXxx 函数 + switch 分派」的简单工厂
3 抽象工厂 Abstract Factory 🔴 低价值 两层接口(工厂 + 产品)表达产品族;Go 中最水土不服的模式
4 建造者 Builder 🟠 变体 链式方法返回自身(Fluent Builder),与函数式选项并列为两大构造惯用法
5 原型 Prototype 🔴 低价值 Go 无 clone,手写深拷贝易漏字段;reflect 慢,最不地道

结构型模式(6–12)

# 模式 Go 形态 一句话要点
6 适配器 Adapter 🟢 原生 接口隐式实现 + 结构体包装即完成适配,http.HandlerFunc 是典型
7 桥接 Bridge 🟢 原生 接口本身就已解耦「抽象」与「实现」两个维度,无需刻意使用
8 组合 Composite 🟢 原生 接口 + 递归切片天然表达树形结构,io/fs 的 WalkDir 就是范例
9 装饰器 Decorator 🟡 简化 最著名形态是 http.Handler 中间件链,函数包函数层层叠加
10 外观 Facade 🟢 原生 Go 的「包」本身就是天然门面,只暴露少数导出 API
11 享元 Flyweight 🟠 变体 对应共享不可变值 + sync.Pool 对象复用,比经典实现简单得多
12 代理 Proxy 🟡 简化 实现同一接口包装真实对象;惰性初始化用 sync.OnceValue 一行搞定

行为型模式(13–23)

# 模式 Go 形态 一句话要点
13 责任链 Chain of Responsibility 🟡 简化 最典型就是 HTTP 中间件,func(next Handler) Handler 嵌套
14 命令 Command 🟡 简化 就是一个函数值 type Command func(),闭包绑定接收者
15 迭代器 Iterator 🟢 原生 原生 range + Go 1.23 range over func 消化殆尽
16 中介者 Mediator 🟠 变体 常用 channel 总线或中心化结构体替代网状引用
17 备忘录 Memento 🟡 简化 结构体值拷贝 + 历史栈即可,注意引用字段要深拷贝
18 观察者 Observer 🟠 变体 回调函数切片或 channel 广播;context 取消传播就是标准库实例
19 状态 State 🟠 变体 状态少用转移表 map[state]map[event]next,行为复杂才用接口
20 策略 Strategy 🟢 原生 就是函数值 type Strategy func(...),运行时替换字段即切换
21 模板方法 Template Method 🟠 变体 Go 无继承,用结构体嵌入 + 钩子函数字段或接口组合实现
22 访问者 Visitor 🔴 低价值 type switch + 空接口替代,双重分派在 Go 中价值低
23 解释器 Interpreter 🔴 低价值 手写递归下降(lexer + parser)更实用,别把语法建模成对象

宏观结论

  • 高价值 / 高频:策略、命令、观察者、责任链(函数一等公民 + channel 让它们极轻量)。
  • 被语言特性原生消化:迭代器、适配器、桥接、组合、外观。
  • 有清晰 Go 变体:建造者(函数式选项)、享元(sync.Pool)、状态(转移表)、中介者(channel 总线)、模板方法(钩子字段)。
  • 明确不建议:单例(反模式)、抽象工厂、原型、访问者、解释器。

三、学习建议

  1. 先过一遍「Go 简化 / 变体」小节,再回头读「模式介绍 / 设计逻辑」,从 Go 视角理解每个模式真正解决什么问题。
  2. 23 个模式的代码都独立可编译运行,建议逐个 go run 一遍,观察输出。
  3. 重点投入第四部分(#24–31)的 Go 社区惯用模式——那是 Go 程序员真实天天写的东西,比照搬 GoF 模式更有生产价值。
  4. 记住一句话:优先用语言原生能力与更简单的结构,避免为模式而模式

第一部分:创建型模式(Creational Patterns)

创建型模式关注「对象如何被创建」,核心目标是让「创建逻辑」与「使用逻辑」解耦。Go 没有继承、没有 protected,因此经典 OOP 创建型模式在 Go 中普遍以「接口 + 结构体 + 组合」重新落地,许多模式甚至被简化成普通函数或包级变量。下面按顺序介绍 5 个经典创建型模式的 Go 惯用写法。

1. 单例(Singleton)

Go 简化要点:单例在 Go 中通常退化为「包级变量 + sync.Once 懒加载」,甚至直接用一个包级 var 初始化,连专门的类都不需要。

模式介绍

单例模式保证某个类型在整个进程中只有一个实例,并提供统一的全局访问点。它适合管理全局共享、有状态的资源(配置、连接池、日志器),避免重复实例化带来的开销和状态分裂。经典 OOP 依赖私有构造函数与静态方法实现,Go 则用「不导出的类型 + 包级访问函数」天然表达「全局唯一」。

设计逻辑

核心角色只有两个:私有的单例实例与公开的获取函数。Go 中首字母小写的类型、字段对外部包不可见,相当于 private;对外只暴露一个 GetXxx() 函数返回共享指针。懒加载要线程安全,常用 sync.Once 保证初始化只执行一次;若实例不依赖运行时参数,直接用 var instance = &Config{...} 包级初始化即可,既简单又天然并发安全。

应用场景
  • 全局配置:程序启动读取一次配置,所有模块共享同一份。
  • 连接池 / 资源管理器:数据库连接、HTTP 连接复用。
  • 日志器(Logger):统一日志级别与输出目标。
  • 全局缓存:避免多处缓存各自为政、数据不一致。
  • 事件总线 / 指标注册表:进程内唯一的通信或注册通道。
生产示例
package main

import (
	"fmt"
	"sync"
)

// Config 全局唯一配置对象;字段不导出,外部只能通过 GetConfig 访问
type Config struct {
	serverName string
	port       int
}

var (
	cfgInstance *Config
	cfgOnce     sync.Once
)

// GetConfig 返回全局唯一配置实例。
// 用 sync.Once 保证并发调用时初始化逻辑也只会执行一次。
func GetConfig() *Config {
	cfgOnce.Do(func() {
		cfgInstance = &Config{
			serverName: "demo-server",
			port:       8080,
		}
	})
	return cfgInstance
}

// String 实现 fmt.Stringer,便于直接打印
func (c *Config) String() string {
	return fmt.Sprintf("%s:%d", c.serverName, c.port)
}

func main() {
	// 无论调用多少次,拿到的都是同一个实例
	a := GetConfig()
	b := GetConfig()
	fmt.Printf("a 与 b 是同一个实例:%t\n", a == b)
	fmt.Printf("当前配置:%s\n", a)
}

运行输出:

a 与 b 是同一个实例:true
当前配置:demo-server:8080
Go 简化 / 变体
  • 经典 OOP 的替代:Java/C++ 靠「私有构造函数 + 静态 getInstance()」保证唯一。Go 没有构造函数、没有 protected,因此「不导出的结构体 + 包级函数」就是天然的单例:外部包无法 new(类型不可见),只能通过导出函数取得实例。
  • 社区真实看法:Go 社区普遍把「单例」视为反模式——它引入隐藏的全局状态,让依赖难以被替换,测试难以隔离。官方更推崇「依赖注入 / 显式传参」:在 main 里创建好依赖对象,显式传给需要的模块。若确实要全局唯一,一个包级 var 就够了;sync.Once 只在需要「懒加载 + 并发安全」时才登场。
  • 标准库对应物database/sqlsql.Open 返回的 *sql.DB 官方明确要求全局共享、不要反复开关,是「应被当作单例使用」的典型;http.DefaultClienthttp.DefaultServeMux 则是直接以包级变量存在的单例。

2. 工厂方法(Factory Method)

Go 简化要点:Go 没有继承,工厂方法通常直接收敛为一个返回接口的普通函数,靠 switch/map 选择具体实现,即所谓「简单工厂」。

模式介绍

工厂方法定义一个「创建对象」的接口,让具体类型决定实例化哪个实现,从而把对象的创建与使用解耦。它解决的是「调用方不该关心具体类型」的问题:新增实现时,调用方代码零改动。Go 中没有「子类覆写」的概念,因此工厂方法最常见的形态是一个返回接口的 NewXxx(kind) 函数。

设计逻辑

三个角色:抽象产品(接口)、具体产品(实现接口的结构体)、工厂(返回接口的函数)。调用方只依赖抽象产品接口,不关心具体类型;新增产品时只需新增一个实现并扩展工厂的分支逻辑。关键接口形如 type Product interface { Use() },工厂函数形如 func NewProduct(kind string) Product,内部用 switch 或 map 完成类型分派。

应用场景
  • 按渠道创建消息发送器(邮件、短信、推送)。
  • 按配置选择存储后端(MySQL、PostgreSQL、内存)。
  • 解析不同格式的输入(JSON、XML、CSV)。
  • 测试中按环境返回 Mock 或真实实现。
  • 生成不同地区 / 语言的本地化组件。
生产示例
package main

import "fmt"

// Sender 抽象产品:所有通知发送器都必须实现 Send
type Sender interface {
	Send(to, msg string) error
}

// EmailSender 具体产品:邮件发送器
type EmailSender struct{}

func (EmailSender) Send(to, msg string) error {
	fmt.Printf("[邮件] 收件人=%s 内容=%s\n", to, msg)
	return nil
}

// SmsSender 具体产品:短信发送器
type SmsSender struct{}

func (SmsSender) Send(to, msg string) error {
	fmt.Printf("[短信] 收件人=%s 内容=%s\n", to, msg)
	return nil
}

// NewSender 工厂方法:按渠道返回对应的发送器。
// 调用方只依赖 Sender 接口,不关心具体类型是邮件还是短信。
func NewSender(channel string) Sender {
	switch channel {
	case "email":
		return EmailSender{}
	case "sms":
		return SmsSender{}
	default:
		return EmailSender{}
	}
}

func main() {
	email := NewSender("email")
	sms := NewSender("sms")

	email.Send("alice@example.com", "你好,欢迎注册")
	sms.Send("13800000000", "验证码:1234")
}

运行输出:

[邮件] 收件人=alice@example.com 内容=你好,欢迎注册
[短信] 收件人=13800000000 内容=验证码:1234
Go 简化 / 变体
  • 经典 OOP 的替代:经典工厂方法依赖「抽象类 + 子类覆写 factoryMethod()」。Go 用「接口 + 结构体」替代继承,工厂方法也不再需要子类层级,而是收敛成一个普通函数,用 switch 或 map 做分派——这在术语上更接近「简单工厂」,但对 Go 来说已经够用。
  • 社区真实看法:Go 社区几乎不会搭建「工厂方法层级」,因为「构造一个对象」用 NewXxx() 函数就够了。更受推崇的是「函数式选项(Functional Options)」:NewXxx(opts ...Option) 接受可变参数函数,调用方按需开启功能,grpc.NewServerhttp.Server 的配置方式都是这种风格。函数式选项与工厂方法可以组合使用。
  • 标准库对应物errors.New 返回 error 接口、strings.NewReaderhttp.NewRequest 等大量「NewXxx 返回接口或具体类型」的构造函数都是工厂思想;database/sql 更是经典:驱动通过 Register 注册,sql.Open("mysql", dsn) 按驱动名返回 *sql.DB

3. 抽象工厂(Abstract Factory)

Go 简化要点:抽象工厂在 Go 中很少单独出现;只有当「产品族必须成套一致」时才用「工厂接口 + 产品接口」表达,否则简单工厂或构造器函数已足够。

模式介绍

抽象工厂提供一个创建「一族相关对象」的接口,而不用指定具体类,保证客户端拿到的一组对象彼此配套。它解决产品族一致性问题:例如浅色主题下的按钮、复选框、弹窗必须属于同一风格。Go 中产品族若只有一个实现,接口就显得多余,因此该模式在 Go 生态中被刻意冷落。

设计逻辑

四个角色:抽象产品(每组产品的接口)、具体产品(接口的实现)、抽象工厂(返回抽象产品的接口)、具体工厂(实现抽象工厂并产出配套产品)。客户端只持有抽象工厂与抽象产品接口,切换一个工厂即可整体切换产品族;新增产品族只需新增一个具体工厂和一组具体产品,客户端代码不改。

应用场景
  • 跨平台 GUI:同一套代码在不同操作系统渲染出风格一致的组件。
  • 主题换肤:浅色 / 深色主题的按钮、输入框、弹窗成套切换。
  • 数据库驱动族:同一数据库提供配套的连接、事务、方言对象。
  • 云厂商封装:AWS / GCP 各自提供一致的存储、计算、网络能力。
  • 游戏阵营体系:不同阵营拥有各自成套的兵种与建筑。
生产示例
package main

import "fmt"

// Button 抽象产品:按钮
type Button interface {
	Render() string
}

// Checkbox 抽象产品:复选框
type Checkbox interface {
	Render() string
}

// ThemeFactory 抽象工厂:创建一组配套的 UI 组件
type ThemeFactory interface {
	CreateButton() Button
	CreateCheckbox() Checkbox
}

// LightButton 浅色主题按钮
type LightButton struct{}

func (LightButton) Render() string { return "浅色按钮" }

// LightCheckbox 浅色主题复选框
type LightCheckbox struct{}

func (LightCheckbox) Render() string { return "浅色复选框" }

// LightThemeFactory 浅色主题产品族
type LightThemeFactory struct{}

func (LightThemeFactory) CreateButton() Button     { return LightButton{} }
func (LightThemeFactory) CreateCheckbox() Checkbox { return LightCheckbox{} }

// DarkButton 深色主题按钮
type DarkButton struct{}

func (DarkButton) Render() string { return "深色按钮" }

// DarkCheckbox 深色主题复选框
type DarkCheckbox struct{}

func (DarkCheckbox) Render() string { return "深色复选框" }

// DarkThemeFactory 深色主题产品族
type DarkThemeFactory struct{}

func (DarkThemeFactory) CreateButton() Button     { return DarkButton{} }
func (DarkThemeFactory) CreateCheckbox() Checkbox { return DarkCheckbox{} }

// renderUI 客户端:只依赖抽象工厂与抽象产品接口
func renderUI(factory ThemeFactory) {
	button := factory.CreateButton()
	checkbox := factory.CreateCheckbox()
	fmt.Printf("%s + %s\n", button.Render(), checkbox.Render())
}

func main() {
	fmt.Println("=== 浅色主题 ===")
	renderUI(LightThemeFactory{})
	fmt.Println("=== 深色主题 ===")
	renderUI(DarkThemeFactory{})
}

运行输出:

=== 浅色主题 ===
浅色按钮 + 浅色复选框
=== 深色主题 ===
深色按钮 + 深色复选框
Go 简化 / 变体
  • 经典 OOP 的替代:经典抽象工厂依赖「抽象类 + 子类」层级。Go 用两层接口(工厂接口 + 产品接口)加具体结构体表达;因为没继承,每个具体工厂只需实现两个方法,结构非常扁平。
  • 社区真实看法:抽象工厂可算 Go 中最「水土不服」的 GoF 模式。Go 强调「少层级、多函数」,当一个工厂只有一个实现(最常见的场景)时接口纯属多余。社区倾向用「简单工厂函数」或「直接返回具体类型」,只有确认存在多个产品族且必须强一致时,才值得引入工厂接口。
  • 标准库对应物image 包的 image.Decode 通过注册的解码器返回统一的 image.Imagedatabase/sql/driver 定义了一组驱动接口(DriverConn 等),各数据库驱动实现它们,构成真实的「驱动产品族」。标准库并无内建抽象工厂,多数库在内部用「注册表 + 工厂函数」实现等价效果。

4. 建造者(Builder)

Go 简化要点:Go 中最常见的建造者是「链式调用构造器(Fluent Builder)」,用指针接收者返回自身,与「函数式选项」并列为构造复杂对象的两大惯用法。

模式介绍

建造者将复杂对象的「构建过程」与「最终表示」分离,让同一套构建步骤能产出不同配置的对象。它尤其适合参数众多、有大量可选字段的对象。Go 中当构造函数签名太长、必填与可选参数混杂时,建造者能显著提升调用方的可读性。

设计逻辑

两个角色:建造者(提供分步设置方法)与客户端(按序调用并最终 Build())。关键技巧是每个设置方法用指针接收者并返回 *Builder 自身,从而支持链式调用;Build() 在结束时校验字段并产出最终对象。字段先暂存在建造者内部,Build() 返回值的副本,保证构建完成后建造者还能复用。

应用场景
  • 动态拼接 SQL(select / where / order 逐步构建)。
  • 配置复杂网络请求(URL、请求头、超时、重试策略)。
  • 生成复杂文档 / 报表(多段落、多格式)。
  • 构造含大量可选字段的配置结构体(服务器、客户端选项)。
  • 组装游戏角色或粒子系统参数。
生产示例
package main

import (
	"fmt"
	"strings"
)

// SQLBuilder SQL 查询构建器:把复杂的查询语句拆成一步步链式调用
type SQLBuilder struct {
	table   string
	columns []string
	where   []string
}

// NewSQLBuilder 返回一个空的构建器
func NewSQLBuilder() *SQLBuilder {
	return &SQLBuilder{}
}

// Table 指定目标表
func (b *SQLBuilder) Table(name string) *SQLBuilder {
	b.table = name
	return b
}

// Select 指定要查询的列(可多个)
func (b *SQLBuilder) Select(cols ...string) *SQLBuilder {
	b.columns = cols
	return b
}

// Where 追加一个查询条件,可多次调用
func (b *SQLBuilder) Where(cond string) *SQLBuilder {
	b.where = append(b.where, cond)
	return b
}

// Build 根据已收集的信息拼出最终 SQL 语句
func (b *SQLBuilder) Build() string {
	sql := "SELECT " + strings.Join(b.columns, ", ") + " FROM " + b.table
	if len(b.where) > 0 {
		sql += " WHERE " + strings.Join(b.where, " AND ")
	}
	return sql
}

func main() {
	// 链式调用:每个设置方法都返回自身,调用看起来像自然语言
	query := NewSQLBuilder().
		Table("users").
		Select("id", "name", "email").
		Where("age > 18").
		Where("active = 1").
		Build()

	fmt.Println(query)
}

运行输出:

SELECT id, name, email FROM users WHERE age > 18 AND active = 1
Go 简化 / 变体
  • 经典 OOP 的替代:经典建造者有 Director(指挥者)与抽象 Builder 两层。Go 社区几乎总是丢掉 Director——函数调用链本身已经表达「有序的构建过程」;若真的需要多态替换构建算法,用接口 + 注入也能做,但多数场景是杀鸡用牛刀。
  • 社区真实看法:Go 里构造复杂对象的两大惯用法是「建造者」和「函数式选项」:建造者在「有必填字段、步骤有顺序」时更清晰;函数式选项在「大量可选字段、需要向后兼容」时更优雅。二者甚至常被组合使用(如 grpcDialContext + WithXxx 系列)。
  • 标准库对应物:标准库中链式建造者不多,url.URLhttp.Request 主要靠字段赋值。真正广泛使用的是第三方库:GORM 的 db.Table(...).Where(...).Find(...)go-redisNewClient 配置、sqlx 的查询构造,都是建造者思想。

5. 原型(Prototype)

Go 简化要点:Go 没有内建 clone,原型退化为手写 Clone() 方法;深拷贝边界(切片、map、指针字段)是最大难点,因此它常被视为最不地道的创建型模式。

模式介绍

原型模式通过「复制现有对象」而非「重新构造」来创建新对象,适合创建成本高、或新对象与原型结构高度相似的场景。Go 中它常用于「从默认配置派生新配置」「复制一个对象后再局部修改」。由于 Go 语言本身不提供克隆机制,核心工作就是正确实现深拷贝。

设计逻辑

核心角色是原型接口(声明 Clone() 方法)与具体原型(实现复制逻辑)。Go 的结构体赋值是浅拷贝,引用字段(切片、map、指针)会共享底层数据,因此 Clone() 通常这样写:先浅拷贝结构体,再手动 copy / 重建所有可变字段。若需要「多态克隆」(通过接口持有未知具体类型),Clone() 的返回类型应为接口。

应用场景
  • 从基础配置模板派生多个实例再各自微调。
  • 复制请求对象,并发发起多个变体请求。
  • 游戏对象复制:把怪物 / 子弹作为模板快速生成。
  • 快照 / 撤销:复制对象当前状态保存。
  • 深拷贝含嵌套结构的领域模型。
生产示例
package main

import "fmt"

// Prototype 原型接口:所有可克隆对象都要实现 Clone
type Prototype interface {
	Clone() Prototype
}

// ServerConfig 服务器配置原型:带一个切片字段,用于演示深拷贝
type ServerConfig struct {
	Name     string
	Port     int
	Features []string
}

// Clone 深拷贝实现。
// 注意:直接赋值结构体只会做浅拷贝,Features 仍共享同一个底层数组,
// 因此这里新建切片并 copy,保证修改克隆体不影响原对象。
func (c *ServerConfig) Clone() Prototype {
	features := make([]string, len(c.Features))
	copy(features, c.Features)
	return &ServerConfig{
		Name:     c.Name,
		Port:     c.Port,
		Features: features,
	}
}

func main() {
	// 原型:一份默认配置
	base := &ServerConfig{
		Name:     "default",
		Port:     8080,
		Features: []string{"tls", "metrics"},
	}

	// 从原型复制并修改,得到全新的实例
	dev := base.Clone().(*ServerConfig)
	dev.Name = "dev"
	dev.Features[0] = "debug" // 修改克隆体的切片元素

	fmt.Printf("原型: %+v\n", base)
	fmt.Printf("克隆: %+v\n", dev)
	fmt.Printf("切片底层数组已隔离:%t\n", dev.Features[0] != base.Features[0])
}

运行输出:

原型: &{Name:default Port:8080 Features:[tls metrics]}
克隆: &{Name:dev Port:8080 Features:[debug metrics]}
切片底层数组已隔离:true
Go 简化 / 变体
  • 经典 OOP 的替代:Java 提供内建 Cloneableclone(),C++ 有拷贝构造函数;Go 两者都没有,只能手写 Clone()。因此原型是 Go 中「最不地道」的创建型模式,社区也没有统一约定。
  • 社区真实看法:Go 社区普遍觉得原型「性价比不高」:深拷贝要么手写逐字段复制(易漏字段、难维护),要么用 reflect(慢且可读性差)。对大多数业务对象,「直接 new + 赋值」比维护 Clone() 更简单;只有确实需要「多态克隆」时才值得实现。
  • 标准库对应物:标准库没有原型的直接实现,但 encoding/jsonencoding/gob 常被用来「序列化 → 反序列化」实现通用深拷贝(代价是慢且依赖可导出字段);copy()append 的「拷贝并修改」语义是社区替代原型的惯用手段。google/go-cmp 等第三方库提供深拷贝辅助函数,也是原型思路的变体。

第二部分:结构型模式(Structural Patterns)

结构型模式(Structural Patterns)关注如何把类与对象组合成更大的结构,同时保持结构灵活、易于扩展。经典 GoF 结构型模式依赖继承与多态实现组合;而 Go 没有继承,只有「接口 + 结构体 + 组合嵌入」,因此这些模式在 Go 中的实现往往更简洁,甚至被语言特性直接替代。

6. 适配器(Adapter)

Go 简化要点:接口隐式实现让「适配器」变成一个结构体包装 + 直接实现目标接口即可,无需像 OOP 那样继承接口再多态。

模式介绍

适配器(Adapter)用于解决「接口不兼容」的问题:当客户端只认识接口 A,而现有组件提供的是接口 B 的能力时,通过一个适配器对象把 B 包装成 A,使原本无法协作的代码一起工作。它不改变任何一方,只是翻译两者之间的调用。

设计逻辑

核心有三个角色:目标接口(客户端依赖的接口,如 Payment)、被适配者(已有的、签名不匹配的实现,如第三方 SDK 的 AlipaySDK)、适配器(包装被适配者并实现目标接口)。客户端只面向目标接口编程,适配器内部把目标接口的调用翻译成被适配者的调用。

关键代码形态:

type Target interface {
	Request()
}
type Adaptee struct{} // 已有组件
func (Adaptee) SpecificRequest() {}
type Adapter struct{ adaptee Adaptee }
func (a Adapter) Request() { a.adaptee.SpecificRequest() } // 翻译调用
应用场景
  • 接入第三方 SDK(支付、短信、地图),其方法签名与应用内统一接口不一致时。
  • 把遗留系统/老版本 API 包装成新接口,平滑迁移。
  • 数据库驱动、云存储等多实现切换,通过统一接口屏蔽差异。
  • 把自定义函数类型适配成标准库接口(如 io.Writerhttp.Handler)。
  • 新代码统一使用标准库的 io.Reader/io.Writer,老代码用别的方法读写。
生产示例
package main

import "fmt"

// Payment 是应用内统一的支付接口,客户端只依赖它。
type Payment interface {
	Pay(amount float64) error
}

// WeChatPay 是微信支付 SDK 自带的实现,方法签名与接口天然吻合。
type WeChatPay struct{ balance float64 }

func (w *WeChatPay) Pay(amount float64) error {
	if w.balance < amount {
		return fmt.Errorf("余额不足,当前 %.2f 元", w.balance)
	}
	w.balance -= amount
	fmt.Printf("微信支付成功:%.2f 元,剩余 %.2f 元\n", amount, w.balance)
	return nil
}

// AlipaySDK 是支付宝 SDK:方法签名与 Payment 完全不同,无法直接使用。
type AlipaySDK struct{ balance float64 }

// Transfer 是支付宝自己的「转账」语义,接口不匹配。
func (a *AlipaySDK) Transfer(to string, amount float64) error {
	if a.balance < amount {
		return fmt.Errorf("余额不足,当前 %.2f 元", a.balance)
	}
	a.balance -= amount
	fmt.Printf("支付宝转账给 %s:%.2f 元,剩余 %.2f 元\n", to, amount, a.balance)
	return nil
}

// AlipayAdapter 是适配器:包装 AlipaySDK,把 Transfer 翻译成 Pay。
type AlipayAdapter struct {
	sdk   *AlipaySDK
	owner string
}

func (a *AlipayAdapter) Pay(amount float64) error {
	return a.sdk.Transfer(a.owner, amount)
}

func main() {
	var payment Payment = &WeChatPay{balance: 100}
	if err := payment.Pay(30); err != nil {
		fmt.Println("支付失败:", err)
	}

	// 支付宝 SDK 通过适配器也能当作 Payment 使用。
	payment = &AlipayAdapter{sdk: &AlipaySDK{balance: 200}, owner: "自己"}
	if err := payment.Pay(50); err != nil {
		fmt.Println("支付失败:", err)
	}
}
Go 简化 / 变体
  • 经典 OOP 中适配器需要继承目标抽象类并持有被适配者;Go 没有继承,适配器就是一个普通的 struct + 目标接口的隐式实现,编译期自动校验类型是否满足接口,代码量更少。
  • Go 社区认为适配器最常见的形态是「把函数类型适配成标准接口」,例如 http.HandlerFuncfunc(http.ResponseWriter, *http.Request) 适配成 http.Handlerio.Reader/io.Writer 这类单方法接口让任意类型都能低成本接入标准库的 io.Copybufio 等工具。
  • 标准库/知名项目对应物:http.HandlerFuncbytes.Buffer(适配 io.Writer)、encoding/jsonMarshaler/Unmarshalerdatabase/sql/driver 的驱动适配层。

7. 桥接(Bridge)

Go 简化要点:桥接在 Go 中几乎被「接口」这一语言特性原生实现——抽象维度是接口,实现维度是接口的具体实现,二者通过组合自由搭配。

模式介绍

桥接(Bridge)用于把「抽象」与「实现」两个维度分开,使它们可以独立变化。典型场景是有两个互相独立的变化轴:例如「消息类型(文本/HTML)」和「发送渠道(邮件/短信)」,任意组合都能工作,且任一维度的新类型都不需要修改另一方。

设计逻辑

经典实现有两个维度:抽象部分(Abstraction,如 Message)与实现部分(Implementation,如 Sender)。抽象部分持有一个实现部分的引用(组合),将高层调用转发给实现部分。两个维度各自有若干具体类型,通过组合(而非继承)自由匹配。Go 中抽象维度用接口表达,具体类型实现接口,并通过结构体组合/方法参数建立联系。

关键代码形态:

type Sender interface {
	Send(msg string)
}            // 实现维度
type Message interface {
	SendTo(s Sender)
}            // 抽象维度
应用场景
  • 跨平台 UI 组件:同一套窗口抽象对接 Windows/macOS/Linux 不同渲染实现。
  • 消息通知系统:消息模板(文本/HTML/邮件模板)× 渠道(邮件/短信/推送)。
  • 设备驱动:指令抽象对接不同厂商的协议实现。
  • 报表引擎:数据源与渲染格式两个维度任意组合。
  • 图形库:形状抽象(圆/方)× 颜色实现(红/蓝)。
生产示例
package main

import "fmt"

// Sender 是实现维度:消息通过什么渠道发送。
type Sender interface {
	Send(msg string)
}

// EmailSender 具体实现之一:邮件渠道。
type EmailSender struct{ address string }

func (e EmailSender) Send(msg string) {
	fmt.Printf("发邮件到 %s:%s\n", e.address, msg)
}

// SMSSender 具体实现之二:短信渠道。
type SMSSender struct{ phone string }

func (s SMSSender) Send(msg string) {
	fmt.Printf("发短信到 %s:%s\n", s.phone, msg)
}

// Message 是抽象维度:消息的类型。把实现(Sender)作为参数传入,
// 二者在调用时自由组合,互不感知。
type Message interface {
	Compose() string
	SendTo(s Sender)
}

// TextMessage 抽象实现:纯文本消息。
type TextMessage struct{ content string }

func (m TextMessage) Compose() string { return "【文本】" + m.content }

func (m TextMessage) SendTo(s Sender) { s.Send(m.Compose()) }

// HTMLMessage 抽象实现:富文本消息。
type HTMLMessage struct{ title, body string }

func (m HTMLMessage) Compose() string {
	return fmt.Sprintf("<h1>%s</h1><p>%s</p>", m.title, m.body)
}

func (m HTMLMessage) SendTo(s Sender) { s.Send(m.Compose()) }

func main() {
	email := EmailSender{address: "boss@example.com"}
	sms := SMSSender{phone: "13800000000"}

	// 文本消息 × 两种渠道。
	msg := Message(TextMessage{content: "今晚八点开会"})
	msg.SendTo(email)
	msg.SendTo(sms)

	// HTML 消息 × 两种渠道:两个维度自由组合。
	msg = HTMLMessage{title: "周报", body: "本周进展顺利"}
	msg.SendTo(email)
}
Go 简化 / 变体
  • 经典 OOP 需要「抽象类」与「实现接口」两套继承体系,再通过引用关联;Go 中接口本身就是最纯粹的抽象,任何类型实现接口即可充当「实现维度」,抽象维度只需面向接口编程,省掉了整棵继承树。
  • Go 社区普遍认为桥接在 Go 中常常是「不需要刻意使用」的模式:接口设计已经天然解耦两个维度,io.Reader/io.Writerhttp.Handler 都是极小的接口,组合自由。Go 的惯用做法是「依赖接口 + 构造时注入实现」,这在依赖注入(DI)场景中无处不在。
  • 标准库对应物:io.Reader/io.Writer 把「数据源」与「消费方」解耦;net/httpRoundTripper 接口使传输层可替换;database/sqldriver 分离「API 抽象」与「驱动实现」。

8. 组合(Composite)

Go 简化要点:接口 + 递归切片天然表达树形结构,叶子与容器实现同一接口,客户端无需区分它们。

模式介绍

组合(Composite)用于处理「整体-部分」的树形层次结构:让叶子对象与容器对象实现同一个接口,客户端就能用一致的方式对待单个对象和对象集合。递归是它的灵魂——容器内部持有子组件,子组件又可以是叶子或容器。

设计逻辑

核心角色有:组件接口(Component,定义叶子与容器共有的操作,如 Name()Size())、叶子(Leaf,没有子节点的最小单元)、容器(Composite,持有子组件列表并聚合/委托操作)。客户端只需面向 Component 编程;容器遍历并递归调用子组件,把「组合」透明化。

关键代码形态:

type Component interface{ Size() int64 }
type File struct{ size int64 }
func (f File) Size() int64 { return f.size }
type Directory struct{ children []Component }
func (d Directory) Size() (total int64) {
	for _, c := range d.children {
		total += c.Size()
	}
	return
}
应用场景
  • 文件系统:目录包含文件与子目录,统一计算大小、遍历。
  • 组织架构:公司包含部门,部门包含员工,统一汇报/统计。
  • UI 组件树:面板包含按钮、文本框、子面板,统一渲染与事件。
  • 菜单系统:菜单项包含子菜单。
  • 配置/JSON 解析树:节点统一遍历。
生产示例
package main

import "fmt"

// Component 是组合模式的核心:叶子与容器统一接口。
type Component interface {
	Name() string
	Size() int64
}

// File 是叶子节点:没有子组件。
type File struct {
	name string
	size int64
}

func (f *File) Name() string { return f.name }
func (f *File) Size() int64  { return f.size }

// Directory 是容器节点:可以包含任意子组件(叶子或容器)。
type Directory struct {
	name     string
	children []Component
}

func (d *Directory) Name() string { return d.name }

// Size 递归累加所有子节点大小,客户端无需区分叶子与容器。
func (d *Directory) Size() (total int64) {
	for _, c := range d.children {
		total += c.Size()
	}
	return
}

func (d *Directory) Add(c Component) {
	d.children = append(d.children, c)
}

// PrintTree 用统一的 Component 视角递归打印整棵树。
func PrintTree(c Component, indent string) {
	fmt.Printf("%s%s(%.1f KB)\n", indent, c.Name(), float64(c.Size())/1024)
	if dir, ok := c.(*Directory); ok {
		for _, child := range dir.children {
			PrintTree(child, indent+"  ")
		}
	}
}

func main() {
	src := &Directory{name: "src"}
	src.Add(&File{name: "main.go", size: 2048})
	src.Add(&File{name: "util.go", size: 1024})

	docs := &Directory{name: "docs"}
	docs.Add(&File{name: "README.md", size: 512})

	root := &Directory{name: "project"}
	root.Add(src)
	root.Add(docs)
	root.Add(&File{name: "go.mod", size: 128})

	PrintTree(root, "")
	fmt.Printf("项目总大小:%.1f KB\n", float64(root.Size())/1024)
}
Go 简化 / 变体
  • 经典 OOP 用继承让叶子与容器共享 Component 抽象类;Go 用接口 + 结构体即可,Directory 通过 children []Component 递归引用自身,天然表达「容器还能包含容器」。
  • Go 社区常把组合与「组合优于继承」的原则混用:用嵌入(embedding)代替继承来复用字段与方法,让代码更扁平。
  • 标准库对应物:io/fsfs.FS + fs.WalkDir 正是组合模式(目录遍历不关心是文件还是目录);net/http 的 handler 树、encoding/xml/encoding/json 的节点遍历也依赖该思想。

9. 装饰器(Decorator)

Go 简化要点:Go 社区最著名的装饰器就是 http.Handler 中间件链——用函数包装函数,层层叠加行为。

模式介绍

装饰器(Decorator)用于在不修改原有对象代码的前提下,动态地为对象添加职责。它通过一层层「包装」来叠加行为(如日志、缓存、鉴权、压缩),而且包装顺序可以任意排列。相比继承,装饰器更灵活:组合行为而不是固定行为。

设计逻辑

核心角色:组件接口(Component,如 http.Handler)、具体组件(Concrete Component,核心业务实现)、装饰器(Decorator,持有组件接口并转发调用,同时在前/后附加行为)。装饰器与具体组件实现同一接口,因此装饰器可以包裹另一个装饰器,形成链。Go 中装饰器常写作「接收一个 handler、返回一个新 handler」的包装函数。

关键代码形态:

type Handler func(...) // 组件接口
func withLog(next Handler) Handler {
	return func(...) {
		log(...)
		next(...)
	}
}
应用场景
  • HTTP 中间件:日志、鉴权、限流、CORS、压缩、超时,按任意顺序叠加。
  • 缓存包装:为昂贵的数据读取/计算添加一层缓存。
  • 观测埋点:为外部调用统计耗时、错误率。
  • 流式处理:为底层读写加缓冲、加校验和、加加密(bufiogzip)。
  • 通知增强:在基础通知上叠加优先级、重试、多渠道。
生产示例
package main

import (
	"fmt"
	"log"
	"net/http"
	"net/http/httptest"
	"time"
)

// 核心 handler:真实业务逻辑,本身不关心被哪些装饰器包裹。
func helloHandler(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintln(w, "Hello, 装饰器!")
}

// withLogging 是装饰器:记录每次请求的方法、路径与耗时。
func withLogging(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		start := time.Now()
		next.ServeHTTP(w, r)
		log.Printf("请求 %s %s 耗时 %s", r.Method, r.URL.Path, time.Since(start))
	})
}

// withHeader 是装饰器:给所有响应统一附加一个自定义头。
func withHeader(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("X-Go-Pattern", "decorator")
		next.ServeHTTP(w, r)
	})
}

func main() {
	// 层层包装形成中间件链:withHeader -> withLogging -> helloHandler。
	var h http.Handler = http.HandlerFunc(helloHandler)
	h = withLogging(h)
	h = withHeader(h)

	// 用 httptest 模拟一次真实请求,无需启动服务器。
	rec := httptest.NewRecorder()
	req := httptest.NewRequest(http.MethodGet, "/greet", nil)
	h.ServeHTTP(rec, req)

	fmt.Println("响应体:", rec.Body.String())
	fmt.Println("响应头 X-Go-Pattern:", rec.Header().Get("X-Go-Pattern"))
}
Go 简化 / 变体
  • 经典 OOP 装饰器需要实现与被装饰对象相同的抽象接口并持有它;Go 中装饰器就是「接收接口、返回接口」的函数,http.HandlerFunc 把函数转成 http.Handler 使包装代码极其简洁,中间件链成为 Go 网络编程的标配。
  • Go 社区观点:装饰器模式在 Go 中的典型代表是 net/http 中间件生态(chi、echo、gin 的中间件均为此思想);io 包同样充满装饰器——bufio.Reader 包裹 io.Reader 加缓冲、gzip.Reader 包裹读取流做解压、io.MultiWriter 把写操作广播到多个 writer。
  • 标准库对应物:http.StripPrefixhttp.TimeoutHandlerhttp.MaxBytesHandler 都是内置装饰器;sync.Once 也可以看作给「函数只执行一次」的装饰。

10. 外观(Facade)

Go 简化要点:Go 的「包」本身就是天然的门面——只暴露少数顶层函数/类型,内部封装复杂子系统。

模式介绍

外观(Facade)为复杂子系统提供一个简单、统一的入口。客户端不必了解子系统内部众多组件及其协作顺序,只调用门面上的少量方法即可完成一个复杂操作。它没有禁止客户端直接使用子系统,只是提供了更友好的默认路径。

设计逻辑

核心角色:外观(Facade,对外暴露高层 API,内部编排各子系统组件)、复杂子系统(多个类/函数,各自承担单一职责)、客户端(只与外观交互)。外观把「调用顺序」「参数组装」「错误处理」等细节收敛在内部,让客户端代码保持简洁。

关键代码形态:

type VideoConverter struct{}

func (VideoConverter) Convert(file, format string) string {
	// 内部依次调用解封装、解码、编码、混音……
}
应用场景
  • 视频/音频转换、图像处理库:对外一个 Convert,内部串联解码、滤镜、编码。
  • 编译/构建流程:对外 Build,内部串联词法分析、语法分析、优化、代码生成。
  • SDK 顶层 API:对外暴露少量易用方法,隐藏鉴权、重试、日志等细节。
  • 支付网关:统一 Pay,内部选择渠道、签名、回调处理。
  • 复杂初始化流程:对外一个 NewClient,内部完成配置读取、连接池、重连逻辑。
生产示例
package main

import "fmt"

// ---- 复杂子系统:客户端本不必接触的多个组件 ----

// Demuxer 把容器拆成独立的视频流与音频流。
type Demuxer struct{}

func (Demuxer) Extract(file string) (video, audio string) {
	return file + ".video", file + ".audio"
}

// VideoDecoder 解码视频流。
type VideoDecoder struct{}

func (VideoDecoder) Decode(stream string) string {
	return "解码后的视频帧(" + stream + ")"
}

// AudioDecoder 解码音频流。
type AudioDecoder struct{}

func (AudioDecoder) Decode(stream string) string {
	return "PCM音频(" + stream + ")"
}

// AudioMixer 混音。
type AudioMixer struct{}

func (AudioMixer) Mix(audio string) string {
	return "混音后的" + audio
}

// VideoEncoder 按目标格式编码。
type VideoEncoder struct{}

func (VideoEncoder) Encode(frames, format string) string {
	return format + "格式(" + frames + ")"
}

// ---- 外观 Facade ----

// VideoConverter 是外观:客户端只调用 Convert 一个方法,
// 内部把解封装、解码、混音、编码按正确顺序编排起来。
type VideoConverter struct{}

func (VideoConverter) Convert(file, format string) string {
	d := Demuxer{}
	video, audio := d.Extract(file)

	frames := VideoDecoder{}.Decode(video)
	pcm := AudioDecoder{}.Decode(audio)
	mixed := AudioMixer{}.Mix(pcm)

	return VideoEncoder{}.Encode(frames+mixed, format)
}

func main() {
	converter := VideoConverter{}
	result := converter.Convert("猫和老鼠.avi", "mp4")
	fmt.Println("转换结果:", result)
}
Go 简化 / 变体
  • 经典 OOP 中外观是一个「类」,需要手动维护对子系统各对象的引用;Go 中包的导出 API 本身就是天然门面——外部只 import 包名并调用导出的少数函数/类型,内部结构完全不暴露。
  • Go 社区观点:追求「小接口、少导出」的包设计(accept interfaces, return structs)与外观模式高度一致;标准库几乎每个包都是门面,例如 os 把底层系统调用包装成 os.ReadFilenet/httphttp.Get 隐藏了 TCP、TLS、重定向、连接池等复杂细节。
  • 标准库对应物:net/httphttp.Get/http.Client)、database/sql(对外简单接口,内部连接池)、encoding/jsonjson.Marshal/json.Unmarshal 隐藏反射与编码细节)。

11. 享元(Flyweight)

Go 简化要点:Go 中享元最常见形态是「共享不可变值」——字符串驻留、值类型复用,以及 sync.Pool 对象复用。

模式介绍

享元(Flyweight)通过「共享」来减少大量细粒度对象的内存占用。它把对象拆成两部分:内在状态(Intrinsic,可共享、不变的部分,如字体的样式)与外在状态(Extrinsic,每个对象独有、随上下文变化的部分,如字符本身)。相同的内在状态只创建一次,多处引用。

设计逻辑

核心角色:享元(Flyweight,共享的内在状态对象)、享元工厂(Flyweight Factory,维护一个缓存,按内在状态取值,命中则复用,未命中则创建)、客户端(把外在状态与享元组合使用)。工厂通常用 map 以「内在状态 → 享元对象」的方式做缓存,保证相同的内在状态只存在一个实例。

关键代码形态:

type styleFactory struct{ cache map[string]*CharStyle }

func (f *styleFactory) get(key string) *CharStyle {
	if s, ok := f.cache[key]; ok {
		return s
	}
	s := &CharStyle{}
	f.cache[key] = s
	return s
}
应用场景
  • 文本编辑器:大量字符共用有限的字体样式对象,而不是每字符一个样式。
  • 游戏粒子/敌人系统:海量同类实体共享外观模型、纹理等不变资源。
  • 数据库连接池、线程池:复用昂贵资源而非重复创建。
  • 字符串驻留(String Interning):运行时复用重复字符串以省内存。
  • sync.Pool:复用临时对象(如 bytes.Buffer)降低 GC 压力。
生产示例
package main

import "fmt"

// CharStyle 是享元对象:字符共享的「内在状态」,不可变。
type CharStyle struct {
	font string
	size int
	bold bool
}

// CharGlyph 是客户端持有的字形:每个字符独有的「外在状态」+ 共享样式。
type CharGlyph struct {
	ch    rune
	style *CharStyle // 指向享元,多个字符可共享同一个指针
}

// styleFactory 是享元工厂:按样式特征缓存,保证相同样式只有一个实例。
type styleFactory struct {
	cache map[string]*CharStyle
}

func newStyleFactory() *styleFactory {
	return &styleFactory{cache: make(map[string]*CharStyle)}
}

func (f *styleFactory) get(font string, size int, bold bool) *CharStyle {
	key := fmt.Sprintf("%s-%d-%v", font, size, bold)
	if s, ok := f.cache[key]; ok {
		return s // 命中缓存,复用既有对象
	}
	s := &CharStyle{font: font, size: size, bold: bold}
	f.cache[key] = s
	return s
}

func (f *styleFactory) count() int { return len(f.cache) }

func main() {
	factory := newStyleFactory()

	text := []rune("Hello, 享元模式")
	glyphs := make([]CharGlyph, 0, len(text)+1)
	for _, r := range text {
		// 正文字符全部共享同一个样式对象。
		glyphs = append(glyphs, CharGlyph{ch: r, style: factory.get("宋体", 12, false)})
	}
	// 标题单独一个样式,也是全篇唯一。
	glyphs = append(glyphs, CharGlyph{ch: '!', style: factory.get("黑体", 24, true)})

	fmt.Printf("共渲染 %d 个字符,但只创建了 %d 个样式对象(享元)\n", len(glyphs), factory.count())
	for _, g := range glyphs {
		fmt.Printf("  %q -> 字体=%s 字号=%d 加粗=%v\n", g.ch, g.style.font, g.style.size, g.style.bold)
	}
}
Go 简化 / 变体
  • 经典 OOP 需要显式区分内在/外在状态并精心设计工厂;Go 中「共享」通常是零成本的——值类型与不可变数据天然可被任意引用,string 切片共享底层数组,time.Time 等小值直接拷贝。
  • Go 社区真正高频使用的是 sync.Pool:为高频分配(如 bytes.Buffer、序列化 buffer)提供临时对象复用,是「享元思想」在并发场景的落地;encoding/json 内部就用 sync.Pool 缓存 decoder/encoder。
  • 标准库对应物:sync.Poolstrings.Builder(复用底层字节数组)、strconv 内部的小对象池、net/http 的连接复用;此外共享不可变值(如 http.MethodGet 这类常量)也是享元的退化形态。

12. 代理(Proxy)

Go 简化要点:Go 中用「实现同一接口 + 包装真实对象」写代理很自然;sync.OnceValue 让惰性初始化(lazy init)一行搞定。

模式介绍

代理(Proxy)为另一个对象提供一个替身或占位符,以控制对这个对象的访问。代理与真实对象实现同一接口,客户端无感知地经由代理间接访问真实对象,从而在不动真实对象的前提下附加控制逻辑:延迟加载、访问控制、缓存、日志、远程调用等。

设计逻辑

核心角色:主题接口(Subject,真实对象与代理共同实现的接口)、真实主题(Real Subject,真正做事的对象)、代理(Proxy,持有真实主题的引用,在转发调用前后附加控制逻辑)。代理必须在接口层面与真实对象等价,客户端只依赖接口,因此替换透明。

关键代码形态:

type ImageLoader interface{ Load(path string) ([]byte, error) }
type CachingProxy struct {
	real  ImageLoader
	cache map[string][]byte
}

func (p *CachingProxy) Load(path string) ([]byte, error) {
	if data, ok := p.cache[path]; ok {
		return data, nil // 缓存代理
	}
	return p.real.Load(path)
}
应用场景
  • 惰性加载(Lazy Init):大对象/重资源首次用到时才创建,如大图片、数据库连接。
  • 访问控制(Protection Proxy):按权限放行,如鉴权中间件、RPC 调用方校验。
  • 缓存代理:避免重复计算或重复网络请求。
  • 远程代理(Remote Proxy):本地接口对应远程服务,如 RPC、gRPC 客户端 stub。
  • 日志/监控代理:给真实调用附加耗时统计、审计日志(也可看作装饰器)。
  • 反向代理:httputil.ReverseProxy 转发请求到后端服务。
生产示例
package main

import "fmt"

// ImageLoader 是主题接口:客户端只依赖它,不关心是真实加载还是代理。
type ImageLoader interface {
	Load(path string) ([]byte, error)
}

// DiskImageLoader 是真实主题:模拟从磁盘读取图片(代价昂贵)。
type DiskImageLoader struct{}

func (DiskImageLoader) Load(path string) ([]byte, error) {
	// 模拟磁盘 IO 开销。
	return []byte("image-data:" + path), nil
}

// CachingProxy 是代理:先查缓存,未命中才调用真实加载,命中则直接返回。
type CachingProxy struct {
	real  ImageLoader
	cache map[string][]byte
}

func NewCachingProxy(real ImageLoader) *CachingProxy {
	return &CachingProxy{real: real, cache: make(map[string][]byte)}
}

func (p *CachingProxy) Load(path string) ([]byte, error) {
	if data, ok := p.cache[path]; ok {
		fmt.Printf("[代理] 命中缓存:%s\n", path)
		return data, nil
	}
	fmt.Printf("[代理] 缓存未命中,调用真实加载:%s\n", path)
	data, err := p.real.Load(path)
	if err != nil {
		return nil, err
	}
	p.cache[path] = data
	return data, nil
}

func main() {
	loader := NewCachingProxy(DiskImageLoader{})

	// 第一次加载触发真实 IO。
	data, err := loader.Load("cat.png")
	if err != nil {
		fmt.Println("加载失败:", err)
		return
	}
	fmt.Println("第一次内容:", string(data))

	// 第二次加载直接命中缓存,不再访问真实对象。
	data, err = loader.Load("cat.png")
	if err != nil {
		fmt.Println("加载失败:", err)
		return
	}
	fmt.Println("第二次内容:", string(data))
}
Go 简化 / 变体
  • 经典 OOP 中代理需要继承主题抽象类;Go 中代理与真实对象实现同一接口即可,且 http.HandlerFunc、接口包装让「代理」和「装饰器」写起来几乎一样(区别在意图:代理管访问,装饰器加行为)。
  • Go 社区对应物:惰性初始化直接使用 sync.OnceValue/sync.OnceFunc(标准库内置,保证只执行一次);访问控制常用 HTTP 中间件实现;远程代理就是 net/rpcgoogle.golang.org/grpc 生成的客户端 stub。
  • 标准库对应物:sync.OnceValue(惰性求值)、net/http/httputil.ReverseProxy(反向代理)、net/httpTransport(连接池 + 复用,充当底层代理)、os/exec 等包装系统调用的类型。

第三部分:行为型模式(Behavioral Patterns)

行为型模式关注对象之间的职责分配、通信方式与算法的组织。由于 Go 没有继承和重载,经典行为型模式在 Go 中通常能大幅简化:函数是一等公民(策略、命令、模板方法可以直接用函数值),接口是隐式实现(无需显式 implements),而 channel 与 goroutine 则天然支持并发场景下的协作(观察者、中介者、迭代器)。本部分按编号 13 到 23 覆盖 11 种行为型模式,每个模式给出可编译的生产示例,并重点讲解「Go 简化 / 变体」与社区真实看法。

13. 责任链(Chain of Responsibility)

Go 简化要点:Go 社区最典型的责任链就是 HTTP 中间件func(next http.Handler) http.Handler 嵌套本质上就是一种责任链实现;核心是「处理器包装处理器」,用接口 + 结构体嵌入保存 next 引用即可。

模式介绍

责任链将「请求的发送者」与「请求的处理者」解耦,让多个处理对象都有机会处理同一个请求。请求沿着链依次传递,直到某个处理器处理它或到达链尾。它解决的问题是「到底由谁来处理这个请求」不需要写死在调用方代码里。

设计逻辑

经典结构包含三个角色:抽象处理者(定义处理接口与后继引用)、具体处理者(决定是处理还是传递给下一个)、客户端(构建链并发出请求)。Go 中抽象处理者通常是一个接口,配合一个内嵌的「持有下一节点」的基础结构体,具体处理器通过嵌入该基础结构体复用转发逻辑,避免在每个处理器里重复写 if next != nil

应用场景
  • HTTP 中间件链(如 net/http 中多个 http.Handler 的嵌套)
  • 日志系统按级别分级处理(DEBUG → INFO → ERROR 逐级兜底)
  • 敏感操作的多级审批流程(员工 → 组长 → 经理)
  • 事件分发器按处理器能力做多级兜底
生产示例
package main

import "fmt"

// Handler 抽象处理者:处理请求,并可把请求转发给链上的下一个节点
type Handler interface {
	// SetNext 设置责任链中的下一个处理器
	SetNext(Handler) Handler
	// Handle 处理消息,返回 true 表示已处理完毕
	Handle(msg string) bool
}

// BaseHandler 提供 next 字段与默认转发逻辑,供具体处理器嵌入复用
type BaseHandler struct {
	next Handler
}

func (b *BaseHandler) SetNext(h Handler) Handler {
	b.next = h
	return h
}

// passToNext 把消息转发给链上的下一个节点
func (b *BaseHandler) passToNext(msg string) bool {
	if b.next != nil {
		return b.next.Handle(msg)
	}
	return false // 到达链尾,无人处理
}

// DebugHandler 只处理 DEBUG 级别的消息
type DebugHandler struct {
	BaseHandler // 嵌入基础处理器,获得 SetNext 与转发能力
}

func (h *DebugHandler) Handle(msg string) bool {
	if msg == "DEBUG" {
		fmt.Println("[DEBUG] 处理了调试日志")
		return true
	}
	return h.passToNext(msg) // 不归自己管,转给下一个
}

// InfoHandler 只处理 INFO 级别的消息
type InfoHandler struct {
	BaseHandler
}

func (h *InfoHandler) Handle(msg string) bool {
	if msg == "INFO" {
		fmt.Println("[INFO] 处理了信息日志")
		return true
	}
	return h.passToNext(msg)
}

func main() {
	// 构建责任链:DEBUG -> INFO
	debug := &DebugHandler{}
	info := &InfoHandler{}
	debug.SetNext(info)

	for _, msg := range []string{"DEBUG", "INFO", "WARN"} {
		fmt.Printf("收到消息: %s\n", msg)
		debug.Handle(msg)
	}
}
Go 简化 / 变体
  • 经典 OOP 用「抽象类 + 模板方法」定义抽象处理者,Go 里直接用一个接口即可;具体处理器通过嵌入 BaseHandler 复用转发逻辑,无需继承。
  • 最典型的 Go 变体是 HTTP 中间件func(next http.Handler) http.Handler 就是一个责任链节点,中间件之间互相包装形成链,net/httpServeMux 就是链尾。
  • 当链的节点数量少、顺序固定时,直接用切片 + 循环遍历[]Handler 按序尝试)比「对象持有 next」更简单,逻辑等价。
  • 社区真实看法:责任链在 Go 中价值较高,中间件模式几乎是 Web 框架标配;但链过长会难以调试,生产上链长一般控制在个位数。

14. 命令(Command)

Go 简化要点:命令在 Go 里就是一个函数值type Command func()func() (revert func(), err error) 即可表达,完全不需要类层次。

模式介绍

命令模式把「请求」封装为独立对象,使请求的发送者(如按钮)与执行者(如编辑器)完全解耦。命令对象可以被排队、记录历史、支持撤销(undo)与重做(redo),甚至远程传输。

设计逻辑

经典结构包含四个角色:命令(声明 Execute 接口)、具体命令(绑定接收者与参数)、接收者(真正执行业务逻辑)、调用者(触发命令)。在 Go 中,命令接口退化为函数类型,具体命令就是「闭包」,调用者只需要持有一个 func() 字段。

应用场景
  • 任务队列 / 定时任务(把操作封装后延迟执行)
  • 编辑器的撤销 / 重做(把每次操作连同其反操作入栈)
  • 按钮、菜单与快捷键共用的统一命令分发
  • 网络远程调用(如 net/rpc 把命令序列化传输)
生产示例
package main

import "fmt"

// Command 命令类型:一个可执行的函数值,无需接口与类层次
type Command func()

// TaskQueue 调用者:把命令与执行时机解耦,支持排队
type TaskQueue struct {
	commands []Command
}

// Add 把命令加入队列
func (q *TaskQueue) Add(c Command) {
	q.commands = append(q.commands, c)
}

// Run 依次执行队列中的所有命令
func (q *TaskQueue) Run() {
	for _, c := range q.commands {
		c()
	}
}

// Light 接收者:真正执行开灯/关灯逻辑
type Light struct{ on bool }

func (l *Light) TurnOn() {
	l.on = true
	fmt.Println("灯已打开")
}

func (l *Light) TurnOff() {
	l.on = false
	fmt.Println("灯已关闭")
}

func main() {
	light := &Light{}

	// 用闭包把「接收者 + 方法」封装成命令对象
	onCmd := func() { light.TurnOn() }
	offCmd := func() { light.TurnOff() }

	// 调用者只认识 Command,不认识 Light
	queue := &TaskQueue{}
	queue.Add(onCmd)
	queue.Add(offCmd)
	queue.Add(onCmd)

	fmt.Println("执行命令队列:")
	queue.Run()
}
Go 简化 / 变体
  • 经典 OOP 需要为每个动作定义一个命令子类;Go 里函数类型 + 闭包直接代替,Command 就是 func()
  • 需要撤销时,惯用写法是把「操作函数」与「反操作函数」成对入栈:type Command func() (undo func()),配合历史栈即可实现 undo。
  • 命令可以天然地通过 channel 排队、在 goroutine 中异步执行,time.AfterFunc、任务调度器等都是命令模式的 Go 变体。
  • 社区真实看法:命令模式在 Go 中价值高,但通常以「函数字段 + 队列」的极简形态出现,很少有人为其定义接口层次。

15. 迭代器(Iterator)

Go 简化要点:Go 原生 range 已支持数组 / 切片 / map / channel;Go 1.23+ 进一步支持 range over func,自定义迭代器只需实现 func(yield func(T) bool) 签名即可,无需任何迭代器对象。

模式介绍

迭代器模式提供一种统一方式顺序访问集合中的元素,而不暴露集合的内部表示。它把「遍历」从「集合」中分离出来,调用方只需要关心「下一个元素是什么」。

设计逻辑

经典结构包含迭代器Next / HasNext)与聚合(返回迭代器的 CreateIterator)两个角色。Go 中该模式被语言特性大幅取代:内置容器直接支持 range,channel 天然适合流式迭代,而 Go 1.23+ 引入的 iter.Seq 约定(func(yield func(T) bool))让函数式迭代器也能被 range 直接遍历。

应用场景
  • 遍历自定义数据结构(树、图、跳表)而不暴露内部指针
  • 惰性序列 / 无限序列(斐波那契、质数生成器)
  • 从数据库游标、网络流中逐条读取数据
  • 并发流水线:用 channel 在多个 goroutine 间传递元素
生产示例
package main

import "fmt"

// Seq 迭代器类型:Go 1.23+ 约定,函数逐个向 yield 投递元素
// 只要实现该签名,就能直接用 range 遍历
type Seq[T any] func(yield func(T) bool)

// Fibonacci 生成斐波那契数列前 n 项的惰性迭代器
func Fibonacci(n int) Seq[int] {
	return func(yield func(int) bool) {
		a, b := 0, 1
		for i := 0; i < n; i++ {
			if !yield(a) { // yield 返回 false 表示调用方提前退出
				return
			}
			a, b = b, a+b
		}
	}
}

// ChannelIter 经典 channel 迭代器:用 channel 实现流式遍历
func ChannelIter(count int) <-chan int {
	ch := make(chan int)
	go func() {
		defer close(ch)
		for i := 1; i <= count; i++ {
			ch <- i
		}
	}()
	return ch
}

func main() {
	// Go 1.23+ 直接 range 一个迭代器函数
	fmt.Println("斐波那契前 6 项:")
	for v := range Fibonacci(6) {
		fmt.Printf("%d ", v)
	}
	fmt.Println()

	// 经典 channel 迭代器
	fmt.Println("channel 迭代自然数 1~5:")
	for n := range ChannelIter(5) {
		fmt.Printf("%d ", n)
	}
	fmt.Println()
}
Go 简化 / 变体
  • 经典 OOP 的 Iterator 接口(Next / HasNext)在 Go 中几乎消失,被原生 range 取代:数组、切片、map、字符串、channel 全部原生支持。
  • Go 1.23+ 的 range over func 是重大简化:只要实现 func(yield func(T) bool) 就能让自定义集合参与 range,标准库 iter 包提供了 Seq / Seq2 类型别名,slicesmaps 包还提供现成的迭代器工具函数。
  • 需要「手动控制遍历进度」时,channel 迭代器(配合 range<-ch)是最常见的替代。
  • 社区真实看法:迭代器在 Go 中基本被语言特性消化,除非实现自己的容器,否则很少需要手写迭代器;如果写,优先用 Go 1.23+ 的函数式迭代器,它惰性求值且不泄漏并发细节。

16. 中介者(Mediator)

Go 简化要点:Go 里常用 channel 或中心化的总线结构体替代对象之间的网状引用;标准库 sync.Cond 也可用于广播式的协作。

模式介绍

中介者模式用一个「中介对象」封装一组对象之间的交互,让这些对象不再互相直接引用,从而降低耦合。它把「多对多」的网状依赖简化为「多对一」的星形依赖,交互逻辑集中到中介者中统一管理。

设计逻辑

经典结构包含中介者(定义通信接口)与同事对象(只依赖中介者)。Go 中中介者可以是:一个持有成员映射和消息 channel 的结构体、一个回调函数表,或一个集中管理协作状态的对象。同事对象只调用中介者的 Notify / Send 方法,具体如何协调由中介者内部决定。

应用场景
  • 聊天室 / 消息群:成员只向聊天室发消息,由聊天室广播
  • 控制塔与飞机的通信协调
  • 复杂 UI 表单:多个输入框、校验器之间的联动
  • 多组件协作的分布式协调器(如基于 channel 的 worker 调度)
生产示例
package main

import "fmt"

// Mediator 中介者接口:统一协调各方
type Mediator interface {
	Notify(sender, event string)
}

// ControlTower 控制塔:中介者的具体实现
type ControlTower struct {
	runwayFree bool
}

func NewControlTower() *ControlTower {
	return &ControlTower{runwayFree: true}
}

// Notify 中介者核心:根据事件类型集中协调各方
func (t *ControlTower) Notify(sender, event string) {
	switch event {
	case "requestLanding":
		if t.runwayFree {
			fmt.Printf("[控制塔] 跑道空闲,允许 %s 降落\n", sender)
			t.runwayFree = false
		} else {
			fmt.Printf("[控制塔] 跑道被占用,%s 需等待\n", sender)
		}
	case "landed":
		fmt.Printf("[控制塔] %s 已降落,跑道释放\n", sender)
		t.runwayFree = true
	}
}

// Aircraft 同事对象:只依赖中介者,不与其他飞机直接通信
type Aircraft struct {
	name  string
	tower Mediator
}

func (a *Aircraft) RequestLanding() {
	fmt.Printf("%s 请求降落\n", a.name)
	a.tower.Notify(a.name, "requestLanding")
}

func (a *Aircraft) Landed() {
	a.tower.Notify(a.name, "landed")
}

func main() {
	tower := NewControlTower()
	planeA := &Aircraft{name: "航班A", tower: tower}
	planeB := &Aircraft{name: "航班B", tower: tower}

	planeA.RequestLanding() // 跑道空闲,允许
	planeB.RequestLanding() // 跑道被 A 占用,需等待
	planeA.Landed()         // 释放跑道
	planeB.RequestLanding() // 跑道已释放,允许
}
Go 简化 / 变体
  • 经典 OOP 为中介者定义抽象接口、为同事定义基类;Go 里中介者就是一个结构体,同事持有它的引用即可,接口按需引入。
  • channel 总线是 Go 特有的中介者变体:同事把消息写进中心 channel,一个分发 goroutine 再把消息投递给其他成员,天然并发安全。
  • sync.Cond 可视为「条件广播式」中介者:多个等待者通过一个条件变量互相协调唤醒。
  • 社区真实看法:中介者价值较高,尤其在并发协作场景中,channel 总线几乎是最自然的解法;但要注意别把「上帝对象」伪装成中介者,交互逻辑应保持单一职责。

17. 备忘录(Memento)

Go 简化要点:Go 中「结构体值拷贝 + 快照」即可实现备忘录,利用值语义天然做到不可变快照;注意含引用字段时要主动深拷贝。

模式介绍

备忘录模式在不破坏封装的前提下,捕获并外部化一个对象的内部状态,以便之后恢复到该状态。它解决了「回滚 / 撤销」需求:对象自己负责生成与恢复快照,外部管理者只负责保存快照。

设计逻辑

经典结构包含发起人(Originator,创建与恢复快照)、备忘录(Memento,存储状态的不可变对象)、负责人(Caretaker,保存并管理备忘录)。Go 中备忘录通常就是一个只读结构体,发起人的 New() State 方法返回当前状态的副本,Restore(State) 方法把状态换回去;外部「历史栈」持有快照切片即可。

应用场景
  • 文本编辑器的撤销 / 重做
  • 数据库事务的回滚日志(保存变更前快照)
  • 游戏存档 / 断点续传
  • 配置中心的版本回退
生产示例
package main

import "fmt"

// EditorState 备忘录:保存编辑器某一时刻的只读快照
type EditorState struct {
	text string
}

// Editor 发起人:负责创建快照与恢复状态
type Editor struct {
	text string
}

// New 生成当前状态的快照(备忘录)
func (e *Editor) New() EditorState {
	// 结构体值拷贝:对基本类型字段天然不可变
	return EditorState{text: e.text}
}

// Restore 从快照恢复状态
func (e *Editor) Restore(s EditorState) {
	e.text = s.text
}

// Write 追加文本
func (e *Editor) Write(s string) {
	e.text += s
}

func (e *Editor) String() string {
	return e.text
}

func main() {
	// 负责人:外部历史栈,只保存快照,不关心快照内部结构
	history := []EditorState{}

	ed := &Editor{}
	ed.Write("第一行\n")
	history = append(history, ed.New()) // 保存快照 1

	ed.Write("第二行\n")
	history = append(history, ed.New()) // 保存快照 2

	ed.Write("第三行\n")
	fmt.Printf("当前内容:\n%s", ed.String())

	// 回滚到上一个快照
	ed.Restore(history[len(history)-1])
	fmt.Printf("回滚到快照后:\n%s", ed.String())
}
Go 简化 / 变体
  • 经典 OOP 用嵌套类、私有字段保证备忘录不被篡改;Go 里用小写字段的导出结构体值拷贝即可,函数签名上直接返回具体类型,无需接口。
  • Go 的结构体是值语义s := *orig 就对基本类型字段实现了深拷贝,天然满足「快照不可变」。
  • 注意陷阱:如果对象包含指针 / slice / map 字段,直接拷贝只复制引用,需用 copyclone 等方式深拷贝,否则快照会随原对象被修改而失效。
  • 社区真实看法:备忘录在 Go 中价值中等,简单场景一个快照结构体 + 历史切片就够了;但复杂对象的状态克隆成本高,生产上常结合「事件溯源」而非全量快照。

18. 观察者(Observer)

Go 简化要点:Go 常用 channel 广播回调函数实现观察者;标准库 context.Context 的取消传播本质上就是一种观察者。

模式介绍

观察者模式定义一对多的依赖:当一个对象(主题)状态改变时,所有依赖它的对象(观察者)都会收到通知并自动更新。它把「状态变更」与「后续处理」解耦,新增观察者无需改动主题。

设计逻辑

经典结构包含主题(维护观察者列表、提供订阅/退订、状态变化时广播)与观察者(提供更新回调)。Go 中观察者退化为函数值 func(event string),主题内部持有一个回调切片;广播可以同步遍历,也可以投递到 channel 由观察者异步消费。

应用场景
  • 事件总线 / 发布订阅(内存消息总线)
  • UI 数据绑定:模型变化自动刷新多个视图
  • 监控系统:指标变化触发告警、日志等多个订阅方
  • context 的取消传播、signal.Notify 的信号通知
生产示例
package main

import (
	"context"
	"fmt"
	"sync"
)

// Observer 观察者:订阅者用回调函数表示即可
type Observer func(event string)

// Subject 主题:被观察对象
type Subject struct {
	observers []Observer
}

// Register 注册观察者(订阅)
func (s *Subject) Register(o Observer) {
	s.observers = append(s.observers, o)
}

// Notify 广播事件给所有观察者
func (s *Subject) Notify(event string) {
	for _, o := range s.observers {
		o(event)
	}
}

func main() {
	sub := &Subject{}

	// 两个观察者分别注册回调
	sub.Register(func(e string) {
		fmt.Printf("观察者A 收到事件: %s\n", e)
	})
	sub.Register(func(e string) {
		fmt.Printf("观察者B 收到事件: %s\n", e)
	})
	sub.Notify("数据已更新")

	fmt.Println("--- 标准库 context 的取消传播也是一种观察者 ---")
	ctx, cancel := context.WithCancel(context.Background())
	var wg sync.WaitGroup
	wg.Add(1)

	// 子协程「订阅」ctx 的取消事件
	go func() {
		defer wg.Done()
		<-ctx.Done() // 阻塞等待取消通知
		fmt.Println("子协程收到取消信号,退出")
	}()

	cancel()  // 广播取消事件给所有监听者
	wg.Wait() // 等待子协程退出,保证输出稳定
	fmt.Println("主程序继续运行")
}
Go 简化 / 变体
  • 经典 OOP 要求观察者实现 Update() 接口;Go 里函数值回调即可,主题只需要存 []func(event)
  • channel 广播是 Go 特有的并发变体:主题把事件写进 channel,订阅者各自从自己的 channel 读取,天然支持异步与多消费者。
  • context.Context 的取消传播是观察者的最典型 Go 实现:多个 goroutine 监听 ctx.Done()cancel() 一次性广播给所有监听者。
  • 注意:回调里不要做耗时操作,避免阻塞广播;生产上可用「带缓冲 channel + 每订阅者一个 goroutine」隔离。
  • 社区真实看法:观察者价值很高,contextsignal.Notifysync.Cond 都是标准库里的活例子;但自建事件总线时要注意退订与生命周期管理,避免 goroutine 泄漏。

19. 状态(State)

Go 简化要点:Go 中状态机常用「当前状态 + 转移表」或「接口封装状态对象」两种方式;状态少时用转移表,状态行为复杂时用接口。

模式介绍

状态模式允许对象在内部状态改变时改变自身行为,看起来像是「换了类」。它把每个状态的专属行为封装成独立对象,避免用大量 if/else 判断状态。

设计逻辑

经典结构包含上下文(持有当前状态对象)、状态接口(声明各状态下可执行的行为)、具体状态(实现某个状态下的行为,并返回下一个状态)。Go 中上下文只需一个 state State 字段和一个转发方法;每个状态对象实现 Handle(ctx, event) State,通过返回值完成状态转移,转移规则集中但分散在各状态对象中。

应用场景
  • 订单 / 工单流转状态机(待支付 → 已支付 → 已发货)
  • 视频播放器的暂停 / 播放 / 缓冲状态切换
  • 网络连接的握手、就绪、关闭状态
  • 游戏角色的行走 / 攻击 / 受击状态
生产示例
package main

import "fmt"

// State 状态接口:封装某个状态下对外事件的响应
type State interface {
	// Handle 处理事件,返回转移后的下一个状态
	Handle(ctx *Context, event string) State
}

// Context 上下文:持有当前状态,并把事件转发给当前状态
type Context struct {
	state State
}

func (c *Context) SetState(s State) {
	c.state = s
}

func (c *Context) Handle(event string) {
	c.state = c.state.Handle(c, event)
}

// OrderedState 具体状态:订单已下单
type OrderedState struct{}

func (s *OrderedState) Handle(_ *Context, event string) State {
	switch event {
	case "pay":
		fmt.Println("已付款,订单进入「已支付」状态")
		return &PaidState{}
	default:
		fmt.Printf("已下单状态无法处理事件 %q\n", event)
		return s
	}
}

// PaidState 具体状态:订单已支付
type PaidState struct{}

func (s *PaidState) Handle(_ *Context, event string) State {
	switch event {
	case "ship":
		fmt.Println("已发货,订单进入「已发货」状态")
		return &ShippedState{}
	default:
		fmt.Printf("已支付状态无法处理事件 %q\n", event)
		return s
	}
}

// ShippedState 具体状态:终态,不再转移
type ShippedState struct{}

func (s *ShippedState) Handle(_ *Context, event string) State {
	fmt.Println("订单已完成,无法再处理事件")
	return s
}

func main() {
	order := &Context{state: &OrderedState{}}

	order.Handle("ship") // 未付款就发货?被拒绝
	order.Handle("pay")  // 付款,进入已支付
	order.Handle("ship") // 发货,进入已发货
	order.Handle("pay")  // 终态,忽略
}
Go 简化 / 变体
  • 经典 OOP 用「抽象状态类 + 每个状态一个子类」;Go 用接口 + 具体状态结构体即可,隐式实现让新增状态更容易。
  • 状态多但逻辑简单时,Go 社区更推荐转移表map[state]map[event]nextState,一个二维表搞定,比一堆状态对象更直观。
  • 状态对象无需持有上下文时,可以直接用函数字段描述行为:type State struct { onEvent func(ctx *Context, e string) },用组合替代继承。
  • 社区真实看法:状态模式价值较高,但 Go 里多数场景用转移表 + 枚举就够;只有状态行为本身复杂(各自有大量逻辑)才值得用接口封装状态对象。

20. 策略(Strategy)

Go 简化要点:策略在 Go 里就是一个函数值或「接口 + 闭包」,type Strategy func(...) 即可,运行时直接替换函数字段完成策略切换。

模式介绍

策略模式定义一族算法,把它们各自封装起来,并使它们可以互相替换。它让「算法的使用」与「算法的实现」分离,客户端可以在运行时选择不同的算法,且不必修改使用算法的代码。

设计逻辑

经典结构包含策略接口(所有算法的统一入口)、具体策略(各自算法实现)、上下文(持有当前策略并委托调用)。Go 中策略接口常常退化为函数类型,上下文持有一个函数字段,运行时直接赋值即可切换策略。

应用场景
  • 排序、压缩、加密等可替换的算法族
  • 电商促销:不同会员等级 / 优惠券采用不同计价规则
  • 日志、限流、负载均衡的多选一实现
  • 支付渠道选择(支付宝 / 微信 / 银行卡)
生产示例
package main

import "fmt"

// Strategy 策略类型:一个函数值即可表达整个算法族
type Strategy func(nums []int) []int

// Ascending 具体策略:升序排序
func Ascending(nums []int) []int {
	out := make([]int, len(nums))
	copy(out, nums)
	for i := 1; i < len(out); i++ {
		for j := i; j > 0 && out[j] < out[j-1]; j-- {
			out[j], out[j-1] = out[j-1], out[j]
		}
	}
	return out
}

// Descending 具体策略:降序排序
func Descending(nums []int) []int {
	out := Ascending(nums)
	// 反转得到降序
	for i, j := 0, len(out)-1; i < j; i, j = i+1, j-1 {
		out[i], out[j] = out[j], out[i]
	}
	return out
}

// Sorter 上下文:持有当前策略
type Sorter struct {
	strategy Strategy
}

// Sort 委托给当前策略执行
func (s *Sorter) Sort(nums []int) {
	fmt.Println(s.strategy(nums))
}

func main() {
	nums := []int{3, 1, 4, 1, 5}

	s := &Sorter{strategy: Ascending}
	fmt.Print("升序: ")
	s.Sort(nums)

	// 运行时直接替换函数字段,完成策略切换
	s.strategy = Descending
	fmt.Print("降序: ")
	s.Sort(nums)
}
Go 简化 / 变体
  • 经典 OOP 需要为每个算法定义一个类并实现统一接口;Go 里函数类型 + 函数值即可,甚至不需要接口。
  • 需要把策略与参数绑定、携带状态时,用闭包func(threshold int) Strategy { return func(n []int) []int { ... } }
  • 策略作为参数传入时,直接用 sort.Sliceless func(i, j int) bool 就是策略模式的教科书例子。
  • 社区真实看法:策略模式在 Go 中价值很高,且是最贴合 Go 思维的模式之一——「函数作为一等公民」让策略几乎零成本;但注意策略过多时可退化为「策略表」统一管理。

21. 模板方法(Template Method)

Go 简化要点:Go 无继承,模板方法常用「结构体嵌入 + 可替换的钩子函数字段」或「接口组合」实现,把算法骨架固定、差异步骤交给注入的函数。

模式介绍

模板方法在基类中定义算法的骨架(固定流程),把其中一些步骤延迟到子类实现。它解决「同一套流程、多个变体」的问题:流程结构不变,个别步骤可以替换,从而避免复制整套流程代码。

设计逻辑

经典结构包含抽象类(骨架 + 抽象步骤)与具体子类(实现抽象步骤)。Go 没有继承,替代方案有两种:一是结构体嵌入 + 函数字段钩子,模板结构体持有一个 steps func(),调用者构造时注入差异实现;二是接口组合,模板结构体持有一个 Processor 接口字段,把差异步骤委托给它。

应用场景
  • 数据处理管道:读取 → 处理 → 输出,各阶段可替换
  • 框架的生命周期钩子(启动 → 业务 → 关闭)
  • 冲泡饮料、做菜等「固定流程 + 可变步骤」的流程建模
  • 网络请求的拦截器 / 钩子机制
生产示例
package main

import "fmt"

// CoffeeMaker 模板:用结构体 + 钩子函数字段定义固定算法骨架
type CoffeeMaker struct {
	// 钩子:由调用者注入的差异步骤,替代经典 OOP 中的抽象方法
	brew   func()
	addons func()
}

// MakeCoffee 模板方法:流程骨架固定,差异步骤延迟到钩子实现
func (c *CoffeeMaker) MakeCoffee() {
	fmt.Println("1. 烧水")
	c.brew() // 子步骤1:冲煮方式(可变)
	fmt.Println("3. 倒入杯中")
	c.addons() // 子步骤2:添加物(可变)
	fmt.Println("5. 完成")
}

func main() {
	// 美式咖啡:注入一组钩子实现
	americano := &CoffeeMaker{
		brew:   func() { fmt.Println("2. 用滴滤壶冲煮咖啡粉") },
		addons: func() { fmt.Println("4. 无需添加") },
	}
	americano.MakeCoffee()

	fmt.Println("---")

	// 拿铁:注入另一组钩子,骨架完全复用
	latte := &CoffeeMaker{
		brew:   func() { fmt.Println("2. 萃取浓缩咖啡液") },
		addons: func() { fmt.Println("4. 加入蒸热牛奶") },
	}
	latte.MakeCoffee()
}
Go 简化 / 变体
  • 经典 OOP 用「抽象类 + 抽象方法」强制子类实现差异步骤;Go 用函数字段注入,调用方在构造时提供差异实现,未提供的钩子用 nil 判断优雅降级。
  • 另一种惯用替代是接口组合:模板结构体持有一个接口字段,把差异步骤委托给接口实现,适合差异步骤多且需要状态的情况。
  • 可以用「嵌入 + 覆盖」模拟继承:内嵌基础结构体,对外方法自定义,但 Go 社区更推荐显式的函数字段或接口字段,因为意图更清晰。
  • 社区真实看法:模板方法在 Go 中价值中等,函数字段 / 接口字段两种替代都很轻量;但要警惕「深模板 + 多钩子」造成的回调地狱,钩子一般控制在两三个以内。

22. 访问者(Visitor)

Go 简化要点:Go 中常用 type switch / 空接口 替代访问者,社区普遍认为该模式在 Go 中价值低,属于「被语言特性取代」的典型案例。

模式介绍

访问者模式将「作用于一组对象结构的操作」与「对象结构本身」分离,让你能在不修改各个对象类的情况下,为它们增加新操作。它擅长处理稳定的对象结构 + 多变的操作

设计逻辑

经典结构包含访问者(为每种元素提供访问方法)、具体访问者(实现某操作)、元素(提供 Accept 方法)、对象结构(持有元素集合)。其核心是双重分派(double dispatch)。Go 没有方法重载,双重分派难以自然表达,因此社区通常改用 type switch + 类型断言:遍历节点,用 switch n := node.(type) 分派到不同逻辑,效果等价且简单得多。

应用场景
  • 在 AST / 表达式树上执行多种遍历(求值、打印、类型检查)
  • 编译器的语义分析:同一语法树支持多个 pass
  • 对对象结构做「一次性附加操作」且不想改动元素类
生产示例
package main

import "fmt"

// Node 表达式节点:用空接口表示,配合 type switch 做分派
type Node any

// 具体节点类型
type Number struct{ val int }
type Add struct {
	left, right Node
}
type Mul struct {
	left, right Node
}

// Eval 借助 type switch 实现「访问者」:新增操作无需修改节点类型
func Eval(n Node) int {
	switch v := n.(type) {
	case Number:
		return v.val
	case Add:
		return Eval(v.left) + Eval(v.right)
	case Mul:
		return Eval(v.left) * Eval(v.right)
	default:
		panic(fmt.Sprintf("未知节点类型 %T", n))
	}
}

// Print 另一个「访问者」:同样靠 type switch,操作之间互不影响
func Print(n Node) string {
	switch v := n.(type) {
	case Number:
		return fmt.Sprintf("%d", v.val)
	case Add:
		return fmt.Sprintf("(%s + %s)", Print(v.left), Print(v.right))
	case Mul:
		return fmt.Sprintf("(%s * %s)", Print(v.left), Print(v.right))
	default:
		return "?"
	}
}

func main() {
	// 表达式: (1 + 2) * 3
	expr := Mul{
		left:  Add{left: Number{1}, right: Number{2}},
		right: Number{3},
	}

	fmt.Printf("表达式 %s 求值结果 = %d\n", Print(expr), Eval(expr))
}
Go 简化 / 变体
  • 经典访问者依赖「方法重载 + 双重分派」,Go 两者都不支持;type switch + 空接口是社区公认的替代,代码量更少、意图更清晰。
  • 新增操作 = 新增一个 switch 函数,无需修改元素类型,这一点与访问者目标一致,但少了 Accept 的样板代码。
  • 若追求「真正的」访问者(避免每次新增元素类型都要改所有 switch),可以用「元素接口带 Accept(Visitor) + 访问者接口」,但 Go 社区认为这种写法收益低、样板多。
  • 社区真实看法:访问者模式在 Go 中价值低,是「过度设计」的典型;type switch 通常足够,且更符合 Go 的风格。只有对象结构非常稳定、操作需要频繁扩展时,才值得考虑经典实现。

23. 解释器(Interpreter)

Go 简化要点:Go 中通常用「成熟的解析器库」或直接手写递归下降解析器,此模式价值低,经典 OOP 的「解释器类层次」很少被采用。

模式介绍

解释器模式为特定语言定义文法,并用一组「解释器对象」逐条解释句子。它把「语法规则」建模为对象,适合表达式求值、简单 DSL 等场景。但一旦文法复杂,该模式会急剧膨胀,生产上更常直接使用解析器生成器或手写递归下降。

设计逻辑

经典结构包含抽象表达式Interpret 接口)、终结符表达式(叶子)、非终结符表达式(组合规则)与上下文(解析输入)。Go 中更实用的是手写递归下降:用「词法分析器(lexer)+ 语法分析器(parser)」两阶段,按文法逐层递归下降解析,配合 strconvfmt 等标准库完成求值,代码清晰且可控。

应用场景
  • 简单表达式求值器(算术、布尔表达式)
  • 配置项 DSL 的解析与执行
  • 规则引擎:把「IF 条件 THEN 动作」文本解析成可执行结构
  • 模板 / 查询语言的小型解释器
生产示例
package main

import (
	"fmt"
	"strconv"
	"unicode"
)

// token 词法单元
type token struct {
	kind  string // "num" 数字 | "op" 运算符 | "eof" 结束
	value string
}

// lexer 词法分析:把输入字符串拆分为 token 序列(支持多位数)
func lexer(input string) []token {
	var tokens []token
	runes := []rune(input)
	for i := 0; i < len(runes); {
		r := runes[i]
		switch {
		case unicode.IsSpace(r):
			i++ // 跳过空白
		case unicode.IsDigit(r):
			start := i
			for i < len(runes) && unicode.IsDigit(runes[i]) {
				i++
			}
			tokens = append(tokens, token{kind: "num", value: string(runes[start:i])})
		case r == '+' || r == '-':
			tokens = append(tokens, token{kind: "op", value: string(r)})
			i++
		default:
			i++ // 忽略非法字符
		}
	}
	return tokens
}

// parser 递归下降解析器:文法为 expr := num (('+'|'-') num)*
type parser struct {
	tokens []token
	pos    int
}

// next 取当前 token 并前进
func (p *parser) next() token {
	t := p.tokens[p.pos]
	p.pos++
	return t
}

// peek 预览当前 token,越界时返回 eof
func (p *parser) peek() token {
	if p.pos >= len(p.tokens) {
		return token{kind: "eof"}
	}
	return p.tokens[p.pos]
}

// parseExpr 解析并求值加减法表达式
func (p *parser) parseExpr() int {
	left, _ := strconv.Atoi(p.next().value) // 第一个操作数
	for p.peek().kind == "op" {
		op := p.next().value
		right, _ := strconv.Atoi(p.next().value)
		if op == "+" {
			left += right
		} else {
			left -= right
		}
	}
	return left
}

func main() {
	expr := "3 + 5 - 2"
	p := &parser{tokens: lexer(expr)}
	fmt.Printf("%s = %d\n", expr, p.parseExpr())
}
Go 简化 / 变体
  • 经典 OOP 用「表达式接口 + 大量具体表达式类」建模语法,Go 里手写递归下降(lexer + parser)更主流,代码量少、性能好、易调试。
  • 文法更复杂时,Go 社区通常引入真实解析器库(如 goyacc、participle、pigeon),而不是扩展解释器模式的对象层次。
  • text/templatehtml/templateregexpgo/ast 等标准库本身就是「解释器 / 编译器的 Go 实现」范例,可以直接复用其设计思想。
  • 社区真实看法:解释器模式在 Go 中价值低,属于经典但很少直接套用的模式;简单表达式用递归下降,复杂文法用现成解析工具,不要为「把语法建模成对象」而过度设计。

本部分小结:行为型模式在 Go 中呈现出明显两极分化。策略、命令、观察者、责任链因函数一等公民和 channel 的存在而变得极其轻量,是日常高频使用的高价值模式;状态、中介者、模板方法有清晰且简洁的 Go 变体(转移表、channel 总线、钩子函数字段);而访问者、解释器则因缺少重载 / 继承而被语言特性或更实用的解析手段取代,价值较低。总体原则与 Go 社区一致:优先用语言原生能力与更简单的结构,避免为模式而模式


第四部分:Go 社区特有的惯用模式(Idiomatic Go Patterns)

前三个部分讲完了经典 23 种 GoF 设计模式在 Go 中的实现,本部分把视角切换到 Go 社区的真实日常。很多 GoF 模式在 Go 里被语言特性简化或替代——比如用闭包替代策略对象、用 channel 替代观察者事件、用 context.Context 取代一层层的超时参数。Go 程序员真正天天写的是这一部分讲的「基于语言特性的惯用模式」。理解这些,你才算真正会写 Go。

24. 函数式选项(Functional Options)

对应 GoF:可视为 Builder 的 Go 变体,用于可扩展配置

核心思想

当一个类型需要大量可选配置参数时,逐一写构造函数重载(Go 不支持)或用一个巨大的配置结构体会让调用方痛苦。函数式选项用「返回闭包的函数」作为参数,把每个可选项封装成一个 Option,构造时按需传入、逐个应用。它天然向后兼容:新增选项只影响新增的 WithXxx 函数,不破坏已有调用。

设计逻辑

结构由三部分组成:

  • type Option func(*Config):一个选项就是一个「接收目标对象并修改它」的函数。
  • WithXxx(value) Option:每个可选项是一个工厂函数,返回一个捕获了 value 的闭包。
  • NewXxx(requiredParams..., opts ...Option) *Config:先构造带默认值的对象,再遍历 opts 依次应用到对象上。

这样设计的好处是:必填参数走普通位置参数(编译器强制),可选参数走变长切片,调用方可以按任意顺序、任意数量传参,且不传就是默认值。

应用场景
  • net/http.ServerReadTimeoutWriteTimeoutMaxHeaderBytes 等一堆可选字段。
  • gRPC:grpc.DialOption / grpc.CallOption,官方就是函数式选项的教科书。
  • database/sqlSetMaxOpenConnsSetMaxIdleConnsSetConnMaxLifetime 这套连接池配置。
  • google.golang.org/grpcclient-go、AWS/腾讯云等各家 SDK 的客户端构造。
  • 自己封装第三方库时,用于暴露「少量必填 + 大量可选」的构造接口。
生产示例
package main

import (
	"fmt"
	"time"
)

// Server 服务器配置结构体
type Server struct {
	addr         string        // 监听地址(必填)
	readTimeout  time.Duration // 读超时(可选)
	writeTimeout time.Duration // 写超时(可选)
	maxConns     int           // 最大连接数(可选)
}

// Option 选项类型:一个接收 *Server 并修改其字段的函数
type Option func(*Server)

// WithReadTimeout 设置读超时,返回一个捕获了 d 的 Option 闭包
func WithReadTimeout(d time.Duration) Option {
	return func(s *Server) {
		s.readTimeout = d
	}
}

// WithWriteTimeout 设置写超时
func WithWriteTimeout(d time.Duration) Option {
	return func(s *Server) {
		s.writeTimeout = d
	}
}

// WithMaxConns 设置最大连接数
func WithMaxConns(n int) Option {
	return func(s *Server) {
		s.maxConns = n
	}
}

// NewServer 构造函数:先填充默认值,再按顺序应用传入的选项
func NewServer(addr string, opts ...Option) *Server {
	s := &Server{
		addr:         addr,
		readTimeout:  5 * time.Second, // 默认读超时
		writeTimeout: 5 * time.Second, // 默认写超时
		maxConns:     100,             // 默认最大连接数
	}
	for _, opt := range opts { // 依次应用每个选项
		opt(s)
	}
	return s
}

func main() {
	// 只传必填参数,其余全部使用默认值
	s1 := NewServer(":8080")

	// 按需覆盖部分配置,未指定的仍为默认值
	s2 := NewServer(":9090",
		WithReadTimeout(10*time.Second),
		WithMaxConns(1000),
	)

	fmt.Printf("s1 地址=%s 读超时=%v 最大连接数=%d\n", s1.addr, s1.readTimeout, s1.maxConns)
	fmt.Printf("s2 地址=%s 读超时=%v 最大连接数=%d\n", s2.addr, s2.readTimeout, s2.maxConns)
}
Go 实现要点 / 注意事项
  • 相比 GoF Builder:函数式选项是编译期检查的——选项目录就是 WithXxx 函数,拼错方法名编译器直接报错,而字符串式的 Builder 要到运行时才失败;配置一旦构造完成就是不可变对象,天然线程安全,无需加锁;Go 没有重载也没有构造器链(链式方法必须返回同一类型、难以做类型安全的阶段约束),所以 Builder 在 Go 里远不如函数式选项顺手。
  • Option 执行顺序敏感:选项是按传入顺序应用的,后传的覆盖先传的。若选项之间互相影响(如 WithPool 依赖 WithAddr),调用方需注意顺序,文档里应写清楚。
  • 默认值处理:默认值只在构造函数里设置一次,Option 里不要再去「恢复默认」。若某字段零值有特殊含义(如 0 表示不超时),默认值阶段要显式区分「未设置」和「设置为 0」。
  • 暴露字段 vs 私有字段:选项闭包只能通过同包构造函数修改私有字段,保证外部无法绕过选项直接篡改,封装性更好;若字段本身需要外部读写,就别用选项模式,直接暴露字段即可,避免过度设计。

25. 并发管道 Pipeline(扇入扇出 Fan-in/Fan-out)

对应 GoF:无直接对应,是「数据流」视角的并发结构;思想与责任链(Chain of Responsibility)相近,但阶段之间用 channel 并行衔接

核心思想

把一次数据处理拆成若干阶段(stage),每个阶段是一个 goroutine:从一个 channel 读入数据、做处理、写入下一个 channel。数据像流水线一样流经各阶段,天然并行。当某个阶段处理很慢时,可启动多个 goroutine 并行处理再合并——多个源汇入一个 channel 叫「扇入(fan-in)」,一个源分发给多个 goroutine 叫「扇出(fan-out)」。

设计逻辑

每个阶段的典型形态是「接收 <-chan T,返回 <-chan R」:

  • 扇出:多个 goroutine 同时 range 同一个只读 channel,每个元素只会被其中一个 goroutine 消费,任务自然分流。
  • 扇入:用 sync.WaitGroup 等所有源 channel 都被读完,再关闭汇合 channel,接收方通过「channel 关闭」感知数据结束。
  • channel 既传递数据也传递结束信号:close(ch)range 自动退出,这是 Go 里最优雅的「流终止」机制。
应用场景
  • ETL 数据处理:读取 → 清洗 → 转换 → 入库。
  • 日志处理:采集 → 解析 → 过滤 → 归档。
  • 图像/视频流水线:解码 → 缩放 → 编码。
  • 需要横向扩容的「生产者-消费者」链,每个环节可独立调整并发度。
生产示例
package main

import (
	"fmt"
	"sync"
)

// gen 生成阶段:把输入的整数依次写入 channel
func gen(nums ...int) <-chan int {
	out := make(chan int)
	go func() {
		for _, n := range nums {
			out <- n
		}
		close(out) // 所有数据发完即关闭,通知下游结束
	}()
	return out
}

// sq 处理阶段:读取一个 channel,把每个数平方后写入新 channel
func sq(in <-chan int) <-chan int {
	out := make(chan int)
	go func() {
		for n := range in {
			out <- n * n
		}
		close(out)
	}()
	return out
}

// fanIn 扇入阶段:把多个 channel 合并为一个 channel
func fanIn(chs ...<-chan int) <-chan int {
	out := make(chan int)
	var wg sync.WaitGroup

	// 每个输入 channel 各启动一个 goroutine 转发到 out
	for _, ch := range chs {
		wg.Add(1)
		go func(c <-chan int) {
			defer wg.Done()
			for n := range c {
				out <- n
			}
		}(ch)
	}

	// 所有转发 goroutine 结束后关闭 out
	go func() {
		wg.Wait()
		close(out)
	}()
	return out
}

func main() {
	// 扇出:同一个 gen 输出,分发给 3 个平方 goroutine 并行处理
	in := gen(1, 2, 3, 4, 5, 6, 7, 8)
	ch1 := sq(in)
	ch2 := sq(in)
	ch3 := sq(in)

	// 扇入:合并 3 条处理流
	for n := range fanIn(ch1, ch2, ch3) {
		fmt.Print(n, " ")
	}
	fmt.Println()
}
Go 实现要点 / 注意事项
  • 记得 close channel:每个阶段结束时必须 close(out),否则下游 range 永远等不到结束而死锁。这是新手最常踩的坑。
  • 扇出时不要 close 共享输入:多个 goroutine 同时读一个 channel 没问题,但只能由生产者 close,消费者绝不能关闭它,否则会 panic: send on closed channelclose of closed channel
  • 扇入记得 wg.Wait() 后再 close 汇合 channel:如果提前关闭,其余 goroutine 再写入就会 panic。
  • channel 的容量语义:无缓冲 channel 天然做「背压」(生产者必须等消费者取走才继续),有缓冲 channel 允许一定程度的流水线缓冲,两者各有取舍。
  • 注意 goroutine 泄漏:如果某个阶段提前退出(比如后面有 break),前面的生产者可能永远阻塞在 out <- n。生产环境要配合 context 取消来兜底(见第 27 节)。

26. 工作池 Worker Pool

对应 GoF:可视为 Object Pool 的变体,但池化的是「执行协程」而非「对象」

核心思想

预先启动固定数量的 worker goroutine,它们从同一个任务 channel 里取任务执行。任务量和 worker 数量解耦:任务再多也只有一个有界 channel 和一个固定 worker 数,天然限流,不会无限开 goroutine。用 sync.WaitGroup 统计所有 worker 是否干完,作为「收尾」信号。

设计逻辑
  • 一个有缓冲jobs chan 承接任务投递,投递方发完任务后 close(jobs) 表示「没有更多任务」。
  • N 个 worker goroutine 并发 range jobs,谁空闲谁取任务,取到就处理,结果写入 results
  • 所有 worker 结束时用 sync.WaitGroup 等待,wg.Wait() 返回后 close(results),主流程再消费结果。

它和管道(第 25 节)的区别:管道每个阶段是单一职责的串联,worker pool 是一组同质 worker 并行消费同一批任务,是「并行的分治」而非「串行的流」。

应用场景
  • 任务队列消费:从消息队列批量拉取并处理任务。
  • 批量 IO/HTTP:几千条 URL 要并发抓取,限制在几十个并发内。
  • 数据库批处理:批量写入/迁移数据。
  • 任何需要「并发但有上限」的限流场景,替代直接 go func() 无脑开协程。
生产示例
package main

import (
	"fmt"
	"sync"
	"time"
)

// worker 单个工作协程:从 jobs 读任务,计算结果写入 results
func worker(id int, jobs <-chan int, results chan<- int, wg *sync.WaitGroup) {
	defer wg.Done() // 本 worker 干完,通知 WaitGroup
	for job := range jobs {
		// 模拟耗时处理
		time.Sleep(50 * time.Millisecond)
		fmt.Printf("worker %d 处理任务 %d\n", id, job)
		results <- job * job
	}
}

func main() {
	const numJobs = 10   // 任务总数
	const numWorkers = 4 // 工作协程数

	jobs := make(chan int, numJobs)    // 有缓冲任务通道
	results := make(chan int, numJobs) // 有缓冲结果通道
	var wg sync.WaitGroup

	// 启动固定数量的 worker
	for i := 1; i <= numWorkers; i++ {
		wg.Add(1)
		go worker(i, jobs, results, &wg)
	}

	// 投递任务,发完关闭 jobs,通知 worker「没有更多任务」
	for j := 1; j <= numJobs; j++ {
		jobs <- j
	}
	close(jobs)

	// 收尾:等所有 worker 结束后再关闭 results
	wg.Wait()
	close(results)

	// 消费结果
	sum := 0
	for r := range results {
		sum += r
	}
	fmt.Println("结果总和:", sum)
}
Go 实现要点 / 注意事项
  • wg.Add 要在启动 goroutine 前调用,且最好在 worker 函数内 defer wg.Done(),避免忘记。绝不能在 goroutine 内部再 wg.Add,否则可能和 wg.Wait 产生竞态。
  • 收尾顺序必须是close(jobs)wg.Wait()close(results)jobsresults 都设为与任务量等容量的有缓冲 channel,可避免投递时和收集时阻塞;若任务量未知,可用无缓冲 channel 配合投递/收集同时进行。
  • 有缓冲 channel 的作用:缓冲让投递方不必等 worker 取走就能继续投,降低调度耦合;但缓冲越大,worker 崩溃时丢失的任务越多,需要权衡。
  • 若任务处理本身可能出错,结果结构应设计为 struct{ data T; err error },或改用 errgroup.Groupgolang.org/x/sync/errgroup)让第一个错误传播并取消其余任务。

27. Context 传递与取消

对应 GoF:无直接对应,是横切关注点;行为上类似「请求作用域」的拦截器/过滤器,贯穿整个调用链

核心思想

context.Context 是 Go 并发世界最核心的惯用法:它承载「调用链的截止时间、取消信号、少量键值元数据」,并作为第一个参数显式沿函数调用链向下传递。父 context 取消,派生的所有子 context 一并取消;超时到期,相关阻塞操作立即被中断。这让「一个请求挂了,它的所有下游协程、IO 都跟着收手」成为可能,是防止 goroutine 泄漏、实现超时控制的关键。

设计逻辑
  • 根节点是 context.Background()(无取消、无截止时间),用 context.WithTimeout / WithCancel / WithDeadline / WithValue 派生子 context。
  • 约定:凡是可能阻塞或需要被取消的函数,第一个参数必须是 ctx context.Context
  • 被取消后,ctx.Done() 返回的 channel 会被关闭,配合 select 即可让阻塞操作「醒过来」;ctx.Err() 返回取消原因(context.Canceledcontext.DeadlineExceeded)。
  • 关键纪律:context 永远作为参数传递,绝不放进结构体字段(除非对象本身代表一次请求)。
应用场景
  • HTTP 请求处理:一个请求的整个处理链共享同一个 context,客户端断开即取消。
  • 数据库/Redis 查询超时:db.QueryContext(ctx, ...) 让慢查询到时自动放弃。
  • 外部服务调用:http.NewRequestWithContext、gRPC 调用都原生接受 context。
  • 优雅关停、定时任务、服务间的分布式取消传播(gRPC/OpenTelemetry 都基于它)。
生产示例
package main

import (
	"context"
	"fmt"
	"time"
)

// doWork 模拟一个可能很慢的任务,随时响应 ctx 的取消信号
func doWork(ctx context.Context, name string) error {
	select {
	case <-time.After(3 * time.Second): // 模拟耗时 3 秒的工作
		fmt.Printf("%s 完成\n", name)
		return nil
	case <-ctx.Done(): // 收到取消/超时信号,立即返回
		return ctx.Err()
	}
}

func main() {
	// 派生一个 1 秒后自动超时的 context
	ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
	defer cancel() // 用完务必 cancel,释放定时器资源

	if err := doWork(ctx, "任务A"); err != nil {
		fmt.Println("任务A 被中断:", err) // 输出: context deadline exceeded
	}

	// 手动取消:ctx2 的取消会传播给基于它派生的所有子 context
	ctx2, cancel2 := context.WithCancel(context.Background())
	go func() {
		time.Sleep(500 * time.Millisecond)
		cancel2() // 500ms 后主动取消
	}()
	if err := doWork(ctx2, "任务B"); err != nil {
		fmt.Println("任务B 被中断:", err) // 输出: context canceled
	}
}
Go 实现要点 / 注意事项
  • defer cancel() 必写WithCancel / WithTimeout 内部会注册定时器或关联父 context,不调用 cancel 会泄漏资源。哪怕函数已经「成功返回」,也要 defer 释放。
  • context 放哪:第一个参数,显式传下去;绝不放进结构体、绝不存全局变量。标准库和 go vet 都有 lostcancel 检查帮助发现漏 cancel。
  • WithValue 要克制:它只适合传请求级、跨多层的少量元数据(如 trace id、user id),不适合传业务参数。键必须是自己定义的私有类型,避免和其他包冲突。
  • 回调风格context 的取消是「协作式」的——框架帮你关掉 Done(),但你的阻塞调用必须配合 select 监听 <-ctx.Done() 才能真正响应,否则依然会卡住。
  • 不要传 context.TODO() 冒充context.TODO() 只应作为「还没想好传什么」的临时占位,生产路径上要尽快替换成真实的 parent context。

28. 错误即值 Error as Value

对应 GoF:无直接对应——Go 用「返回值」替代了异常机制,这是语言级设计决策

核心思想

Go 没有异常(exception),错误是普通的一等值:函数把错误作为最后一个返回值显式交还给调用方,调用方必须处理。这个设计让错误处理路径和正常路径一样可见、可测试,不会像异常那样穿越多层隐式传播。配套的 fmt.Errorf("%w") 错误包装、errors.Is / errors.As 检查、哨兵错误(sentinel error)让错误既能「逐层添加上下文」又能「精确匹配根因」。

设计逻辑
  • 哨兵错误:用 errors.New 定义包级错误变量(如 sql.ErrNoRows),作为可比较的「错误常量」。
  • 包装fmt.Errorf("操作 X 失败: %w", err)%w 把原错误作为「链」保留下来。
  • 解包errors.Is(err, target) 沿链查找是否等于某个哨兵错误;errors.As(err, &target) 沿链查找是否存在某个类型的错误,取出后访问其附加字段。
  • 惯例:错误字符串小写、不结尾带标点,因为上层还会继续用 %w 拼上下文。
应用场景
  • 文件/网络/数据库等系统调用失败,层层包装传递现场信息。
  • 业务规则校验失败:定义自定义错误类型携带字段名等信息,用 errors.As 取出做精细化处理。
  • 需要区分「可重试」与「不可重试」、404500 等错误语义时,用哨兵错误或自定义类型区分。
  • 所有「可能失败」的函数——在 Go 里这几乎是全部 I/O 和资源操作。
生产示例
package main

import (
	"errors"
	"fmt"
	"os"
)

// 哨兵错误:包级错误变量,全包共用、可比较
var (
	ErrNotFound = errors.New("资源不存在")
	ErrInvalid  = errors.New("参数无效")
)

// ConfigError 自定义错误类型,可携带附加字段
type ConfigError struct {
	Field string // 出错的配置字段
	Msg   string // 错误详情
}

// Error 实现 error 接口
func (e *ConfigError) Error() string {
	return fmt.Sprintf("配置字段 %s 无效: %s", e.Field, e.Msg)
}

// loadUser 模拟加载用户,错误通过返回值传递
func loadUser(name string) (string, error) {
	if name == "" {
		return "", fmt.Errorf("loadUser: %w", ErrInvalid) // 包装哨兵错误
	}
	if name != "alice" {
		return "", fmt.Errorf("查找用户 %s: %w", name, ErrNotFound)
	}
	return "alice@example.com", nil
}

// parseConfig 返回自定义错误类型
func parseConfig() error {
	return &ConfigError{Field: "timeout", Msg: "必须是正数"}
}

func main() {
	// errors.Is:判断错误链中是否包含某个哨兵错误(能穿透 %w 包装)
	_, err := loadUser("bob")
	if errors.Is(err, ErrNotFound) {
		fmt.Println("命中哨兵错误 ErrNotFound:", err)
	}

	// errors.As:把错误断言为具体类型,取回附加字段
	if err := parseConfig(); err != nil {
		var cfgErr *ConfigError
		if errors.As(err, &cfgErr) {
			fmt.Printf("命中自定义错误类型,字段=%s 详情=%s\n", cfgErr.Field, cfgErr.Msg)
		}
	}

	// 标准库哨兵错误同样适用 errors.Is
	if _, ferr := os.Open("/不存在/文件.txt"); errors.Is(ferr, os.ErrNotExist) {
		fmt.Println("文件不存在:", ferr)
	}
}
Go 实现要点 / 注意事项
  • %w 只用于传递:如果要直接展示或只是临时用一次,用 %v(不包装)即可;只有需要被 errors.Is/As 追溯时才用 %w。不要用 fmt.Errorf%w 去包装「非 error」的值。
  • errors.As 的 target 必须是指向实现了 error 的类型的指针(如 var cfgErr *ConfigError; errors.As(err, &cfgErr)),写错会 panic 或永远返回 false。
  • 哨兵错误要用 errors.Is 而不是 ==:直接 err == ErrNotFound 在错误被包装后就会失配。
  • 不要吞错误result, _ := doSomething() 是反模式。要么 return err,要么显式记录日志并说明为何可忽略(例如 _ = w.Close() 这类尽力而为的清理)。
  • 错误字符串的坑:首字母小写、不加句号;错误信息要面向人,判断逻辑交给 errors.Is/As,不要用字符串 strings.Contains(err.Error(), "...") 判断错误。

29. 组合与嵌入 Composition & Embedding

对应 GoF:替代「继承」的核心手段,是 Composite、Decorator 等结构型模式在 Go 里的底层承载

核心思想

Go 用「组合」取代「继承」:一个结构体可以匿名嵌入另一个结构体(或接口),被嵌入类型的字段和方法被**提升(promote)**到外层,外层就像「天然拥有」了这些能力。嵌入不是继承——没有虚方法、没有多态覆写,纯粹是「把别人的方法打包到我的方法集里」。接口层面同样组合:io.ReadWriter 就是 io.Reader + io.Writer 拼出来的,大接口由小接口组合而成。

设计逻辑
  • 结构体嵌入:type Server struct { *Logger; addr string }Server 自动获得 Logger 的全部导出方法,可以 s.Log(...) 直接调用;若内嵌字段有名字(l *Logger),则只能 s.l.Log(...)
  • 方法集提升规则:值/指针嵌入决定提升的是值接收者还是全部方法,外层通常嵌入指针(*Logger)以便共享状态。
  • 接口组合:type ReadWriteCloser interface { io.Reader; io.Writer; Close() error },实现方只需实现全部单个方法,无需显式声明实现了组合接口。
应用场景
  • 标准库:io.Reader / io.Writer / io.ReadWriter 的接口组合;http.ResponseWriter 内嵌了 io.Writer
  • 为类型扩展能力:数据库模型嵌入 time.Time、服务结构嵌入共享的日志/指标组件。
  • 包装器模式:自定义 Writer 嵌入 io.Writer,只覆写需要改的方法(类似 Decorator)。
  • 中间件链:http.HandlerFunc 与结构体组合实现责任链。
生产示例
package main

import (
	"fmt"
	"io"
	"strings"
)

// Logger 基础日志组件,提供 Log 方法
type Logger struct {
	prefix string // 日志前缀
}

func (l *Logger) Log(msg string) {
	fmt.Printf("[%s] %s\n", l.prefix, msg)
}

// Server 匿名嵌入 *Logger,自动获得 Log 方法(方法提升)
type Server struct {
	*Logger // 匿名嵌入指针,Log 方法被提升到 Server
	name    string
}

// ReadWriteCloser 组合接口:由两个小接口拼接成一个大接口
type ReadWriteCloser interface {
	io.Reader
	io.Writer
	Close() error
}

func main() {
	// 嵌入:Server 直接调用提升上来的 Log 方法
	s := &Server{Logger: &Logger{prefix: "SERVER"}, name: "api"}
	s.Log("服务已启动") // 等价于 s.Logger.Log("服务已启动")

	// 接口组合:io.MultiWriter 把多个 Writer 拼成一个 Writer(装饰器思想)
	var b1, b2 strings.Builder
	mw := io.MultiWriter(&b1, &b2) // mw 同时满足 io.Writer
	if _, err := mw.Write([]byte("hello")); err != nil {
		fmt.Println("写入失败:", err)
	}
	fmt.Printf("b1=%q b2=%q\n", b1.String(), b2.String())
}
Go 实现要点 / 注意事项
  • 嵌入不是继承:Go 没有虚方法表,外层无法「覆写」内嵌方法;如果外层自己定义了同名方法,它只是遮蔽(shadow)内嵌方法,而不是多态覆写。需要多态请用接口(第 30 节)。
  • 字段/方法名冲突:两层嵌入了同名方法或字段会编译报错(ambiguous selector),要小心组合的层次。
  • 指针嵌入 vs 值嵌入:嵌入 *Logger 共享同一个实例,嵌入 Logger 是值拷贝;修改状态时用指针,只读用值。
  • 嵌入的滥用:嵌入应该表达「is-a-kind-of」的能力组合(如 bufio.Reader 内嵌 io.Reader),而不是随手把一堆字段塞进去图省事;嵌入公开类型会把它的导出方法全部暴露成自己的 API,形成「隐式公共接口」,需要慎重。

30. 接口小而聚焦、在使用处定义

对应 GoF:替代「抽象基类」,是 Strategy、Adapter 等模式的隐式实现载体

核心思想

Go 的接口满足是隐式的:类型只要实现了接口声明的方法,就自动满足该接口,不需要写 implements。基于这一点,社区有两个铁律:接口要小——尽量单方法(如 io.Readerhttp.Handler),因为大接口难实现、难替换;接口在使用处定义——由消费方声明「我只需要什么能力」,而不是由实现方声明「我提供了什么」。这让代码只在需要的边界上耦合,替换实现、写测试替身都极其容易。

设计逻辑
  • 小而聚焦:单方法接口最理想。type Handler interface { ServeHTTP(http.ResponseWriter, *http.Request) }io.Reader 只有一个 Read
  • 在使用处定义:消费方在自己的包里写 type UserStore interface { GetUser(id int) (string, error) },只声明它需要的最小方法集;实现方(可能在别的包)无需 import 消费方的接口,天然满足。
  • 接受接口,返回具体类型:函数参数声明成接口,函数返回值返回具体类型,避免过度抽象。
  • 可选能力:用类型断言 if f, ok := w.(Flusher); ok { f.Flush() } 探测实现方是否额外支持某个接口。
应用场景
  • 标准库遍地是单方法接口:io.Readerio.Writerhttp.Handlerfmt.Stringer
  • 存储层抽象:服务层定义自己需要的最小 UserStore,测试时用内存实现代替数据库。
  • 依赖注入与解耦:控制器不依赖具体 DB 驱动,只依赖接口。
  • 中间件/插件:http.Handler 这种单方法接口让任何函数都能 HandlerFunc(...) 转成处理器。
生产示例
package main

import (
	"fmt"
	"io"
	"strings"
)

// UserStore 在使用处(消费方)定义的最小接口:我只需要「按 id 查用户」
// 注意这里没有声明任何实现方,谁实现谁自动满足
type UserStore interface {
	GetUser(id int) (string, error)
}

// memoryStore 具体实现,无需声明「implements UserStore」,隐式满足
type memoryStore struct {
	users map[int]string
}

func (m *memoryStore) GetUser(id int) (string, error) {
	if name, ok := m.users[id]; ok {
		return name, nil
	}
	return "", fmt.Errorf("用户 %d 不存在", id)
}

// PrintUser 消费方只依赖最小接口 UserStore,替换实现/测试替身都很容易
func PrintUser(store UserStore, id int) {
	name, err := store.GetUser(id)
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println("用户:", name)
}

func main() {
	// 传具体类型即可,它隐式满足 UserStore 接口
	store := &memoryStore{users: map[int]string{1: "alice", 2: "bob"}}
	PrintUser(store, 1)
	PrintUser(store, 3)

	// 单方法接口示例:strings.Reader 隐式满足 io.Reader
	var r io.Reader = strings.NewReader("hello world")
	data, err := io.ReadAll(r)
	if err != nil {
		fmt.Println("读取失败:", err)
		return
	}
	fmt.Printf("从 io.Reader 读取到 %d 字节\n", len(data))
}
Go 实现要点 / 注意事项
  • 接口越大越难用:多方法接口(如 io.ReadWriteCloser)应尽量由小接口组合而成(第 29 节),而不是平铺一堆方法。实现者若只差一个方法就无法满足接口,这种耦合要尽量避免。
  • 不要「为将来」定义接口:单一实现时先返回具体类型,等出现第二个实现、真的有替换需求时再抽接口(YAGNI)。过早抽象是 Go 社区最常见的过度设计。
  • 接口值判空有个经典坑:一个持有 nil 指针的接口变量(var s UserStore = (*memoryStore)(nil)s == nilfalse,因为接口内部存了类型信息。判空要 reflect.ValueOf(s).IsNil() 或避免把 nil 指针装进接口。
  • interface{}any)要克制:它放弃一切静态类型保证,只在处理异构数据(如 json.Unmarshal)或泛型边界时使用。
  • 接受接口,返回具体类型是默认姿势:参数用接口表达「我需要什么」,返回值用具体类型表达「我给了你什么」,别反向操作。

31. 生成器迭代器 Generator via channel

对应 GoF:Iterator(迭代器)的 Go 变体,用 channel 或函数迭代器实现惰性序列

核心思想

生成器(Generator)指「按需逐个产生值」的惰性序列:调用方要一个值才算一个值,而不是一次性算完塞进切片。Go 里两种主流做法:channel 生成器——goroutine 把值逐个发进 channel,close 表示序列结束;Go 1.23 引入的 range over func——迭代器就是一个接收 yield 回调的函数,序列怎么产生完全由函数控制,且不依赖 goroutine、没有泄漏风险。

设计逻辑
  • channel 版本:函数返回 <-chan T,内部 go func() 逐个 out <- v,结束 close(out);调用方 for v := range ch 消费。缺点:无论调用方是否需要,goroutine 都在跑,提前 break 会造成 goroutine 泄漏(除非配合 context 取消)。
  • range over func 版本(Go 1.23+):迭代器是形如 func(yield func(T) bool) 的函数。for v := range it 时,运行时把 yield 传给迭代器,迭代器每产出一个值就调一次 yield(v)yield 返回 false 表示调用方 break 提前退出,迭代器立即 return,不会有 goroutine 泄漏。
  • 两者的共同点:都是惰性求值、都可用 range 消费;不同点在于 range over func 是同步的、无协程、可提前终止。
应用场景
  • 斐波那契、质数、随机数等「数学序列」的惰性生成。
  • 逐行读大文件、按页翻数据库游标——不需要把整份数据载入内存。
  • 无限序列配合 break 取前 N 个。
  • 任何「计算昂贵但通常只消费一小部分」的数据流。
生产示例
package main

import (
	"fmt"
)

// fib 基于 Go 1.23+ range over func 的迭代器:
// 迭代器函数接收 yield 回调,yield 返回 false 表示调用方提前退出
func fib(n int) func(yield func(int) bool) {
	return func(yield func(int) bool) {
		a, b := 0, 1
		for i := 0; i < n; i++ {
			if !yield(a) { // 消费方 break,立即停止,不会泄漏
				return
			}
			a, b = b, a+b
		}
	}
}

// fibChan 基于 channel 的生成器版本:goroutine 逐个发送,close 收尾
func fibChan(n int) <-chan int {
	out := make(chan int)
	go func() {
		defer close(out) // 序列结束必须关闭,否则消费方 range 会死锁
		a, b := 0, 1
		for i := 0; i < n; i++ {
			out <- a
			a, b = b, a+b
		}
	}()
	return out
}

func main() {
	// range over func 版本(Go 1.23+)
	fmt.Print("range over func 版本: ")
	for v := range fib(10) {
		fmt.Print(v, " ")
	}
	fmt.Println()

	// channel 版本,二者消费语法一致
	fmt.Print("channel 版本: ")
	for v := range fibChan(10) {
		fmt.Print(v, " ")
	}
	fmt.Println()

	// 提前退出:range over func 的 yield 返回 false,迭代器立即返回,无泄漏
	fmt.Print("提前退出取前 5 个: ")
	count := 0
	for v := range fib(10) {
		if count == 5 {
			break
		}
		fmt.Print(v, " ")
		count++
	}
	fmt.Println()
}
Go 实现要点 / 注意事项
  • 版本要求range over func 需要 Go 1.23+,且是实验性语法,之前用 golang.org/x/exp/iter 或自定义迭代器库过渡;现在主流的容器库(如 slicesmaps)已支持在 func(yield func(...) bool) 上直接 range
  • channel 生成器的泄漏风险:调用方提前 break 时,生产者 goroutine 可能永远阻塞在 out <- v。要么保证序列有限且一定会消费完,要么配合 context 取消(用 select 监听 ctx.Done())。
  • channel 生成器要无条件 close(out):即使中途出错也要 defer close(out),否则消费方死锁。
  • range over func 的 yield 返回值:迭代器必须检查 yield 的返回值,false 就停止;忽略它会浪费计算甚至出 bug。反过来,迭代器也不要在 yield 返回 false 后继续调用 yield。
  • 性能:channel 版本有 goroutine 调度开销,range over func 是纯函数调用,更轻量;数据量小、要频繁创建时优先 range over func。