# 05 原型（创建型）

> 从头造一个对象太贵？那就照着现成的拷一个。讲清原型模式的“以复制代替新建”思路、Go 结构体值拷贝的天然优势，以及深拷贝 vs 浅拷贝这个能坑死人的头号陷阱。

- 作者：David（道雾轩）
- 专栏：经典设计模式浅析（https://daiw.org/manual/design-patterns.md）
- 最后更新：2026-10-01
- 原文：https://daiw.org/manual/design-patterns/prototype
- 转载与引用：请注明出处并附原文链接（https://daiw.org/about/copyright）

# 05 原型 · Prototype（创建型）

创建型的最后一位。前四个模式都在琢磨“怎么把一个对象**造**出来”，原型模式偏不：**别造了，照着现成的拷一个不就完了？**

## 一、这是什么：复印一份再改，别重写

你要写一份合同。是打开空白文档从头敲，还是找一份**去年的旧合同，复制一份，改几个关键字段**？显然后者快得多——绝大部分条款是现成的，你只需在副本上动几处。

原型模式就是这个思路：**系统里已经有一个配置好的对象(原型)，需要新对象时，不从零构造，而是克隆这个原型，再在副本上做少量修改。**

配套代码克隆图形：有一个配置好的 `Circle`，要新圆时直接 `Clone()` 出一份，改改位置就用，不用重新设置颜色、半径等一堆属性。

## 二、解决什么痛：新建太贵，或者你不想认识它的类

原型模式主要止两种痛：

1. **“造一个”的成本很高。** 有些对象初始化要查数据库、跑复杂计算、读网络、解析大文件。如果你手头已经有一个初始化好的同类对象，**拷贝内存**几乎总比**重新初始化**便宜得多。游戏里刷一片一模一样的小怪，与其每只都重新加载模型和属性，不如以一只为模板疯狂复制。
2. **你想要副本，却不想依赖它的具体类。** 你手上是个抽象的 `Shape`，想复制它。但它到底是 `Circle` 还是 `Rectangle`？你不知道，也不想用一堆 `if type == Circle` 去判断再分别 new。**让对象自己提供 `Clone()`，复制这件事就交给它自己——它最清楚怎么拷贝自己。**

<Callout type="info">
  **原型隔离的“会变的东西”，是“具体要复制哪个类”。** 客户端只对着 `Clone()` 喊一声，至于复制出的是圆是方，由对象自身负责。这一点和工厂方法异曲同工，只是手段从“new”换成了“copy”。
</Callout>

## 三、怎么实现：Go 结构体天生会拷贝

这是 Go 占尽便宜的一个模式。因为 **Go 的结构体赋值(`cp := *c`)本身就是逐字段的值拷贝**，对只含值类型字段的结构体来说，一行就完成了克隆：

```go title="go/prototype/main.go（核心）"
type Shape interface {
	Clone() Shape
	Info() string
	SetPosition(x, y int)
}

type Circle struct {
	Color  string
	X, Y   int
	Radius int
}

// Clone 返回自身的一份拷贝。Go 中 *c 解引用再赋值,
// 就是逐字段值拷贝——对全值类型字段的结构体,这即是深拷贝。
func (c *Circle) Clone() Shape {
	cp := *c          // ← 一行搞定
	return &cp
}
```

用起来，改副本完全不影响原型：

```go
original := &Circle{Color: "红色", X: 10, Y: 20, Radius: 5}
cloned := original.Clone()
cloned.SetPosition(100, 200)   // 只动副本
// original 依旧是 (10,20),纹丝不动
```

Java 里就没这么轻松了，得实现 `Cloneable` 接口、覆写 `clone()`、处理受检异常——而且默认还是浅拷贝，坑一堆(见下)。

## 四、有哪些坑：深拷贝 vs 浅拷贝，头号杀手

这是原型模式**最危险、最必须讲清**的一节。上面 `cp := *c` 之所以是完整拷贝，**全靠 `Circle` 的字段都是值类型**(string、int)。一旦结构体里含有**指针、slice、map、channel** 这类引用类型，`cp := *c` 就只拷贝了“引用”本身——副本和原型会**共享**底层数据：

