跳至主要内容

23 种设计模式:实际应用场景(Go 伪代码)


一、创建型

关注如何创建对象:把“怎么 new”与“怎么用”解耦,让系统不依赖具体类型。

1. 单例 Singleton

思想:保证一个类只有一个实例,并提供全局访问点。逻辑:包内私有变量 + 受控获取入口 + 并发安全的一次性初始化。代价是隐含全局状态、不利测试,能用依赖注入时优先注入。

场景:全局配置;日志器;数据库连接池(sql.DB 本身即单例使用)。

var once sync.Once
var cfg *Config
func GetConfig() *Config { once.Do(func() { cfg = loadConfig() }); return cfg }

2. 工厂方法 Factory Method

思想:把“创建哪种对象”这个变化点封装起来,调用方只依赖接口,不直接 new 具体类型。逻辑:定义创建接口,由具体工厂决定产出,新增类型无需改调用方(开闭原则)。

场景:按渠道创建通知器(短信/邮件/推送);按文件类型创建解析器;日志输出目标创建。

type Notifier interface{ Send(msg string) }
type Creator interface{ Create() Notifier }
type SMSCreator struct{}
func (SMSCreator) Create() Notifier { return &SMS{} }

3. 抽象工厂 Abstract Factory

思想:一次创建“一整族”相关对象,保证它们互相兼容(不会把 S3 存储和阿里云队列混用)。逻辑:工厂接口含多个创建方法,切换工厂即整族切换。

场景:多云适配(AWS/阿里云/GCP 的存储+队列成套创建);多数据库方言;UI 主题套件。

type CloudFactory interface { NewStorage() Storage; NewQueue() Queue }
type AWSFactory struct{}
func (AWSFactory) NewStorage() Storage { return &S3{} }
func (AWSFactory) NewQueue() Queue     { return &SQS{} }

4. 建造者 Builder

思想:把复杂对象的构造过程拆成多步,避免超长参数列表和非法中间状态。逻辑:链式设置属性,最后 Build 统一校验。Go 中也常用 Functional Options 达到同样效果。

场景:SQL 查询构造(squirrel、GORM 链式);构造 HTTP 请求;复杂订单/报表对象。

q := NewQuery().Table("users").Where("age > ?", 18).Limit(10).Build()

5. 原型 Prototype

思想:通过复制现有对象来创建新对象,而不是从头构造。逻辑:对象自带 Clone 方法。关键是区分浅拷贝与深拷贝,切片、map、指针字段要单独复制。

场景:克隆配置模板;游戏怪物/道具模板;文档模板复制。

func (c *Config) Clone() *Config {
    n := *c
    n.Tags = append([]string(nil), c.Tags...) // 深拷贝切片
    return &n
}

二、结构型

关注如何组合类与对象:通过组合、包装,构成更大、更灵活的结构。

6. 适配器 Adapter

思想:转换接口,让原本不兼容的类能一起工作。逻辑:新建一层包装,对外实现调用方期望的接口,对内调用被适配者,不修改原有代码。

场景:旧支付接口适配新接口;第三方 SDK 统一成内部接口;http.HandlerFunc 把函数适配为 Handler。

type OldPay struct{}
func (OldPay) MakePayment(amt float64) {}
type PayAdapter struct{ old OldPay }
func (a PayAdapter) Pay(cents int) { a.old.MakePayment(float64(cents) / 100) }

7. 桥接 Bridge

思想:把抽象和实现分离,让两个维度各自独立变化。逻辑:用组合代替继承,将 M×N 个子类降为 M+N 个,避免类爆炸。

场景:通知类型 × 发送渠道两维度独立扩展;形状 × 渲染引擎;database/sql 抽象与驱动分离。

type Sender interface{ Send(string) }           // 实现维度
type Alert struct{ s Sender }                   // 抽象维度
func (a Alert) Notify(m string) { a.s.Send("[ALERT] " + m) }

8. 组合 Composite

思想:用统一接口对待“单个对象”和“对象集合”,形成树形结构。逻辑:叶子和容器实现同一接口,容器递归调用子节点,调用方无需区分。

场景:文件系统目录树;组织架构/权限菜单树;UI 组件树。

type Node interface{ Size() int }
type Dir struct{ children []Node }
func (d Dir) Size() (s int) { for _, c := range d.children { s += c.Size() }; return }

9. 装饰器 Decorator

思想:不修改原对象,动态叠加额外职责。逻辑:装饰器与被装饰对象实现同一接口,内部持有被装饰对象,像洋葱一样层层包裹,用组合代替继承来扩展功能。

