Skip to content

Commit

Permalink
provide generic API in github.com/huandu/go-clone/generic
Browse files Browse the repository at this point in the history
  • Loading branch information
huandu committed Sep 6, 2022
1 parent 80d4f78 commit 22eb9ab
Show file tree
Hide file tree
Showing 9 changed files with 248 additions and 22 deletions.
6 changes: 6 additions & 0 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ jobs:
- name: Test
run: go test -v -coverprofile=covprofile.cov ./...

- name: Test generic
run: |
cd generic
go test -v ./...
cd ..
- name: Send coverage
env:
Expand Down
56 changes: 34 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
# go-clone: Deep clone any Go data
# go-clone: Clone any Go data structure deeply and thoroughly

[![Go](https://github.com/huandu/go-clone/workflows/Go/badge.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)

Package `clone` provides functions to deep clone any Go data.
It also provides a wrapper to protect a pointer from any unexpected mutation.
Package `clone` provides functions to deep clone any Go data. It also provides a wrapper to protect a pointer from any unexpected mutation.

For users who use Go 1.18+, it's recommended to import `github.com/huandu/go-clone/generic` for generic APIs.

`Clone`/`Slowly` can clone unexported fields and "no-copy" structs as well. Use this feature wisely.

Expand Down Expand Up @@ -60,6 +61,30 @@ for i := 0; i < 10; i++ {
}
```

### Generic APIs

Starting from go1.18, Go started to support generic. With generic syntax, `Clone`/`Slowly` and other APIs can be called much cleaner like following.

```go
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
```

It's required to update minimal Go version to 1.18 to opt-in generic syntax. It may not be a wise choice to update this package's `go.mod` and drop so many old Go compilers for such syntax candy. Therefore, I decide to create a new standalone package `github.com/huandu/go-clone/generic` to provide APIs with generic syntax.

For new users who use Go 1.18+, the generic package is preferred and recommended.

### Mark struct type as scalar

Some struct types can be considered as scalar.
Expand Down Expand Up @@ -127,25 +152,12 @@ As there is no way to predefine a custom clone function for generic type `atomic
Suppose we instantiate `atomic.Pointer[T]` with type `MyType1` and `MyType2` in a project, and then we can register custom clone functions like following.

```go
// registerAtomicPointer registers a custom clone function for atomic.Pointer[T].
func registerAtomicPointer[T any]() {
clone.SetCustomFunc(reflect.TypeOf(atomic.Pointer[T]{}), func(old, new reflect.Value) {
if !old.CanAddr() {
return
}

// Clone value inside atomic.Pointer[T].
oldValue := old.Addr().Interface().(*atomic.Pointer[T])
newValue := new.Addr().Interface().(*atomic.Pointer[T])
v := oldValue.Load()
newValue.Store(v)
})
}
import "github.com/huandu/go-clone/generic"

func init() {
// Register all instantiated atomic.Pointer[T] types in this project.
registerAtomicPointer[MyType1]()
registerAtomicPointer[MyType2]()
clone.RegisterAtomicPointer[MyType1]()
clone.RegisterAtomicPointer[MyType2]()
}
```

Expand Down Expand Up @@ -198,10 +210,10 @@ goarch: amd64
pkg: github.com/huandu/go-clone
cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
BenchmarkSimpleClone-12 7903873 142.9 ns/op 24 B/op 1 allocs/op
BenchmarkComplexClone-12 590836 1755 ns/op 1488 B/op 21 allocs/op
BenchmarkUnwrap-12 14988664 71.46 ns/op 0 B/op 0 allocs/op
BenchmarkComplexClone-12 590836 1755 ns/op 1488 B/op 21 allocs/op
BenchmarkUnwrap-12 14988664 71.46 ns/op 0 B/op 0 allocs/op
BenchmarkSimpleWrap-12 3823450 304.4 ns/op 72 B/op 2 allocs/op
BenchmarkComplexWrap-12 867642 1197 ns/op 736 B/op 15 allocs/op
BenchmarkComplexWrap-12 867642 1197 ns/op 736 B/op 15 allocs/op
```

## License
Expand Down
8 changes: 8 additions & 0 deletions generic/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Generic `go-clone` API

[![Go](https://github.com/huandu/go-clone/workflows/Go/badge.svg)](https://github.com/huandu/go-clone/actions)
[![Go Doc](https://godoc.org/github.com/huandu/go-clone/generic?status.svg)](https://pkg.go.dev/github.com/huandu/go-clone/generic)

This package is a set of generic API for `go-clone`. All methods are simple proxies. It requires `go1.18` or later to build this package.

Please read document in [the main project](../README.md) for more information.
50 changes: 50 additions & 0 deletions generic/api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Copyright 2022 Huan Du. All rights reserved.
// Licensed under the MIT license that can be found in the LICENSE file.

// Package clone provides functions to deep clone any Go data.
// It also provides a wrapper to protect a pointer from any unexpected mutation.
//
// This package is only a proxy to original go-clone package with generic support.
// To minimize the maintenace cost, there is no doc in this package.
// Please read the document in https://pkg.go.dev/github.com/huandu/go-clone instead.
package clone

import (
"reflect"

"github.com/huandu/go-clone"
)

type Func = clone.Func

func Clone[T any](t T) T {
return clone.Clone(t).(T)
}

func Slowly[T any](t T) T {
return clone.Slowly(t).(T)
}

func Wrap[T any](t T) T {
return clone.Wrap(t).(T)
}

func Unwrap[T any](t T) T {
return clone.Unwrap(t).(T)
}

func Undo[T any](t T) {
clone.Undo(t)
}

func MarkAsOpaquePointer(t reflect.Type) {
clone.MarkAsOpaquePointer(t)
}

func MarkAsScalar(t reflect.Type) {
clone.MarkAsScalar(t)
}

func SetCustomFunc(t reflect.Type, fn Func) {
clone.SetCustomFunc(t, fn)
}
61 changes: 61 additions & 0 deletions generic/api_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// Copyright 2022 Huan Du. All rights reserved.
// Licensed under the MIT license that can be found in the LICENSE file.

package clone

import (
"reflect"
"testing"

"github.com/huandu/go-assert"
)

type MyType struct {
Foo int
bar string
}

func TestGenericAPI(t *testing.T) {
a := assert.New(t)
original := &MyType{
Foo: 123,
bar: "player",
}

var v *MyType = Clone(original)
a.Equal(v, original)

v = Slowly(original)
a.Equal(v, original)

v = Wrap(original)
a.Equal(v, original)
a.Assert(Unwrap(v) == original)

v.Foo = 777
a.Equal(Unwrap(v).Foo, original.Foo)

Undo(v)
a.Equal(v, original)
}

type MyPointer struct {
Foo *int
P *MyPointer
}

func TestMarkAsAPI(t *testing.T) {
a := assert.New(t)
MarkAsScalar(reflect.TypeOf(MyPointer{}))
MarkAsOpaquePointer(reflect.TypeOf(&MyPointer{}))

n := 0
orignal := MyPointer{
Foo: &n,
}
orignal.P = &orignal

v := Clone(orignal)
a.Assert(v.Foo == orignal.Foo)
a.Assert(v.P == &orignal)
}
10 changes: 10 additions & 0 deletions generic/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module github.com/huandu/go-clone/generic

go 1.18

require (
github.com/huandu/go-assert v1.1.5
github.com/huandu/go-clone v1.4.0
)

require github.com/davecgh/go-spew v1.1.1 // indirect
12 changes: 12 additions & 0 deletions generic/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/huandu/go-assert v1.1.5 h1:fjemmA7sSfYHJD7CUqs9qTwwfdNAx7/j2/ZlHXzNB3c=
github.com/huandu/go-assert v1.1.5/go.mod h1:yOLvuqZwmcHIC5rIzrBhT7D3Q9c3GFnd0JrPVhn/06U=
github.com/huandu/go-clone v1.4.0 h1:NlnghW4lsmMoz+3N4yb4Ouff86ArRYPo/1aCsqQKKF4=
github.com/huandu/go-clone v1.4.0/go.mod h1:ReGivhG6op3GYr+UY3lS6mxjKp7MIGTknuU5TbTVaXE=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
29 changes: 29 additions & 0 deletions generic/register.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Copyright 2022 Huan Du. All rights reserved.
// Licensed under the MIT license that can be found in the LICENSE file.

package clone

import (
"reflect"
"sync/atomic"
)

// Record the count of cloning atomic.Pointer[T] for test purpose only.
var registerAtomicPointerCalled int32

// RegisterAtomicPointer registers a custom clone function for atomic.Pointer[T].
func RegisterAtomicPointer[T any]() {
SetCustomFunc(reflect.TypeOf(atomic.Pointer[T]{}), func(old, new reflect.Value) {
if !old.CanAddr() {
return
}

// Clone value inside atomic.Pointer[T].
oldValue := old.Addr().Interface().(*atomic.Pointer[T])
newValue := new.Addr().Interface().(*atomic.Pointer[T])
v := oldValue.Load()
newValue.Store(v)

atomic.AddInt32(&registerAtomicPointerCalled, 1)
})
}
38 changes: 38 additions & 0 deletions generic/register_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Copyright 2022 Huan Du. All rights reserved.
// Licensed under the MIT license that can be found in the LICENSE file.

package clone

import (
"sync/atomic"
"testing"

"github.com/huandu/go-assert"
)

type RegisteredPayload struct {
T string
}

type UnregisteredPayload struct {
T string
}

type Pointers struct {
P1 atomic.Pointer[RegisteredPayload]
P2 atomic.Pointer[UnregisteredPayload]
}

func TestRegisterAtomicPointer(t *testing.T) {
a := assert.New(t)
s := &Pointers{}
stackPointerCannotBeCloned := atomic.Pointer[RegisteredPayload]{}

// Register atomic.Pointer[RegisteredPayload] only.
RegisterAtomicPointer[RegisteredPayload]()

prev := registerAtomicPointerCalled
Clone(s)
Clone(stackPointerCannotBeCloned)
a.Equal(registerAtomicPointerCalled, prev+1)
}

0 comments on commit 22eb9ab

Please sign in to comment.