This commit is contained in:
2025-09-22 17:49:41 +08:00
parent 4268c402eb
commit 5cc42f5528
10 changed files with 515 additions and 1 deletions

View File

@@ -1,3 +1,43 @@
# copier
Golang object deep copy library
Golang object deep copy library
map->map
slice->slice
struct->struct
map->struct
struct->map
## Usage
```go
package main
import (
"fmt"
"github.com/chenquan/copier"
)
type User struct {
Name string
Age int
}
type User2 struct {
Name string
Age int
}
func main() {
user := User{
Name: "chenquan",
Age: 18,
}
user2 := User2{}
err := copier.Copy(&user2, &user)
if err != nil {
fmt.Println(err)
}
fmt.Println(user2)
}

171
copier.go Normal file
View File

@@ -0,0 +1,171 @@
package copier
import (
"reflect"
"strings"
)
func Copy(src, dst any, opts ...option) error {
opt := getOpt(opts...)
return copy(src, dst, opt)
}
func copy(src, dst any, opt *options) error {
var (
srcValue = indirect(reflect.ValueOf(dst))
dstValue = indirect(reflect.ValueOf(src))
)
if !dstValue.IsValid() {
return ErrInvalidCopyFrom
}
if !dstValue.CanSet() {
return ErrInvalidCopyDestination
}
return deepCopy(srcValue, dstValue, 0, opt)
}
func deepCopy(dst, src reflect.Value, depth int, opt *options) error {
switch dst.Kind() {
case reflect.Slice, reflect.Array:
// slice -> slice
return copySlice(src, dst, depth, opt)
case reflect.Map:
switch src.Kind() {
case reflect.Map:
// map -> map
return copyMap(src, dst, depth, opt)
case reflect.Struct:
// struct -> map
return copyStruct2Map(src, dst, depth, opt)
default:
return ErrNotSupported
}
case reflect.Struct:
switch src.Kind() {
case reflect.Map:
// map -> struct
return copyMap2Struct(src, dst, depth, opt)
case reflect.Struct:
// struct -> struct
return copyStruct(src, dst, depth, opt)
default:
return ErrNotSupported
}
case reflect.Pointer:
if dst.IsNil() {
if !dst.CanSet() {
return ErrInvalidCopyDestination
}
p := reflect.New(dst.Type().Elem())
dst.Set(p)
}
src = src.Elem()
dst = dst.Elem()
return deepCopy(dst, src, depth, opt)
default:
return copyDefault(src, dst)
}
}
func copyStruct2Map(src, dst reflect.Value, depth int, opt *options) error {
return nil
}
func copyMap2Struct(src, dst reflect.Value, depth int, opt *options) error {
return nil
}
func copyMap(dst, src reflect.Value, depth int, opt *options) error {
if dst.IsNil() {
dst.Set(reflect.MakeMapWithSize(dst.Type(), src.Len()))
}
for _, key := range src.MapKeys() {
value := src.MapIndex(key)
dst.SetMapIndex(key, value)
}
return nil
}
func copySlice(dst, src reflect.Value, depth int, opt *options) error {
len := src.Len()
if dst.Len() > 0 && dst.Len() < src.Len() {
len = dst.Len()
}
if dst.Kind() == reflect.Slice && dst.Len() == 0 && src.Len() > 0 {
dstType := dst.Type().Elem()
newDst := reflect.MakeSlice(reflect.SliceOf(dstType), len, len)
dst.Set(newDst)
}
for i := 0; i < len; i++ {
if err := deepCopy(dst.Index(i), src.Index(i), depth, opt); err != nil {
return err
}
}
return nil
}
func copyStruct(dst, src reflect.Value, depth int, opt *options) error {
typ := src.Type()
for i := 0; i < typ.NumField(); i++ {
sf := typ.Field(i)
dstValue := fieldByName(dst, sf.Name, opt)
if !dstValue.IsValid() {
continue
}
if err := deepCopy(dstValue, src.Field(i), depth+1, opt); err != nil {
return err
}
}
return nil
}
func copyDefault(dst, src reflect.Value) error {
if src.Type().AssignableTo(dst.Type()) || dst.Type().ConvertibleTo(src.Type()) {
dst.Set(src.Convert(dst.Type()))
}
switch dst.Kind() {
}
return nil
}
func fieldByName(v reflect.Value, name string, opt *options) reflect.Value {
if opt.caseSensitive {
return v.FieldByName(name)
}
return v.FieldByNameFunc(func(s string) bool { return strings.EqualFold(s, name) })
}
func indirect(reflectValue reflect.Value) reflect.Value {
for reflectValue.Kind() == reflect.Pointer {
reflectValue = reflectValue.Elem()
}
return reflectValue
}
func indirectType(reflectType reflect.Type) (_ reflect.Type, isPtr bool) {
for reflectType.Kind() == reflect.Pointer || reflectType.Kind() == reflect.Slice {
reflectType = reflectType.Elem()
isPtr = true
}
return reflectType, isPtr
}

28
copier_map_test.go Normal file
View File

@@ -0,0 +1,28 @@
package copier
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
func TestCopyMap(t *testing.T) {
type person struct {
Name string
Age int
}
src := map[string]*person{
"John": {
Name: "John",
Age: 30,
},
}
dst := map[string]any{}
assert.NoError(t, Copy(&dst, src))
fmt.Println(src, dst)
}

60
copier_slice_test.go Normal file
View File

@@ -0,0 +1,60 @@
package copier
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
func TestSliceCopy(t *testing.T) {
type person struct {
Name string
Age int
}
src := []person{
{Name: "Alice", Age: 20},
{Name: "Bob", Age: 30},
}
dst := []person{}
Copy(&dst, src)
// Check if the copy was successful
if len(dst) != len(src) {
t.Errorf("Expected %d elements, got %d", len(src), len(dst))
}
fmt.Println("dst:", dst)
}
func TestPtrSliceCopy(t *testing.T) {
type person struct {
Name string
Age int
}
src := []*person{
{Name: "Alice", Age: 20},
{Name: "Bob", Age: 30},
}
dst := []*person{}
Copy(&dst, src)
// Check if the copy was successful
if len(dst) != len(src) {
t.Errorf("Expected %d elements, got %d", len(src), len(dst))
}
assert.Equal(t, src[0].Name, dst[0].Name)
for i := range src {
assert.Equal(t, src[i].Name, dst[i].Name)
assert.Equal(t, src[i].Age, dst[i].Age)
assert.NotSame(t, src[i], dst[i])
}
}

2
copier_test.go Normal file
View File

@@ -0,0 +1,2 @@
package copier

11
errors.go Normal file
View File

@@ -0,0 +1,11 @@
package copier
import "errors"
var (
ErrInvalidCopyDestination = errors.New("copy destination must be non-nil and addressable")
ErrInvalidCopyFrom = errors.New("copy from must be non-nil and addressable")
ErrMapKeyNotMatch = errors.New("map's key type doesn't match")
ErrNotSupported = errors.New("not supported")
ErrFieldNameTagStartNotUpperCase = errors.New("copier field name tag must be start upper case")
)

11
go.mod Normal file
View File

@@ -0,0 +1,11 @@
module git.charlienet.top/go/copier
go 1.25
require github.com/stretchr/testify v1.11.1
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

10
go.sum Normal file
View File

@@ -0,0 +1,10 @@
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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

118
optiongs.go Normal file
View File

@@ -0,0 +1,118 @@
package copier
import "reflect"
const (
noDepthLimited = -1
)
type options struct {
tagName string // 标签名
maxDepth int // 最大复制深度
ignoreEmpty bool // 复制时忽略空字段
caseSensitive bool // 复制时大小写敏感
must bool // 只复制具有must标识的字段
converters []TypeConverter // 类型转换器
fieldNameMapping map[string]string // 字段名转映射
nameConverter func(string) string // 字段名转换器
}
type option func(*options)
type TypeConverter struct {
FieldName string
SrcType any
DstType any
Fn func(src any) (dst any, err error)
}
type FieldNameConverter struct {
SrcFieldName string
DstFieldName string
}
func getOpt(opts ...option) *options {
opt := DefaultOptions
for _, o := range opts {
o(opt)
}
return opt
}
func (opt *options) NameConvert(name string) string {
if opt.nameConverter != nil {
name = opt.nameConverter(name)
}
if toname, ok := opt.fieldNameMapping[name]; ok {
name = toname
}
return name
}
func (opt options) TypeConvert(value reflect.Value) (reflect.Value, bool) {
for _, c := range opt.converters {
_ = c
}
return value, false
}
func (opt *options) ExceedMaxDepth(depth int) bool {
return opt.maxDepth != noDepthLimited && depth > opt.maxDepth
}
func WithMaxDepth(depth int) option {
return func(o *options) {
o.maxDepth = depth
}
}
func WithIgnoreEmpty() option {
return func(o *options) {
o.ignoreEmpty = true
}
}
func WithCaseSensitive() option {
return func(o *options) {
o.caseSensitive = true
}
}
func WithMust() option {
return func(o *options) {
o.must = true
}
}
func WithConverters(converters ...TypeConverter) option {
return func(o *options) {
o.converters = converters
}
}
func WithNameMapping(mappings map[string]string) option {
return func(o *options) {
o.fieldNameMapping = mappings
}
}
func WithNameFn(fn func(string) string) option {
return func(o *options) {
o.nameConverter = fn
}
}
func WithTagName(tagName string) option {
return func(o *options) {
o.tagName = tagName
}
}
var DefaultOptions = &options{
maxDepth: noDepthLimited,
tagName: defaultTag,
}

63
tags.go Normal file
View File

@@ -0,0 +1,63 @@
package copier
import "strings"
const (
defaultTag = "copier"
)
type tagt uint8
const (
tagMust tagt = 1 << iota
tagIgnore
tagToName
)
// 标签 copier 取值 must、ignore、toname 等
// copier:"must,toname=xxx" 必须复制,并重命名为 xxx
type tagOption struct {
flg tagt
toname string
}
func parseTag(tag string) *tagOption {
opt := tagOption{}
flg := tagt(0)
for t := range strings.SplitSeq(tag, ",") {
tag, value, found := strings.Cut(t, "=")
switch tag {
case "-", "ignore":
flg |= tagIgnore
case "must":
flg |= tagMust
case "toname":
flg |= tagToName
if found {
opt.toname = value
}
}
}
opt.flg = flg
return &opt
}
func (o *tagOption) Contains(tag tagt) bool {
if o.flg == 0 {
return false
}
return o.flg&tag == tag
}
func (o *tagOption) ToName() string {
if o.flg&tagToName == 0 {
return ""
}
return o.toname
}