🎯 什麼情境該想到我
當你「已經有一個設定好的物件,想要一個一樣的,但重新建一次很貴或很麻煩」的時候。
★ 但在 Go 與 TS 裡,這個模式本身很簡單——真正的陷阱全部在「深拷貝 vs 淺拷貝」上,所以這頁的重點在那裡。
⚙️ 怎麼用
意圖
用原型實例指定要建立的物件種類,並透過複製這個原型來建立新物件。
GoF 的原始動機是:在 C++/Java 裡,「依執行期的型別建立同型物件」很麻煩(沒有頭等的型別值)。
有了 clone(),你不需要知道具體型別就能複製它。
★ Go 的長相:沒有 clone 慣例,自己寫 Copy
type Template struct {
Name string
Tags []string // ★ 參考型別
Meta map[string]string // ★ 參考型別
Nested *Config // ★ 指標
}
// ❌ 淺拷貝:Go 的賦值會複製 struct,但 slice/map/指標只複製「表頭/位址」
func (t Template) ShallowCopy() Template { return t } // ★ 危險
// ✅ 深拷貝
func (t Template) Clone() Template {
c := t // 先複製值型別的欄位
c.Tags = slices.Clone(t.Tags) // Go 1.21+
c.Meta = maps.Clone(t.Meta) // Go 1.21+
if t.Nested != nil {
n := *t.Nested // ★ 若 Config 自己也含參考型別,這裡還要再往下
c.Nested = &n
}
return c
}★ Go 的淺拷貝陷阱(實務上最容易中的一個)
a := Template{Tags: []string{"x", "y"}}
b := a // 看起來是複製
b.Tags[0] = "CHANGED" // ★ a.Tags[0] 也變成 "CHANGED"!
// 原因:slice 的賦值只複製「指標 + len + cap」這個表頭,
// 底層陣列是共用的(見 [[工具-Go切片的共享與陷阱]])哪些欄位需要特別處理:
| 型別 | 賦值時 | 要不要手動深拷貝 |
|---|---|---|
int/string/bool/struct(純值) | 真正複製 | ❌ 不用 |
[]T slice | 只複製表頭 | ✅ slices.Clone |
map[K]V | 只複製指標 | ✅ maps.Clone |
*T 指標 | 只複製位址 | ✅ 手動 |
chan、func | 只複製參考 | ⚠️ 通常不該複製 |
sync.Mutex 等 | 會被複製 | ★ 絕對不能複製(go vet 會警告 copylocks) |
★
slices.Clone與maps.Clone仍然是「一層」深拷貝。
[][]int或map[string][]string要再往下複製一層——slices.Clone不會遞迴。
要完全通用的深拷貝:序列化再反序列化(json.Marshal → Unmarshal)——
慢、會丟掉未匯出欄位與型別資訊,但簡單可靠。效能敏感的地方請手寫。
TypeScript 的長相
const a = { tags: ["x"], meta: { k: "v" } };
const shallow = { ...a }; // ★ 淺:a.tags === shallow.tags
const deep = structuredClone(a); // ★ Node 17+ / 現代瀏覽器內建,真正的深拷貝
structuredClone處理循環參照、Map/Set/Date/TypedArray,
但不會複製函式、DOM 節點、原型鏈(class 實例會變成普通物件)。
JSON.parse(JSON.stringify(x))會丟掉undefined、Date變字串、無法處理循環——盡量別用。
實務用途
| 用途 | 說明 |
|---|---|
| 設定模板 | 一份 base config,每個租戶 clone 後微調 |
| ★ 測試夾具 | 一份標準測試資料,每個測試 clone 一份,避免測試互相污染 |
| 備忘錄模式/undo | 快照就是 clone |
| 避免昂貴的初始化 | 物件建立要讀檔/連網/大量計算時 |
| 物件池 | 從原型複製出新實例 |
🧪 我實際套用的紀錄
- (待填)
⚠️ 注意 / 什麼時候不適用
- ★ 深淺拷貝是這個模式唯一真正的難題。「clone 完兩個物件還是互相影響」是最常見的 bug,
而且它會在很久之後才以奇怪的形式爆出來。 - ★ 深拷貝不一定是你要的。共用一個不可變的大物件(如載入好的字典、設定)是正確且省記憶體的。
「要複製到哪一層」是一個需要明確決定的設計問題,不是「越深越好」。 - 含
sync.Mutex/sync.WaitGroup的 struct 不可複製。go vet的 copylocks 檢查會抓,不要忽略那個警告。 - ★ 最好的解法常常是「不要可變」。用不可變的值型別 + 「修改時產生新值」,深拷貝的問題根本不存在(函數式的思路)。
- 反射式的通用深拷貝套件(
copier、deepcopy)在效能敏感的路徑上很慢,而且會靜默略過未匯出欄位。 - Go 沒有
Cloneable介面的慣例。要多型複製就自己定義interface{ Clone() T }(泛型後更好寫)。 - TS 的
structuredClone不保留原型:class 實例 clone 後方法就不見了。