struct slice map

This commit is contained in:
2025-09-23 13:58:04 +08:00
parent 5cc42f5528
commit 80c5cb350b
7 changed files with 408 additions and 57 deletions

146
copier.go
View File

@@ -3,20 +3,21 @@ package copier
import (
"reflect"
"strings"
"time"
)
func Copy(src, dst any, opts ...option) error {
func Copy(dst, src any, opts ...option) error {
opt := getOpt(opts...)
return copy(src, dst, opt)
return copy(dst, src, opt)
}
func copy(src, dst any, opt *options) error {
func copy(dst, src any, opt *options) error {
var (
srcValue = indirect(reflect.ValueOf(dst))
dstValue = indirect(reflect.ValueOf(src))
srcValue = indirect(reflect.ValueOf(src))
dstValue = indirect(reflect.ValueOf(dst))
)
if !dstValue.IsValid() {
if !srcValue.IsValid() {
return ErrInvalidCopyFrom
}
@@ -24,60 +25,94 @@ func copy(src, dst any, opt *options) error {
return ErrInvalidCopyDestination
}
return deepCopy(srcValue, dstValue, 0, opt)
return deepCopy(dstValue, srcValue, 0, opt)
}
func deepCopy(dst, src reflect.Value, depth int, opt *options) error {
switch dst.Kind() {
if src.IsZero() {
return nil
}
if opt.ExceedMaxDepth(depth) {
return nil
}
switch src.Kind() {
case reflect.Slice, reflect.Array:
// slice -> slice
return copySlice(src, dst, depth, opt)
return copySlice(dst, src, depth, opt)
case reflect.Map:
switch src.Kind() {
switch dst.Kind() {
case reflect.Map:
// map -> map
return copyMap(src, dst, depth, opt)
return copyMap(dst, src, depth, opt)
case reflect.Struct:
// struct -> map
return copyStruct2Map(src, dst, depth, opt)
return copyStruct2Map(dst, src, depth, opt)
default:
return ErrNotSupported
}
case reflect.Struct:
switch src.Kind() {
switch dst.Kind() {
case reflect.Map:
// map -> struct
return copyMap2Struct(src, dst, depth, opt)
return copyMap2Struct(dst, src, depth, opt)
case reflect.Struct:
// struct -> struct
return copyStruct(src, dst, depth, opt)
return copyStruct(dst, src, depth, opt)
default:
return ErrNotSupported
}
case reflect.Func:
if dst.Kind() == reflect.Func && dst.IsNil() {
dst.Set(src)
}
return nil
case reflect.Pointer:
if dst.IsNil() {
if dst.Kind() == reflect.Pointer && dst.IsNil() {
if !dst.CanSet() {
return ErrInvalidCopyDestination
return nil
}
p := reflect.New(dst.Type().Elem())
dst.Set(p)
}
src = src.Elem()
dst = dst.Elem()
if src.Kind() == reflect.Pointer {
src = indirect(src)
}
if dst.Kind() == reflect.Pointer {
dst = dst.Elem()
}
return deepCopy(dst, src, depth, opt)
case reflect.Interface:
if src.Kind() != dst.Kind() {
return nil
}
src = src.Elem()
newDst := reflect.New(src.Type().Elem())
if err := deepCopy(newDst, src, depth, opt); err != nil {
return err
}
dst.Set(newDst)
return nil
default:
return copyDefault(src, dst)
return copyDefault(dst, src)
}
}
func copyStruct2Map(src, dst reflect.Value, depth int, opt *options) error {
func copyStruct2Map(dst, src reflect.Value, depth int, opt *options) error {
return nil
}
func copyMap2Struct(src, dst reflect.Value, depth int, opt *options) error {
func copyMap2Struct(dst, src reflect.Value, depth int, opt *options) error {
return nil
}
@@ -86,9 +121,26 @@ func copyMap(dst, src reflect.Value, depth int, opt *options) error {
dst.Set(reflect.MakeMapWithSize(dst.Type(), src.Len()))
}
for _, key := range src.MapKeys() {
value := src.MapIndex(key)
dst.SetMapIndex(key, value)
dstType, _ := indirectType(dst.Type())
iter := src.MapRange()
for iter.Next() {
key := iter.Key()
value := iter.Value()
var copitedValue reflect.Value
switch dstType.Elem().Kind() {
case reflect.Interface:
copitedValue = reflect.New(value.Type()).Elem()
default:
copitedValue = reflect.New(dstType.Elem()).Elem()
}
if err := deepCopy(copitedValue, value, depth+1, opt); err != nil {
return err
}
dst.SetMapIndex(key, copitedValue)
}
return nil
@@ -116,27 +168,48 @@ func copySlice(dst, src reflect.Value, depth int, opt *options) error {
}
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)
if dst.CanSet() {
if _, ok := src.Interface().(time.Time); ok {
dst.Set(src)
return nil
}
}
dstValue := fieldByName(dst, sf.Name, opt)
typ := src.Type()
for i, n := 0, src.NumField(); i < n; i++ {
sf := typ.Field(i)
if sf.PkgPath != "" && !sf.Anonymous {
continue
}
tag := parseTag(sf.Tag.Get(opt.tagName))
if tag.Contains(tagIgnore) {
continue
}
name := toName(sf.Name, tag, opt)
dstValue := fieldByName(dst, name, opt) // fieldByName(dst, name, opt)
if !dstValue.IsValid() {
continue
}
if err := deepCopy(dstValue, src.Field(i), depth+1, opt); err != nil {
sField := src.Field(i)
if opt.ignoreEmpty && sField.IsZero() {
continue
}
if err := deepCopy(dstValue, sField, 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()) {
if src.Type().AssignableTo(dst.Type()) || src.Type().ConvertibleTo(dst.Type()) {
dst.Set(src.Convert(dst.Type()))
return nil
}
switch dst.Kind() {
@@ -145,6 +218,13 @@ func copyDefault(dst, src reflect.Value) error {
return nil
}
func toName(name string, tag *tagOption, opt *options) string {
if tag != nil && tag.Contains(tagToName) {
return tag.toname
}
return opt.NameConvert(name)
}
func fieldByName(v reflect.Value, name string, opt *options) reflect.Value {
if opt.caseSensitive {

View File

@@ -4,6 +4,7 @@ import (
"fmt"
"testing"
"github.com/charlienet/go-misc/json"
"github.com/stretchr/testify/assert"
)
@@ -20,7 +21,34 @@ func TestCopyMap(t *testing.T) {
},
}
dst := map[string]any{}
dst := map[string]*person{}
assert.NoError(t, Copy(&dst, src))
fmt.Println(src, dst)
fmt.Println(json.Struct2JsonIndent(dst))
}
func TestDiffStructMapCopy(t *testing.T) {
type person struct {
Name string
Age int
}
type user struct {
Name string
Age int
}
src := map[string]*person{
"John": {
Name: "John",
Age: 30,
},
}
dst := map[string]*user{}
assert.NoError(t, Copy(&dst, src))

View File

@@ -1,2 +1,225 @@
package copier
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
type Person struct {
Age int ``
Name string ``
Address Address ``
}
type Address struct {
Country string
City string
}
type Person2 struct {
Name string
Age int
}
func TestCopySameStruct(t *testing.T) {
src := &Person{
Name: "John",
Age: 30,
Address: Address{
Country: "USA",
City: "New York",
},
}
var dst Person
assert.NoError(t, Copy(&dst, src))
if dst.Name != src.Name || dst.Age != src.Age {
t.Errorf("Expected %v, got %v", src, dst)
}
fmt.Println("src,dst:", src, dst)
}
func TestCopyDiffStruct(t *testing.T) {
src := &Person{
Name: "John",
Age: 30,
Address: Address{
Country: "USA",
City: "New York",
},
}
var dst Person2
assert.NoError(t, Copy(&dst, src))
t.Log("src,dst:", src, dst)
}
func TestCopyPtrStruct(t *testing.T) {
type User struct {
Name string
Age int
Address *Address
}
src := User{
Name: "John",
Age: 30,
Address: &Address{
Country: "USA",
City: "New York",
},
}
var dst User
assert.NoError(t, Copy(&dst, src))
assert.Equal(t, src, dst)
t.Log("src,dst:", src, dst)
}
func TestAnonymousFields(t *testing.T) {
t.Run("Should work with unexported ptr fields", func(t *testing.T) {
type nested struct {
A string
}
type parentA struct {
*nested
}
type parentB struct {
*nested
}
from := parentA{nested: &nested{A: "a"}}
to := parentB{}
err := Copy(&to, &from)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
from.nested.A = "b"
if to.nested != nil {
t.Errorf("should be nil")
}
})
t.Run("Should work with unexported fields", func(t *testing.T) {
type nested struct {
A string
}
type parentA struct {
nested
}
type parentB struct {
nested
}
from := parentA{nested: nested{A: "a"}}
to := parentB{}
err := Copy(&to, &from)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
from.nested.A = "b"
if to.nested.A == from.nested.A {
t.Errorf("should be different")
}
})
t.Run("Should work with exported ptr fields", func(t *testing.T) {
type Nested struct {
A string
}
type parentA struct {
*Nested
}
type parentB struct {
*Nested
}
fieldValue := "a"
from := parentA{Nested: &Nested{A: fieldValue}}
to := parentB{}
err := Copy(&to, &from)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
from.Nested.A = "b"
if to.Nested.A != fieldValue {
t.Errorf("should not change")
}
})
t.Run("Should work with exported ptr fields with same name src field", func(t *testing.T) {
type Nested struct {
A string
}
type parentA struct {
A string
}
type parentB struct {
*Nested
}
fieldValue := "a"
from := parentA{A: fieldValue}
to := parentB{}
err := Copy(&to, &from)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
from.A = "b"
if to.Nested.A != fieldValue {
t.Errorf("should not change")
}
})
t.Run("Should work with exported fields", func(t *testing.T) {
type Nested struct {
A string
}
type parentA struct {
Nested
}
type parentB struct {
Nested
}
fieldValue := "a"
from := parentA{Nested: Nested{A: fieldValue}}
to := parentB{}
err := Copy(&to, &from)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
from.Nested.A = "b"
if to.Nested.A != fieldValue {
t.Errorf("should not change")
}
})
}

8
go.mod
View File

@@ -2,10 +2,16 @@ module git.charlienet.top/go/copier
go 1.25
require github.com/stretchr/testify v1.11.1
require (
github.com/charlienet/go-misc v0.0.0-20250920151122-cb147afeabdf
github.com/stretchr/testify v1.11.1
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

13
go.sum
View File

@@ -1,7 +1,20 @@
github.com/charlienet/go-misc v0.0.0-20250920151122-cb147afeabdf h1:AQS/jJma5vCm03dbHd2h8FcT/fWlhACpiqMWeuz3XgU=
github.com/charlienet/go-misc v0.0.0-20250920151122-cb147afeabdf/go.mod h1:+NpQcYzYcl/sLr+d04plyLWd2BS6dRmPoccyKn36Yl8=
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/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
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/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
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=

View File

@@ -7,14 +7,15 @@ const (
)
type options struct {
tagName string // 标签名
maxDepth int // 最大复制深度
ignoreEmpty bool // 复制时忽略空字段
caseSensitive bool // 复制时大小写敏感
must bool // 只复制具有must标识的字段
converters []TypeConverter // 类型转换器
fieldNameMapping map[string]string // 字段名转映射
nameConverter func(string) string // 字段名转换器
tagName string // 标签名
maxDepth int // 最大复制深度
ignoreEmpty bool // 复制时忽略空字段
caseSensitive bool // 复制时大小写敏感
must bool // 只复制具有must标识的字段
convertersByName map[string]TypeConverter // 根据名称处理的类型转换器
converters []TypeConverter // 根据源和目标类型处理的类型转换器
fieldNameMapping map[string]string // 字段名转映射
nameConverter func(string) string // 字段名转换器
}
type option func(*options)
@@ -32,7 +33,11 @@ type FieldNameConverter struct {
}
func getOpt(opts ...option) *options {
opt := DefaultOptions
opt := &options{
maxDepth: noDepthLimited,
tagName: defaultTag,
convertersByName: make(map[string]TypeConverter),
}
for _, o := range opts {
o(opt)
@@ -88,6 +93,15 @@ func WithMust() option {
}
}
func WithTypeConvertByName(name string, f func(src any) (dst any, err error)) option {
return func(o *options) {
o.convertersByName[name] = TypeConverter{
FieldName: name,
Fn: f,
}
}
}
func WithConverters(converters ...TypeConverter) option {
return func(o *options) {
o.converters = converters
@@ -111,8 +125,3 @@ func WithTagName(tagName string) option {
o.tagName = tagName
}
}
var DefaultOptions = &options{
maxDepth: noDepthLimited,
tagName: defaultTag,
}

View File

@@ -53,11 +53,3 @@ func (o *tagOption) Contains(tag tagt) bool {
return o.flg&tag == tag
}
func (o *tagOption) ToName() string {
if o.flg&tagToName == 0 {
return ""
}
return o.toname
}