huandu/go-clone

GitHub: huandu/go-clone

一个支持深度克隆任意 Go 数据结构并提供指针修改保护的通用工具库。

Stars: 331 | Forks: 32

# go-clone:深度彻底克隆任意 Go 数据结构 [![Go](https://static.pigsec.cn/wp-content/uploads/repos/cas/2a/2a44ebb2294c01676e19eed14adc6cea930435b1ac09f01b83d80f1ec8c274bf.svg)](https://github.com/huandu/go-clone/actions) [![Go Doc](https://godoc.org/github.com/huandu/go-clone?status.svg)](https://pkg.go.dev/github.com/huandu/go-clone) [![Go Report](https://goreportcard.com/badge/github.com/huandu/go-clone)](https://goreportcard.com/report/github.com/huandu/go-clone) [![Coverage Status](https://coveralls.io/repos/github/huandu/go-clone/badge.svg?branch=master)](https://coveralls.io/github/huandu/go-clone?branch=master) `clone` 包提供了深度克隆任意 Go 数据的函数。它还提供了一个包装器,用于保护指针免受任何意外的修改。 对于使用 Go 1.18+ 的用户,建议导入 `github.com/huandu/go-clone/generic` 以使用泛型 API 和 arena 支持。 `Clone`/`Slowly` 也可以克隆未导出的字段和 "no-copy" 结构体。请谨慎使用此功能。 ## 安装 使用 `go get` 安装此包。 ``` go get github.com/huandu/go-clone ``` ## 用法 ### `Clone` 和 `Slowly` 如果我们想克隆任何 Go 值,请使用 `Clone`。 ``` t := &T{...} v := clone.Clone(t).(*T) reflect.DeepEqual(t, v) // true ``` 出于性能考虑,`Clone` 不处理包含指针循环的值。 如果我们需要克隆此类值,请改用 `Slowly`。 ``` type ListNode struct { Data int Next *ListNode } node1 := &ListNode{ Data: 1, } node2 := &ListNode{ Data: 2, } node3 := &ListNode{ Data: 3, } node1.Next = node2 node2.Next = node3 node3.Next = node1 // We must use `Slowly` to clone a circular linked list. node := Slowly(node1).(*ListNode) for i := 0; i < 10; i++ { fmt.Println(node.Data) node = node.Next } ``` ### 泛型 API 从 Go 1.18 开始,Go 开始支持泛型。借助泛型语法,可以像下面这样更简洁地调用 `Clone`/`Slowly` 和其他 API。 ``` import "github.com/huandu/go-clone/generic" type MyType struct { Foo string } original := &MyType{ Foo: "bar", } // The type of cloned is *MyType instead of interface{}. cloned := Clone(original) println(cloned.Foo) // Output: bar ``` 要启用泛型语法,需要将最低 Go 版本更新至 1.18。为了这种语法糖而更新此包的 `go.mod` 并放弃对众多旧版 Go 编译器的支持可能并非明智之举。因此,我决定创建一个独立的新包 `github.com/huandu/go-clone/generic`,以提供包含泛型语法的 API。 对于使用 Go 1.18+ 的新用户,推荐并首选使用该泛型包。 ### Arena 支持 从 Go 1.20 开始,引入了 arena 作为一种分配内存的新方式。这在特定场景下对于提升整体性能非常有用。 为了克隆一个内存由 arena 分配的值,在 `github.com/huandu/go-clone/generic` 中提供了新的方法 `ArenaClone` 和 `ArenaCloneSlowly`。 ``` // ArenaClone recursively deep clones v to a new value in arena a. // It works in the same way as Clone, except it allocates all memory from arena. func ArenaClone[T any](a *arena.Arena, v T) (nv T) // ArenaCloneSlowly recursively deep clones v to a new value in arena a. // It works in the same way as Slowly, except it allocates all memory from arena. func ArenaCloneSlowly[T any](a *arena.Arena, v T) (nv T) ``` 由于 arena API 的限制,`map` 和 `chan` 内部数据结构的内存始终由 Go 运行时在堆上分配([参见此 issue](https://github.com/golang/go/issues/56230))。 **警告**:根据 [arena 提案中的讨论](https://github.com/golang/go/issues/51317),arena 包在未来可能会发生不兼容的更改或被移除。此包中所有与 arena 相关的 API 也将相应地进行更改。 ### 结构体标签 有一些结构体标签可用于控制如何克隆结构体字段。 ``` type T struct { Normal *int Foo *int `clone:"skip"` // Skip cloning this field so that Foo will be zero in cloned value. Bar *int `clone:"-"` // "-" is an alias of skip. Baz *int `clone:"shadowcopy"` // Copy this field by shadow copy. } a := 1 t := &T{ Normal: &a, Foo: &a, Bar: &a, Baz: &a, } v := clone.Clone(t).(*T) fmt.Println(v.Normal == t.Normal) // false fmt.Println(v.Foo == nil) // true fmt.Println(v.Bar == nil) // true fmt.Println(v.Baz == t.Baz) // true ``` ### 内存分配与 `Allocator` `Allocator` 旨在在克隆时分配内存。它还用于保存所有自定义设置,例如自定义克隆函数、标量类型和不透明指针等。有一个默认的分配器用于从堆中分配内存。此包中几乎所有的公开 API 都使用此默认分配器来完成工作。 我们可以通过 `NewAllocator` 创建新的 `Allocator` 来控制如何分配内存。它使我们能够在克隆时完全控制内存分配。请参阅 [Allocator 示例代码](https://pkg.go.dev/github.com/huandu/go-clone#example-Allocator) 以了解如何自定义分配器。 让我们仔细看看 `NewAllocator` 函数。 ``` func NewAllocator(pool unsafe.Pointer, methods *AllocatorMethods) *Allocator ``` - 第一个参数 `pool` 是指向内存池的指针。它用于在克隆时分配内存。如果我们不需要内存池,它可以设为 `nil`。 - 第二个参数 `methods` 是指向一个结构体的指针,该结构体包含所有用于分配内存的方法。如果我们不需要自定义内存分配,它可以设为 `nil`。 - `Allocator` 结构体由 `methods.New` 或 `methods.Parent` 分配器分配,或者从堆中分配。 `AllocatorMethods` 中的 `Parent` 用于指示新分配器的父级。借助此功能,我们可以将分配器组织成树状结构。所有的自定义设置(包括自定义克隆函数、标量类型和不透明指针等)都会从父分配器继承。 为了方便起见,还提供了一些 API。 - 我们可以通过调用 `FromHeap()` 或 `FromArena(a *arena.Arena)` 为堆或 arena 创建专用的分配器。 - 我们可以调用 `MakeCloner(allocator)` 来创建一个辅助结构体,该结构体包含输入和输出参数类型均为 `interface{}` 的 `Clone` 和 `CloneSlowly` 方法。 ### 将结构体类型标记为标量 某些结构体类型可以被视为标量。 一个众所周知的例子是 `time.Time`。 尽管 `time.Time` 内部存在指针 `loc *time.Location`,但在所有方法中,我们总是按值使用 `time.Time`。 在克隆 `time.Time` 时,返回一个浅拷贝应该也是可以接受的。 目前,默认将以下类型标记为标量。 - `time.Time` - `reflect.Value` 如果内置包中定义的任何类型应被视为标量,请提交新 issue 告知我。 我将更新默认设置。 如果有任何自定义类型应被视为标量,请调用 `MarkAsScalar` 手动标记它。有关更多详细信息,请参阅 [MarkAsScalar 示例代码](https://pkg.go.dev/github.com/huandu/go-clone#example-MarkAsScalar)。 ### 将指针类型标记为不透明 某些指针值被用作可枚举的常量值。 一个众所周知的例子是 `elliptic.Curve`。在 `crypto/tls` 包中,证书的 curve 类型是通过将值与预定义的 curve 值(例如 `elliptic.P521()`)进行比较来检查的。在这种情况下,curve 值(即指针或结构体)不能被深度克隆。 目前,默认将以下类型标记为标量。 - `elliptic.Curve`,即 `*elliptic.CurveParam` 或 `elliptic.p256Curve`。 - `reflect.Type`,即在 `runtime` 中定义的 `*reflect.rtype`。 如果内置包中定义的任何指针类型应被视为不透明,请提交新 issue 告知我。 我将更新默认设置。 如果有任何自定义指针类型应被视为不透明,请调用 `MarkAsOpaquePointer` 手动标记它。有关更多详细信息,请参阅 [MarkAsOpaquePointer 示例代码](https://pkg.go.dev/github.com/huandu/go-clone#example-MarkAsOpaquePointer)。 ### 克隆 `sync` 和 `sync/atomic` 中定义的 "no-copy" 类型 有一些像 `sync.Mutex`、`atomic.Value` 等的 "no-copy" 类型。 它们不能通过逐个复制所有字段来克隆,但我们可以分配一个新的零值并调用相关方法来进行正确的初始化。 目前,定义在 `sync` 和 `sync/atomic` 中的所有 "no-copy" 类型都可以使用以下策略进行正确的克隆。 - `sync.Mutex`:克隆后的值是一个新分配的零值 mutex。 - `sync.RWMutex`:克隆后的值是一个新分配的零值 mutex。 - `sync.WaitGroup`:克隆后的值是一个新分配的零值 wait group。 - `sync.Cond`:克隆后的值是一个带有新分配的零值锁的 cond。 - `sync.Pool`:克隆后的值是一个具有相同 `New` 函数的空 pool。 - `sync.Map`:克隆后的值是一个包含已克隆的键/值对的 sync map。 - `sync.Once`:克隆后的值是一个带有相同 done 标志的 once 类型。 - `atomic.Value`/`atomic.Bool`/`atomic.Int32`/`atomic.Int64`/`atomic.Uint32`/`atomic.Uint64`/`atomic.Uintptr`:克隆后的值是一个具有相同值的新原子值。 如果内置包中定义的任何类型应被视为 "no-copy" 类型,请提交新 issue 告知我。 我将更新默认设置。 ### 设置自定义克隆函数 如果默认的克隆策略不适用于某个结构体类型,我们可以调用 `SetCustomFunc` 来注册自定义克隆函数。 ``` SetCustomFunc(reflect.TypeOf(MyType{}), func(allocator *Allocator, old, new reflect.Value) { // Customized logic to copy the old to the new. // The old's type is MyType. // The new is a zero value of MyType and new.CanAddr() always returns true. }) ``` 我们可以使用 `allocator` 来克隆任何值或分配新内存。 允许对 `old` 调用 `allocator.Clone` 或 `allocator.CloneSlowly` 来深度克隆其结构体字段,而无需担心出现死循环。 有关更多详细信息,请参阅 [SetCustomFunc 示例代码](https://pkg.go.dev/github.com/huandu/go-clone#example-SetCustomFunc)。 ### 克隆 `atomic.Pointer[T]` 由于无法为泛型类型 `atomic.Pointer[T]` 预定义自定义克隆函数,因此默认情况下不支持克隆此类原子类型。如果我们想支持它,需要手动注册自定义克隆函数。 假设我们在一个项目中使用类型 `MyType1` 和 `MyType2` 实例化了 `atomic.Pointer[T]`,然后我们可以像下面这样注册自定义克隆函数。 ``` import "github.com/huandu/go-clone/generic" func init() { // Register all instantiated atomic.Pointer[T] types in this project. clone.RegisterAtomicPointer[MyType1]() clone.RegisterAtomicPointer[MyType2]() } ``` ### `Wrap`、`Unwrap` 和 `Undo` `clone` 包提供了 `Wrap`/`Unwrap` 函数,以保护指针值免受任何意外的修改。 当我们想要保护一个在设计上应该是不可变的变量时, (例如全局配置、存储在 context 中的值、发送到 chan 的值等),它非常有用。 ``` // Suppose we have a type T defined as following. // type T struct { // Foo int // } v := &T{ Foo: 123, } w := Wrap(v).(*T) // Wrap value to protect it. // Use w freely. The type of w is the same as that of v. // It's OK to modify w. The change will not affect v. w.Foo = 456 fmt.Println(w.Foo) // 456 fmt.Println(v.Foo) // 123 // Once we need the original value stored in w, call `Unwrap`. orig := Unwrap(w).(*T) fmt.Println(orig == v) // true fmt.Println(orig.Foo) // 123 // Or, we can simply undo any change made in w. // Note that `Undo` is significantly slower than `Unwrap`, thus // the latter is always preferred. Undo(w) fmt.Println(w.Foo) // 123 ``` ## 性能 以下是在我的开发机器上运行的性能数据。 ``` go 1.20.1 goos: darwin goarch: amd64 cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz BenchmarkSimpleClone-12 7164530 156.7 ns/op 24 B/op 1 allocs/op BenchmarkComplexClone-12 628056 1871 ns/op 1488 B/op 21 allocs/op BenchmarkUnwrap-12 15498139 78.02 ns/op 0 B/op 0 allocs/op BenchmarkSimpleWrap-12 3882360 309.7 ns/op 72 B/op 2 allocs/op BenchmarkComplexWrap-12 949654 1245 ns/op 736 B/op 15 allocs/op ``` ## 许可证 此包基于 MIT 许可证授权。有关详细信息,请参阅 LICENSE。
标签:EVTX分析, Go, Ruby工具, 克隆, 数据结构, 日志审计, 泛型, 深拷贝, 通用库