在golang中深度复制数据结构

17

我想复制一个数据结构实例。由于Go语言没有内置函数,我正在使用第三方库:https://github.com/emirpasic/gods

例如,我可能会尝试使用哈希集合进行深拷贝。

var c, d hashset.Set
c = *hashset.New()
c.Add(1)
deepcopy.Copy(d, c)
c.Add(2)
fmt.Println(c.Contains(2))
fmt.Println(d.Contains(2))
fmt.Println(c.Contains(1))
fmt.Println(d.Contains(1))
然而,哈希集合的内容根本没有被复制。我知道深复制模块无法复制未导出的值,但由于库中没有内置的“复制构造函数”,这是否意味着在不修改其代码的情况下无法使用该库完全复制数据结构实例?(我研究过的一些其他库也存在类似的问题。)
我是 Go 语言的新手,感觉不太对劲,因为类似的事情在 C++ 中可以很容易地实现。我知道我可以编写自己的版本或修改它们的代码,但这比预期的工作量还要大,这就是我认为应该有一个惯用的方法的原因。
注:对于那些可能会说“没有这样的功能”的人,我正在将一些复杂的状态与一些数据结构分发到并行计算线程中,他们直接使用状态,并且不能相互干扰。

使用reflect包,您应该能够使用unsafe实现自己的深度复制,包括未导出的字段,如果我没有弄错的话。 - mkopriva
@mkopriva:但这听起来像是无中生有的大量工作 :) 事实上,拥有深度复制所有字段的能力确实非常有用。 - Sergio Tulentsev
1
@mkopriva,您可以使用反射读取未导出的字段,但无法设置它们。请参见如何克隆具有未导出字段的结构? - icza
1
@icza https://dev59.com/HFgQ5IYBdhLWcg3wfULp#43918797(我在我的初始评论中提到了 unsafe - mkopriva
1
@mkopriva 哦,抱歉,我没有注意到你第一条评论中的“unsafe”这个词。 - icza
显示剩余2条评论
5个回答

15

不管是否令人遗憾,Go中没有这样的方法。首先想到的工具是反射(包reflect),但是使用反射只能读取未公开的字段,而不能设置它们。请参见如何克隆具有未公开字段的结构?

唯一可以克隆带有未公开字段的结构的方法是使用unsafe包(在此处查看示例:Access unexported fields in golang/reflect? ),但是正如其名称所示:它是不安全的,应尽可能避免使用。使用unsafe创建的程序无法保证它们能够继续与新版本的Go一起工作,或者在每个平台上都表现相同。

通常情况下,支持在Go中进行克隆的唯一适当方法是如果该软件包本身支持此类操作。

注1:

这并不意味着在某些特定情况下,您无法通过创建一个新值并手动构建其状态来“模仿”克隆。例如,您可以通过创建一个新的映射,遍历原始键值对,并将它们设置在新映射中来克隆一个map

注2:

请注意,您可以通过将其分配给另一个结构体变量(相同类型)来创建有未公开字段的结构体的“精确”副本,这也会正确复制未公开的字段。

就像在这个例子中:

type person struct {
    Name string
    age  *int
}

age := 22
p := &person{"Bob", &age}
fmt.Println(p)

p2 := new(person)
*p2 = *p
fmt.Println(p2)

这将输出以下内容(在Go Playground上尝试):

&{Bob 0x414020}
&{Bob 0x414020}

我们甚至可以使用reflect进行泛化,而不必依赖具体类型:

type person struct {
    Name string
    age  *int
}

age := 22
p := &person{"Bob", &age}
fmt.Println(p)

v := reflect.ValueOf(p).Elem()
vp2 := reflect.New(v.Type())
vp2.Elem().Set(v)
fmt.Println(vp2)

Go Playground上尝试这个。

但是我们不能够改变person.age未导出字段指向其他内容。除非得到声明包的帮助,它只能是nil或与原始字段相同的指针值(指向同一对象)。

另请参见相关问题:Quicker way to deepcopy objects in golang


8
如果你的结构体可序列化,你就可以将其转换为JSON格式,然后再转回来。这对于我的使用情况已经足够好了。
func CloneMyStruct(orig *model.MyStruct) (*model.MyStruct, error) {
    origJSON, err := json.Marshal(orig)
    if err != nil {
        return nil, err
    }

    clone := model.MyStruct{}
    if err = json.Unmarshal(origJSON, &clone); err != nil {
        return nil, err
    }

    return &clone, nil
}

4

如果您需要深度复制一个 protobuf 结构体,您可以在 https://github.com/golang/protobuf 中使用proto.Clone 函数, 请查看godoc文档 获取更多帮助信息。


0

如果你使用的包没有为数据结构提供复制函数,那么我会编写自己的类型安全复制函数。此外,我不会担心未导出的字段,因为开发人员决定将这些字段隐藏起来与用户无关。

就你的例子而言:

func NewHashSetCopy(src *hashset.Set) *hashset.Set{
    dst := *hashset.New()
    iterator := src.Iter()
    for elem := range iterator {
        dst.Add(elem)
    }
    return dst
}

根据我的经验,在许多情况下,未导出的字段之所以未导出是因为开发人员没有考虑到深拷贝用例(或者可能是有意决定不提供它,并且通常没有记录该决定的原因)。绝对存在这样的情况(除非使用unsafe),否则无法进行有效的深度复制,因为某些关键字段未导出,而包未提供内置机制来进行复制。 - JakeRobb

0
Reprint library具有完善的深拷贝支持,包括拷贝未导出的字段。
package main

import (
    "fmt"

    "github.com/qdm12/reprint"
)

type Test struct {
    field []int
}

func main() {
    orig := Test{field: []int{1, 2}}
    copy := reprint.This(orig).(Test)
    orig.field[0] = 3
    fmt.Println("orig", orig.field[0])
    fmt.Println("copy", copy.field[0])
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接