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 总线)、模板方法(钩子字段)。 - 明确不建议:单例(反模式)、抽象工厂、原型、访问者、解释器。
三、学习建议
- 先过一遍「Go 简化 / 变体」小节,再回头读「模式介绍 / 设计逻辑」,从 Go 视角理解每个模式真正解决什么问题。
- 23 个模式的代码都独立可编译运行,建议逐个
go run一遍,观察输出。 - 重点投入第四部分(#24–31)的 Go 社区惯用模式——那是 Go 程序员真实天天写的东西,比照搬 GoF 模式更有生产价值。
- 记住一句话:优先用语言原生能力与更简单的结构,避免为模式而模式。
第一部分:创建型模式(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/sql的sql.Open返回的*sql.DB官方明确要求全局共享、不要反复开关,是「应被当作单例使用」的典型;http.DefaultClient、http.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.NewServer、http.Server的配置方式都是这种风格。函数式选项与工厂方法可以组合使用。 - 标准库对应物:
errors.New返回error接口、strings.NewReader、http.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.Image;database/sql/driver定义了一组驱动接口(Driver、Conn等),各数据库驱动实现它们,构成真实的「驱动产品族」。标准库并无内建抽象工厂,多数库在内部用「注册表 + 工厂函数」实现等价效果。
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 里构造复杂对象的两大惯用法是「建造者」和「函数式选项」:建造者在「有必填字段、步骤有顺序」时更清晰;函数式选项在「大量可选字段、需要向后兼容」时更优雅。二者甚至常被组合使用(如
grpc的DialContext+WithXxx系列)。 - 标准库对应物:标准库中链式建造者不多,
url.URL、http.Request主要靠字段赋值。真正广泛使用的是第三方库:GORM 的db.Table(...).Where(...).Find(...)、go-redis的NewClient配置、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 提供内建
Cloneable与clone(),C++ 有拷贝构造函数;Go 两者都没有,只能手写Clone()。因此原型是 Go 中「最不地道」的创建型模式,社区也没有统一约定。 - 社区真实看法:Go 社区普遍觉得原型「性价比不高」:深拷贝要么手写逐字段复制(易漏字段、难维护),要么用
reflect(慢且可读性差)。对大多数业务对象,「直接new+ 赋值」比维护Clone()更简单;只有确实需要「多态克隆」时才值得实现。 - 标准库对应物:标准库没有原型的直接实现,但
encoding/json、encoding/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.Writer、http.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.HandlerFunc把func(http.ResponseWriter, *http.Request)适配成http.Handler;io.Reader/io.Writer这类单方法接口让任意类型都能低成本接入标准库的io.Copy、bufio等工具。 - 标准库/知名项目对应物:
http.HandlerFunc、bytes.Buffer(适配io.Writer)、encoding/json的Marshaler/Unmarshaler、database/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.Writer、http.Handler都是极小的接口,组合自由。Go 的惯用做法是「依赖接口 + 构造时注入实现」,这在依赖注入(DI)场景中无处不在。 - 标准库对应物:
io.Reader/io.Writer把「数据源」与「消费方」解耦;net/http的RoundTripper接口使传输层可替换;database/sql与driver分离「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/fs的fs.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、压缩、超时,按任意顺序叠加。
- 缓存包装:为昂贵的数据读取/计算添加一层缓存。
- 观测埋点:为外部调用统计耗时、错误率。
- 流式处理:为底层读写加缓冲、加校验和、加加密(
bufio、gzip)。 - 通知增强:在基础通知上叠加优先级、重试、多渠道。
生产示例
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.StripPrefix、http.TimeoutHandler、http.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.ReadFile,net/http的http.Get隐藏了 TCP、TLS、重定向、连接池等复杂细节。 - 标准库对应物:
net/http(http.Get/http.Client)、database/sql(对外简单接口,内部连接池)、encoding/json(json.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.Pool、strings.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/rpc、google.golang.org/grpc生成的客户端 stub。 - 标准库对应物:
sync.OnceValue(惰性求值)、net/http/httputil.ReverseProxy(反向代理)、net/http的Transport(连接池 + 复用,充当底层代理)、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/http的ServeMux就是链尾。 - 当链的节点数量少、顺序固定时,直接用切片 + 循环遍历(
[]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类型别名,slices、maps包还提供现成的迭代器工具函数。 - 需要「手动控制遍历进度」时,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 字段,直接拷贝只复制引用,需用
copy、clone等方式深拷贝,否则快照会随原对象被修改而失效。 - 社区真实看法:备忘录在 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」隔离。
- 社区真实看法:观察者价值很高,
context、signal.Notify、sync.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.Slice的less 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)」两阶段,按文法逐层递归下降解析,配合 strconv、fmt 等标准库完成求值,代码清晰且可控。
应用场景
- 简单表达式求值器(算术、布尔表达式)
- 配置项 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/template、html/template、regexp、go/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.Server:ReadTimeout、WriteTimeout、MaxHeaderBytes等一堆可选字段。- gRPC:
grpc.DialOption/grpc.CallOption,官方就是函数式选项的教科书。 database/sql:SetMaxOpenConns、SetMaxIdleConns、SetConnMaxLifetime这套连接池配置。google.golang.org/grpc、client-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 channel或close 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)。jobs和results都设为与任务量等容量的有缓冲 channel,可避免投递时和收集时阻塞;若任务量未知,可用无缓冲 channel 配合投递/收集同时进行。 - 有缓冲 channel 的作用:缓冲让投递方不必等 worker 取走就能继续投,降低调度耦合;但缓冲越大,worker 崩溃时丢失的任务越多,需要权衡。
- 若任务处理本身可能出错,结果结构应设计为
struct{ data T; err error },或改用errgroup.Group(golang.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.Canceled或context.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取出做精细化处理。 - 需要区分「可重试」与「不可重试」、
404与500等错误语义时,用哨兵错误或自定义类型区分。 - 所有「可能失败」的函数——在 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.Reader、http.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.Reader、io.Writer、http.Handler、fmt.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 == nil是false,因为接口内部存了类型信息。判空要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或自定义迭代器库过渡;现在主流的容器库(如slices、maps)已支持在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。