场景:HTTP 中间件(日志、鉴权、限流);gRPC 拦截器;io.Reader 包装(gzip、bufio)。

func Logging(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        log.Println(r.URL); next.ServeHTTP(w, r)
    })
}

10. 外观 Facade

思想:为复杂子系统提供一个简化的统一入口,降低调用方与子系统的耦合(最少知识原则)。逻辑:入口负责编排子系统调用;不阻止高级用户直接使用子系统。

场景:下单一个入口封装库存、支付、物流;对复杂 SDK 提供简化 API;启动器统一初始化各模块。

type OrderFacade struct{ inv Inventory; pay Payment; ship Shipping }
func (f OrderFacade) Place(o Order) { f.inv.Lock(o); f.pay.Charge(o); f.ship.Create(o) }

11. 享元 Flyweight

思想:共享大量细粒度对象中不变的“内部状态”,可变的“外部状态”由调用方传入,以节省内存和创建开销。逻辑:用工厂/池管理并复用共享对象。

场景:sync.Pool 对象复用;字符串驻留;游戏中大量相同粒子/地图瓦片共享外形数据。

var tiles = map[string]*Tile{} // 内部状态共享
func GetTile(kind string) *Tile { if t, ok := tiles[kind]; ok { return t }; t := newTile(kind); tiles[kind] = t; return t }

12. 代理 Proxy

思想:用代理对象控制对真实对象的访问。逻辑:代理与真实对象同接口,在转发调用前后插入缓存、鉴权、限流、懒加载等逻辑。与装饰器的区别:代理侧重“控制访问”,装饰器侧重“增强功能”。

场景:缓存代理;权限/限流代理;RPC 客户端 stub;图片/数据懒加载。

type CacheProxy struct{ real Repo; cache map[int]User }
func (p *CacheProxy) Get(id int) User {
    if u, ok := p.cache[id]; ok { return u }
    u := p.real.Get(id); p.cache[id] = u; return u
}

三、行为型

关注对象之间如何协作与分配职责:让交互松耦合、算法可替换。

13. 责任链 Chain of Responsibility

思想:让请求沿着一条链传递,每个节点自行决定处理或交给下一个。逻辑:发送者与接收者解耦,链可以动态增删、调整顺序。

场景:审批流(组长→经理→总监);风控规则依次校验;日志级别过滤;中间件链。

type Handler interface{ Handle(req Req) bool; SetNext(Handler) }
// 每个节点:能处理就处理,否则 next.Handle(req)

14. 命令 Command

思想:把“请求”封装成对象,从而可以排队、记录日志、撤销。逻辑:将“做什么”(命令对象)与“谁来做、何时做”(调用者、接收者)解耦。

场景:异步任务队列;编辑器撤销/重做;操作日志回放;CLI 子命令(cobra)。

type Command interface{ Execute(); Undo() }
history := []Command{}
cmd.Execute(); history = append(history, cmd)
history[len(history)-1].Undo() // 撤销

15. 解释器 Interpreter

思想:为一种简单语言定义文法,并用对象树表示、递归求值。逻辑:每条文法规则对应一个表达式类型。只适合小型 DSL,复杂语法应使用解析器生成工具。

场景:规则引擎(age>18 AND vip==true);搜索/过滤语法;模板与表达式求值。

type Expr interface{ Eval(ctx map[string]any) bool }
type And struct{ L, R Expr }
func (a And) Eval(c map[string]any) bool { return a.L.Eval(c) && a.R.Eval(c) }

16. 迭代器 Iterator

思想:提供顺序访问集合元素的统一方式,而不暴露集合内部结构。逻辑:把“遍历”从集合中抽离,调用方只关心“下一个元素”。

场景:分页游标遍历数据库/对象存储(S3 ListObjects);bufio.Scanner;Go 1.23 iter.Seq。

func (p *Pager) Items() iter.Seq[Item] {
    return func(yield func(Item) bool) {
        for p.HasNext() { for _, it := range p.NextPage() { if !yield(it) { return } } }
    }
}

17. 中介者 Mediator

思想:用中介对象封装一组对象之间的交互,把网状的多对多关系变成星形。逻辑:同事对象只与中介通信,互不直接引用,交互规则集中管理。

场景:聊天室(用户经房间转发);事件总线;UI 控件联动;空管/调度中心。

type Room struct{ users []*User }
func (r *Room) Broadcast(from *User, msg string) {
    for _, u := range r.users { if u != from { u.Receive(msg) } }
}

18. 备忘录 Memento

