🎯 什麼情境該想到我

當你「已經有一個設定好的物件,想要一個一樣的,但重新建一次很貴或很麻煩」的時候。
★ 但在 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 後方法就不見了。

🔗 相關工具