『八股文™』詳解 Go 中的深拷貝與淺拷貝
大家好,我是 moooofly。
今天我們來聊一聊:Go 語言中關於深拷貝和淺拷貝的問題。
0x01 基礎概念
在 Go 語言中,針對如下結構體,如何對其進行淺拷貝和深拷貝?
代碼示例如下
上述問題的核心在於:
-
需要理解 Go 語言中的值類型和引用類型
-
需要知道 Go 語言中哪些類型可以直接通過賦值完成安全拷貝,哪些類型需要做深拷貝處理
-
需要知道如何手動處理嵌套結構中的指針類型,以及需要通過 copy 完成切片的複製
上述問題很常見,於是我們可以繼續追問如下:
-
淺拷貝時拷貝了些什麼
-
淺拷貝與深拷貝的核心區別是什麼?
-
哪些類型可以被安全的淺拷貝,哪些類型需要進行深拷貝
-
除了 copy 之外,是否還有其他辦法完成切片的深拷貝?
- 淺拷貝後的數據關係
在示例代碼中可以看出,“淺拷貝” 對應的就是一條簡單的賦值語句。
淺拷貝的本質是逐字段值複製,
-
針對值類型字段,如 Name,u2.Name 會複製 u1.Name 的值。
-
針對引用類型字段,如 Roles 和 Profile,複製的是引用,即指針和切片頭結構體等,但底層數據會被共享。
正是因爲存在 “底層數據被共享” 的問題,才產生了 “深拷貝” 的概念,因爲在很多場景中,共享底層數據會導致意想不到的事情發生。
- 深拷貝相關數據類型
除了切片,以下數據結構同樣需要顯式地進行深拷貝處理:
a. map
針對 map 的淺拷貝僅會複製其指針,導致底層哈希表數據會被共享,需要新建 map 並遍歷鍵值對重新插入完成深拷貝
b. 指針
針對指針的淺拷貝僅會複製對應的地址,深拷貝時複製的是其指向的實際內容。
c. Channel
針對 Channel 的淺拷貝會導致共享同一通道實例,因此針對 Channel 的深拷貝其實是以新建的方式完成的。
d. 函數(Func)
函數本身是引用類型,淺拷貝後會共享同一函數實例,需要通過重新定義函數實現的方式達成深拷貝效果。
e. 不可淺拷貝的 sync.Mutex 和 sync.RWMutex
**sync.Mutex **鎖狀態是與協程綁定的,淺拷貝後會導致不可預測的併發行爲,需在深拷貝時需重置鎖狀態或標記爲不可複製。
- 小結
以下是 Go 語言中數據類型在深拷貝 / 淺拷貝場景下的安全性總結表,涵蓋常見類型的複製行爲和底層原理:
-
結構體的條件安全:若結構體包含引用類型(切片、map 或 channel)、指針、sync 類型等不可安全複製的字段,則整個結構體不可安全複製。
-
接口的動態類型風險:若接口存儲的是值類型(如 int),則複製安全;若存儲指針或引用類型,則複製後操作可能影響原對象。
-
函數的 “僞安全”:函數本身是代碼段指針,複製後調用行爲一致,但若函數閉包捕獲外部變量,則可能共享狀態(需謹慎處理閉包)。
-
強制不可複製的標記:通過嵌入 sync.Mutex 或 noCopy 結構體,並結合 go vet 工具檢測,可以禁止複製行爲的發生。
最佳實踐:
-
優先使用值類型:如 int、數組、純值結構體。
-
針對引用類型的處理:對切片、map、指針等類型進行手動深拷貝。
-
針對複雜結構的處理:針對存在遞歸引用的數據結構,需要手動實現拷貝邏輯。
-
避免複製含鎖對象:若結構體中含有 sync.Mutex 或 sync.RWMutex ,則可以通過傳遞結構體指針的方式避免發生鎖的淺拷貝,也可以通過序列化的方式解決。
-
靜態代碼檢查:通過 go vet 和代碼審查確保無鎖複製(如 -copylocks 標誌)。
0x02 如何實現一個通用的深拷貝函數
假設需要實現一個通用深拷貝工具函數,支持任意結構體,應該如何做?
常見的方案有如下兩種:
-
利用 encoding/gob 或 encoding/json 序列化反序列化實現深拷貝
-
基於 reflect 實現深拷貝
-
基於 json 實現深拷貝
基於 json 或 gob 序列化實現通用深拷貝簡單直接,但需要針對循環引用和非導出字段進行特殊處理。
- 基於 reflect 實現深拷貝
基於白名單機制,僅處理可安全複製的類型。
反射 + 遞歸:通過 reflect 包動態處理嵌套結構,但需注意性能開銷。
上述兩種方案都存在一些侷限,在實際使用過程中,還需要做一些額外的設計調整。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/o_no8VBJeclwobMfKI6M3A