思想:在不破坏封装的前提下,捕获对象的内部状态并保存在外部,以便日后恢复。逻辑:由原对象生成快照,由管理者保管,需要时交还给原对象。

场景:编辑器撤销;游戏存档;事务回滚前快照;表单草稿。

type Snapshot struct{ text string }
func (e *Editor) Save() Snapshot     { return Snapshot{e.text} }
func (e *Editor) Restore(s Snapshot) { e.text = s.text }

19. 观察者 Observer

思想:定义一对多的依赖关系:主题状态变化时,自动通知所有订阅者。逻辑:主题维护订阅列表并广播,双方只依赖抽象,实现松耦合的发布/订阅。

场景:事件订阅/发布;Kubernetes informer/watch;配置热更新;订单状态推送。

type Subject struct{ subs []chan Event }
func (s *Subject) Publish(e Event) { for _, ch := range s.subs { ch <- e } }

20. 状态 State

思想:把与状态相关的行为封装进各个状态对象,对象状态改变时切换状态对象。逻辑:用多态取代大量 if/switch,让状态转换规则清晰可扩展。

场景:订单状态机(待付款→已付款→已发货);TCP 连接状态;工作流审批。

type State interface{ Next(o *Order) }
type Paid struct{}
func (Paid) Next(o *Order) { o.state = Shipped{} }

21. 策略 Strategy

思想:定义一组可互换的算法,各自封装成独立单元,运行时按需选择。逻辑:把“怎么做”从“做什么”中抽离,调用方依赖策略接口(Go 中常用函数类型)。

场景:多种支付方式;促销折扣算法;负载均衡(轮询/随机/加权);限流算法;sort.Slice 传比较函数。

type Discount func(price float64) float64
func Checkout(p float64, d Discount) float64 { return d(p) }
Checkout(100, func(p float64) float64 { return p * 0.8 })

22. 模板方法 Template Method

思想:在一个函数中固定算法骨架,把其中某些步骤留给外部实现。逻辑:流程不变、步骤可变(好莱坞原则:别调用我,我会调用你)。Go 无继承,用接口或函数字段注入步骤。

场景:数据导入/导出固定流程(读取→校验→处理→写入);爬虫流程;测试 setup/teardown。

type Steps struct{ Read func() []byte; Validate func([]byte) bool; Save func([]byte) }
func Run(s Steps) { d := s.Read(); if s.Validate(d) { s.Save(d) } } // 固定骨架,步骤可替换

23. 访问者 Visitor

思想:把作用于数据结构的操作从结构中分离出来,新增操作无需修改元素类。逻辑:元素提供 Accept,访问者提供针对各类型的 Visit(双分派)。适合结构稳定、操作多变的场景。

场景:AST 遍历(go/ast.Inspect,代码检查/生成);报表导出多格式;对文件树做统计/压缩等不同操作。

type Visitor interface{ VisitFile(*File); VisitDir(*Dir) }
type Element interface{ Accept(Visitor) }
func (f *File) Accept(v Visitor) { v.VisitFile(f) }

速查:Go 中最常见的几种

  • 装饰器 / 责任链:HTTP、gRPC 中间件。
  • 策略:函数类型,最轻量。
  • 观察者:channel + goroutine。
  • 单例:sync.Once。
  • 迭代器:range 与 iter.Seq。

此博客中的热门博文

Elasticsearch 读写原理指南

### 1. 什么是 segment,里面装了什么? 在 Lucene(也是 Elasticsearch)里,索引被切分成若干 **segment(段)**,每个 segment 是一个完整的、只读的倒排索引单元。一个 segment 包含: * **倒排词典** —— 用 **FST(Finite‑State Transducer)** 以高度压缩的形式保存每个字段出现的所有 term 以及 term→ord 的映射。对应的磁盘文件是 `*.tim`(新版)或 `*.tis/*.tii`(旧版)。 * **倒排列表(postings)** —— 保存每个 term 出现的文档 ID、频次、位置信息等,文件名通常是 `*.doc`、`*.pos`、`*.pay`。 * **存储字段**(_source、store:true 的字段)—— 以二进制块的形式写入 `*.fdt` / `*.fdx`。 * **doc‑values、norms、向量** 等辅助结构,分别保存在 `*.dv`、`*.norm`、`*.tv` 等文件里。 * **deleted‑docs bitmap**(`*.del`),标记哪些文档已被删除或被更新。 所有这些文件在 segment **写入磁盘后即成为只读**,后续的查询只能读取,永远不会在原文件上进行增删改。 --- ### 2. 原始文档和 FST 为什么都在 segment 里? * **原始文档**:Elasticsearch 默认把完整的 JSON(_source)以及任何 `store:true` 的字段写入 segment 的 `*.fdt/*.fdx` 文件。每个 segment 保存自己的那部分文档,旧的 segment 在合并前仍然保留,直到合并后被删除。 * **FST**:每个字段的词典在每个 segment 中单独维护,采用 FST 进行前缀共享和字节压缩。这样即使同一个 term 在多个 segment 中出现,也会在每个 segment 里拥有独立的映射,查询时只需要在对应 segment 的 FST 中定位即可。 --- ### 3. 查询时到底是怎么遍历 segment 的? 1. **请求入口**      客户端的搜索请求先到达 **协调节点**,协调节点把请求 ...