```go title="浅拷贝陷阱 —— 副本和原型偷偷共享了同一个 slice"
type Config struct {
	Name  string
	Tags  []string        // ← 引用类型!
}

func (c *Config) Clone() *Config {
	cp := *c               // 浅拷贝:cp.Tags 和 c.Tags 指向同一个底层数组
	return &cp
}

orig := &Config{Name: "prod", Tags: []string{"a", "b"}}
clone := orig.Clone()
clone.Tags[0] = "HACKED"   // 想改副本...
// 结果 orig.Tags[0] 也变成了 "HACKED"!两个对象共享同一个数组。
```

正确的深拷贝必须手动把引用类型字段也复制一份：

```go title="深拷贝 —— 引用类型字段要逐个"再拷一层""
func (c *Config) Clone() *Config {
	cp := *c
	cp.Tags = append([]string(nil), c.Tags...)   // 复制一份新的底层数组
	return &cp
}
```

<Callout type="warn">
  **这是原型模式踩坑率数一数二的地方。** 记死一句话：**`cp := *c` 只拷贝“第一层”。** 结构体里凡是指针、slice、map，浅拷贝后都和原型共享同一份底层数据，改一个动全家。深拷贝要不要做、做到几层，取决于你的对象结构——嵌套引用越深，越要小心，必要时逐层手写或借助序列化(如 `json.Marshal` 再 `Unmarshal`，虽慢但省心)。**循环引用**更是深拷贝的经典噩梦，遇到要专门处理。
</Callout>

另外两个小坑：

- **Java 的 `Cloneable` 是反面教材。** 《Effective Java》专门开一条批判它：`clone()` 机制设计得别扭(受检异常、`Object.clone()` 的浅拷贝默认、和构造函数不一致)。Java 社区更推荐用**拷贝构造函数**或**拷贝工厂**代替 `Cloneable`。这说明：即便是 GoF 钦定的模式，具体语言的实现也可能是个坑，别盲从。
- **别为了用而用。** 如果你的对象构造成本很低，直接 new 一个新的、干干净净，比“克隆再修改”更清晰，也没有共享数据的风险。原型的价值，建立在“新建确实贵”或“确实要摆脱具体类”之上。

## 五、开源里怎么用

- **Go 的 `Clone()` / `Copy()` 家族**：`proto.Clone()`(Protocol Buffers 深拷贝一个 message)、很多库给复杂配置对象提供的 `DeepCopy()`。**Kubernetes** 尤其典型——它的 API 对象(Pod、Deployment 等)几乎每个都自动生成了 `DeepCopy()` 方法(`zz_generated.deepcopy.go`)，因为 K8s 内部大量传递对象副本以避免并发修改共享状态，**这是原型模式在超大型项目里的工业级应用**。
- **Go 的值语义**：`time.Time`、`net/url.URL` 等被设计成值类型，赋值即安全拷贝——语言层面把“原型”变成了默认行为。
- **JavaScript `structuredClone()`**：浏览器/Node 内置的深拷贝函数，一行完成复杂对象(含嵌套、循环引用)的深克隆，是原型思路的现代标配。
- **各类编辑器/设计工具的“复制粘贴”**：复制一个图层、一个组件、一个节点，底层几乎都是“克隆原型对象再改属性”，和配套代码的图形克隆一模一样。

---

**创建型五连，收工。** 回头看这一组的主线：它们全在管一件事——**别让“创建对象”这个动作，把你的业务代码绑死在具体类型上。** 单例管数量，工厂管“造哪个”，抽象工厂管“造哪族”，建造者管“怎么拼”，原型管“照着拷”。

下一章进入[结构型](https://daiw.org/manual/design-patterns/adapter)：对象已经造好了，该研究怎么把它们**拼装**成更大的结构，还不别扭。第一个出场的是最实用的老黄牛——适配器。
