huandu/go-clone
GitHub: huandu/go-clone
一个支持深度克隆任意 Go 数据结构并提供指针修改保护的通用工具库。
Stars: 331 | Forks: 32
# go-clone:深度彻底克隆任意 Go 数据结构
[](https://github.com/huandu/go-clone/actions)
[](https://pkg.go.dev/github.com/huandu/go-clone)
[](https://goreportcard.com/report/github.com/huandu/go-clone)
[](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工具, 克隆, 数据结构, 日志审计, 泛型, 深拷贝, 通用库