clone是深拷贝还是浅拷贝_go-clone:深拷贝 Go 数据结构
繼續(xù)閑來寫寫碼,這次來介紹一下半年多以前寫的一個庫,最近工作中發(fā)現(xiàn)真的有用,還是值得推薦一下的。
背景
這個庫是 github.com/huandu/go-clone,主要用途是對任意的 Go 結構進行深拷貝,創(chuàng)造一個內(nèi)容完全相同的副本,得到的值可通過 reflect.DeepEqual 檢查。
這個功能看起來挺常用的,不過很奇怪在 Go 世界里面可用的實現(xiàn)卻很少,在動手實現(xiàn)之前我調(diào)查了幾個類似的庫或者可用來做深拷貝:
- encoding/gob 或 encoding/json:先將數(shù)據(jù)結構進行編碼(gob.Encoder 或 json.Marshal),得到 []byte之后再解碼(gob.Decoder 或 json.Unmarshal)。這種做法的好處是簡單粗暴,基本上能夠應對大部分的需求,缺點則是性能極低,且有各種限制,比如無法處理遞歸指針、無法處理 interface 類型數(shù)據(jù),特別是 JSON,會丟失缺少大部分數(shù)據(jù)類型甚至精度。
- github.com/jinzhu/copier 或 github.com/ulule/deepcopier:這兩個庫都實現(xiàn)了基本的 struct 拷貝能力,不過缺乏遞歸指針的處理,也不能作為通用的深拷貝來使用。
如果大家有看到其他類似功能的庫,歡迎留言,我會去研究學習一下。當前所看到的幾個庫都不太能滿足需求。
實現(xiàn)思路
要實現(xiàn)深拷貝函數(shù) Clone(v interface{}) interface{},其基本思路很簡單:
- 首先通過函數(shù) val := reflect.ValueOf(v) 拿到 v 的反射值;
- 根據(jù) val.Kind() 區(qū)分各種類型,主要分兩種:一種是 scala 類型,即數(shù)值類型,包括各種整型、浮點、虛數(shù)、字符串等,直接返回原值即可;一種是復雜類型,每種類型用對應的反射方法來創(chuàng)建,包括 reflect.New/reflect.MakeMap/reflect.MakeSlice / reflect.MakeChan 等方法;
- 通過各種反射方法來將新申請 val.Set* 方法將新值設置到新申請的變量里面。
這里面比較麻煩的是處理 struct,為了深拷貝結構,必須首先通過 val.NumField() 得到 struct field 個數(shù),然后用循環(huán)不斷的將 val.Field(i) 的值拷貝到新申請的變量對應字段里面去,這里遞歸調(diào)用深拷貝方法即可。
思路看起來很簡單,似乎都是些體力活,但做了之后就會發(fā)現(xiàn)有一些特殊情況還得多加小心,真要實現(xiàn)好不容易。
處理遞歸數(shù)據(jù)
當我們使用循環(huán)鏈表的時候就會遇到遞歸數(shù)據(jù)。一個首尾相連的鏈表,如果一直跟著指針深拷貝所有數(shù)據(jù),那么深拷貝函數(shù)一定會陷入死循環(huán)而無法退出。
下面是一個例子。
type ListNode struct {Data intNext *ListNode } node1 := &ListNode{Data: 1, } node2 := &ListNode{Data: 2, } node3 := &ListNode{Data: 3, } node1.Next = node2 node2.Next = node3 node3.Next = node1其中 node1 -> node2 -> node3 -> node1 -> ... 形成了一個循環(huán)鏈表。
如果直接按照一般思路來實現(xiàn) Clone,這個函數(shù)會因為不斷的深度遍歷 Next *ListNode 而陷入死循環(huán),永遠無法返回。
為了解決這個問題,應該使用經(jīng)典的有向圖檢查環(huán)路的方法來實現(xiàn),需要記下那些會產(chǎn)生循環(huán)的類型的訪問記錄,下次再訪問到同樣的數(shù)據(jù)時直接返回之前記錄的結果即可打破循環(huán)。
可能會循環(huán)的類型其實不多,只有map、slice 和指針總共三種。 這個事實可能會有點違反直覺:struct 和 interface 都不會造成循環(huán)?這還真不會。
我們無法僅通過 struct 嵌套來構造出一個循環(huán)結構,這是無法通過編譯器檢查的。我們也無法通過 struct + interface 構造出循環(huán)結構,考慮以下代碼:
type T struct {Loop interface{} }// 無法不使用 map、slice 和指針構造出循環(huán)結構。 t := T{} t.Loop = t t.Loop = tfmt.Println(t == t.Loop.(T)) // false// t 實際內(nèi)容是: // t == T{ // Loop: T{ // Loop: T{}, // }, // }可以看到,在 Go 里面并不能把 interface 當做一種萬能指針,當我們將一個結構 T 賦值給 interface{} 時候,Go 內(nèi)部會將 T 的內(nèi)容拷貝一份再賦值,而不是「引用」T 的原值。
在實現(xiàn)環(huán)路檢查時還會遇到一個問題:雖然檢查循環(huán)的關鍵是發(fā)現(xiàn)訪問了一個訪問過的值,但問題是怎么才知道兩個值相等呢?我們總不能用 reflect.DeepEqual 來檢查吧,那就太浪費性能了。我們也不能使用 map[interface{}]struct{} 來判斷,雖然 Go 允許用 interface{} 作為 map 的 KeyType,但 Go 編譯器和運行時都不允許將 map 類型作為 KeyType,而可循環(huán)類型包含 map,所以還得找其他辦法
m := map[interface{}]bool{} key := map[int]string{} m[key] = true // 可以被編譯,但是運行時會 panic。容易想到,同樣的問題 reflect.DeepEqual 也會遇到,那么直接去看一下官方實現(xiàn)就能得到答案。
// During deepValueEqual, must keep track of checks that are // in progress. The comparison algorithm assumes that all // checks in progress are true when it reencounters them. // Visited comparisons are stored in a map indexed by visit. type visit struct {a1 unsafe.Pointera2 unsafe.Pointertyp Type }func deepValueEqual(v1, v2 Value, visited map[visit]bool, depth int) bool {// 略…… }繼續(xù)看代碼可以發(fā)現(xiàn),官方使用的是 reflect.Value 的 Pointer 方法來得到 unsafe.Pointer,恰好 map、slice 和指針都可以調(diào)用這個方法,因此我們可以用類似手法實現(xiàn)。需要注意的是,reflect.DeepEqual 需要判斷 v1 / v2 是否不同,所以在 visit 里面同時記錄了兩個指針,但我們在 Clone 的時候只需要知道變量是否已經(jīng)遍歷過,所以只需要一個指針就可以。
同時,我們也不需要使用 unsafe.Pointer 來記錄指針,直接使用 uintptr 即可。這是因為在 Clone 結束前,新申請的變量一定能被當前 goroutine 的 stack 訪問到,不會被 GC。當前 Go 的 GC 也不支持內(nèi)存移動,可預見的將來也不會支持這種能力,所以無需多此一舉用 unsafe.Pointer 平添 GC 壓力。
還有一個細節(jié)必須注意:通過反射拿到的 slice 內(nèi)部指針只是 slice 第一個元素的地址,不足以區(qū)分不同長度的 slice,這會造成誤判。 reflect.DeepEqual 更注重的是數(shù)據(jù)「相等」,而不是精確「相同」,不做區(qū)分也沒事,但 Clone 時候則必須區(qū)分同一個數(shù)組的不同 slice 的問題。
slice := []int{1, 2, 3, 4, 5} s1 := slice[:2] s2 := slice[:5]p1 := reflect.ValueOf(s1).Pointer() p2 := reflect.ValueOf(s2).Pointer()fmt.Println(p1 == p2) // true綜上,最后采用如下結構來記錄訪問過的值。
type visit struct {p uintptrextra intt reflect.Type }type visitMap map[visit]reflect.Value這個 extra 里面存儲的是 slice 的長度。
在記錄時也需要注意,必須先將新數(shù)據(jù)放入 visitMap 然后再深度遍歷和填充數(shù)值才行,否則依然會死循環(huán)。以指針為例,與 visitMap 相關的實現(xiàn)代碼如下:
func clonePtr(v reflect.Value, visited visitMap) reflect.Value {t := v.Type()if visited != nil {visit := visit{p: v.Pointer(),t: t,}if val, ok := visited[visit]; ok {return val}}elemType := t.Elem()nv := reflect.New(elemType)if visited != nil {visit := visit{p: v.Pointer(),t: t,}visited[visit] = nv}// 省略填充 nv 內(nèi)容的過程……return nv }最后,由于記錄 visitMap 會有額外內(nèi)存和性能損耗,而絕大多數(shù)數(shù)據(jù)結構并不會包含任何循環(huán)結構,所以 clone.Clone 默認不做任何循環(huán)檢查,專門提供的 clone.Slowly 則負責解決這種復雜問題。
小結
以上內(nèi)容已經(jīng)可以幫助我們了解如何實現(xiàn)一個簡單可用的深拷貝的工具庫了, 這篇文章就暫時到此為止。
其實 github.com/huandu/go-clone 已經(jīng)實現(xiàn)的功能遠不止如此,還有不少硬核內(nèi)容值得在未來繼續(xù)分享。例如,如何深拷貝 struct 里面未導出的私有數(shù)據(jù)、如何極致優(yōu)化深拷貝性能、如何實現(xiàn) immutable struct 數(shù)據(jù)等。這些內(nèi)容以后再分享吧。
總結
以上是生活随笔為你收集整理的clone是深拷贝还是浅拷贝_go-clone:深拷贝 Go 数据结构的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 程序自动启动_如何在Gnome Shel
- 下一篇: jsp 使用base标签 没有作用_JS