PyTorch学习路线图

  第一阶段:基础知识(2-3周) 第1周:Python与机器学习基础 Python数据结构与NumPy基础 张量概念与线性代数基础 机器学习基本概念 第2-3周:PyTorch核心 张量操作与计算图 自动微分(autograd)机制 数据加载与预处理(DataLoader, Dataset) 构建神经网络模块(nn.Module) 第二阶段:深度学习基础(4-6周) 第4周:线性模型与优化器 线性回归与逻辑回归实现 优化器(SGD, Adam等) 损失函数与评估指标 第5-6周:基础神经网络 多层感知机(MLP) 卷积神经网络(CNN) 循环神经网络(RNN, LSTM, GRU) 第7-8周:训练技巧 模型保存与加载 学习率调度 正则化技术 迁移学习 第三阶段:进阶应用(6-8周) 第9-10周:计算机视觉 图像分类 目标检测 图像分割 视觉Transformer 第11-12周:自然语言处理 词嵌入 序列到序列模型 Transformer架构 预训练语言模型应用 第13-14周:生成模型 自编码器 变分自编码器(VAE) 生成对抗网络(GAN) 扩散模型 第四阶段:工程实践(4-6周) 第15-16周:模型部署 模型量化与优化 TorchScript与ONNX导出 服务化部署 移动端部署 第17-18周:高级训练技术 分布式训练 混合精度训练 梯度累积与梯度裁剪 模型剪枝与蒸馏 第19-20周:项目实战 完整项目流程 模型性能优化 工业级代码实践

LLM缓存详解

 可以把“大模型缓存”理解成: 把已经算过的结果(或中间结果)存下来,下次尽量复用 。但这里面其实分几层,不只是简单的“问题→答案”缓存。 1️⃣ 常见的几种缓存类型 (1)KV Cache(推理内部缓存) Transformer 在生成时,会把前面 token 的 Key/Value 向量 缓存下来。 本质:避免重复计算 attention 作用: 同一请求内部加速 特点: 👉 只对“同一上下文继续生成”有效 👉 不跨用户、不跨请求 这类缓存是你体感“流式输出越来越快”的原因之一。 (2)Prompt Cache(提示词缓存) 缓存的是: 相同(或高度相似)的 prompt → 对应的中间表示 / 输出 典型场景: 系统提示词(system prompt)很长 多轮对话里前文基本不变 👉 这里能省掉 前缀计算成本(prefill) (3)Embedding / 语义缓存(Semantic Cache) 这个才是你问题的关键 👇 不是按“字符串完全一致”,而是: 把问题转成向量 → 找“语义相似”的历史问题 → 直接复用答案 2️⃣ 为什么命中缓存成本低很多? 因为大模型推理成本主要在两块: (1)Prefill(吃 prompt) 复杂度 ~ O(n²) 很贵(尤其长 prompt) (2)Decode(逐 token 生成) 每个 token 都要算一遍模型 而缓存命中后: KV cache:不用重复 attention Prompt cache:不用重新 encode 语义缓存: 直接跳过模型推理 👉 相当于从: 几十~几百毫秒 + GPU算力 变成: 一次向量检索(毫秒级)+ 直接返回 所以成本差一个数量级是正常的。 3️⃣ “每个人问法不同,怎么命中缓存?” 这是核心难点,也是工程重点👇 ❌ 不能靠字符串匹配 比如: “今天天气怎么样” “今天外面热不热” 字符串完全不同 → 必须 miss ✅ 用语义相似度(Embedding) 流程一般是: 把问题转 embedding(向量) 在向量数据库里找 TopK 相似问题 如果相似度 > 阈值(比如 0.9) 直接返回缓存答案 一个简单示意 Q1: 北京天气怎么样 → embedding A Q2: 北京今天热吗 → embedding B cosine(A, B) ≈ 0.95...