map->struct

This commit is contained in:
2025-09-23 18:05:41 +08:00
parent 80c5cb350b
commit 442cff29ff
3 changed files with 402 additions and 15 deletions
+213 -12
View File
@@ -1,6 +1,7 @@
package copier package copier
import ( import (
"fmt"
"reflect" "reflect"
"strings" "strings"
"time" "time"
@@ -37,6 +38,7 @@ func deepCopy(dst, src reflect.Value, depth int, opt *options) error {
return nil return nil
} }
// fmt.Println("do deep copy src:", src.Kind().String(), "dst:", dst.Kind().String())
switch src.Kind() { switch src.Kind() {
case reflect.Slice, reflect.Array: case reflect.Slice, reflect.Array:
// slice -> slice // slice -> slice
@@ -47,17 +49,26 @@ func deepCopy(dst, src reflect.Value, depth int, opt *options) error {
// map -> map // map -> map
return copyMap(dst, src, depth, opt) return copyMap(dst, src, depth, opt)
case reflect.Struct: case reflect.Struct:
// struct -> map // map -> struct
return copyStruct2Map(dst, src, depth, opt) return copyMap2Struct(dst, src, depth, opt)
case reflect.Pointer:
elemType := dst.Type().Elem()
newPtr := reflect.New(elemType)
if err := deepCopy(newPtr.Elem(), src, depth+1, opt); err != nil {
return err
}
dst.Set(newPtr)
return nil
default: default:
return ErrNotSupported return ErrNotSupported
} }
case reflect.Struct: case reflect.Struct:
switch dst.Kind() { switch dst.Kind() {
case reflect.Map: case reflect.Map:
// map -> struct // struct -> map
return copyMap2Struct(dst, src, depth, opt) return copyStruct2Map(dst, src, depth, opt)
case reflect.Struct: case reflect.Struct:
// struct -> struct // struct -> struct
return copyStruct(dst, src, depth, opt) return copyStruct(dst, src, depth, opt)
@@ -90,10 +101,14 @@ func deepCopy(dst, src reflect.Value, depth int, opt *options) error {
return deepCopy(dst, src, depth, opt) return deepCopy(dst, src, depth, opt)
case reflect.Interface: case reflect.Interface:
if src.Kind() != dst.Kind() { if src.IsNil() {
return nil return nil
} }
if src.Kind() != dst.Kind() {
return set(dst, src)
}
src = src.Elem() src = src.Elem()
newDst := reflect.New(src.Type().Elem()) newDst := reflect.New(src.Type().Elem())
@@ -103,16 +118,97 @@ func deepCopy(dst, src reflect.Value, depth int, opt *options) error {
dst.Set(newDst) dst.Set(newDst)
return nil return nil
case reflect.Chan:
return ErrNotSupported
default: default:
return copyDefault(dst, src) return set(dst, src)
} }
} }
func copyStruct2Map(dst, src reflect.Value, depth int, opt *options) error { func copyStruct2Map(dst, src reflect.Value, depth int, opt *options) error {
for i, n := 0, src.NumField(); i < n; i++ {
sf := src.Type().Field(i)
if sf.PkgPath != "" && !sf.Anonymous {
continue
}
if sf.Anonymous {
switch sf.Type.Kind() {
case reflect.Struct, reflect.Pointer:
if err := deepCopy(dst, src.Field(i), depth+1, opt); err != nil {
return err
}
}
continue
}
tag := parseTag(sf.Tag.Get(opt.tagName))
if tag.Contains(tagIgnore) {
continue
}
name := toName(sf.Name, tag, opt)
sField := src.Field(i)
if opt.ignoreEmpty && sField.IsZero() {
continue
}
if sField.Kind() == reflect.Pointer || sField.Kind() == reflect.Struct {
var newDst = reflect.ValueOf(make(map[string]any))
sField = indirect(sField)
if err := deepCopy(newDst, sField, depth+1, opt); err != nil {
return err
}
dst.SetMapIndex(reflect.ValueOf(name), newDst)
} else {
dst.SetMapIndex(reflect.ValueOf(name), sField)
}
}
return nil return nil
} }
func copyMap2Struct(dst, src reflect.Value, depth int, opt *options) error { func copyMap2Struct(dst, src reflect.Value, depth int, opt *options) error {
// 循环结构,然后从map取值
typ := dst.Type()
for i := 0; i < dst.NumField(); i++ {
field := dst.Field(i)
sf := typ.Field(i)
if !field.CanSet() {
continue
}
fieldName := getFieldName(sf)
if fieldName == "-" {
continue
}
mapValue := src.MapIndex(reflect.ValueOf(fieldName))
if !mapValue.IsValid() {
continue
}
if mapValue.Kind() == reflect.Interface {
mapValue = mapValue.Elem()
}
if mapValue.Kind() == reflect.Map || mapValue.Kind() == reflect.Array || mapValue.Kind() == reflect.Slice {
if err := deepCopy(field, mapValue, depth+1, opt); err != nil {
return err
}
} else {
if err := set(field, mapValue); err != nil {
return err
}
}
}
return nil return nil
} }
@@ -131,7 +227,14 @@ func copyMap(dst, src reflect.Value, depth int, opt *options) error {
var copitedValue reflect.Value var copitedValue reflect.Value
switch dstType.Elem().Kind() { switch dstType.Elem().Kind() {
case reflect.Interface: case reflect.Interface:
copitedValue = reflect.New(value.Type()).Elem() switch value.Kind() {
case reflect.Interface:
copitedValue = reflect.New(value.Elem().Type()).Elem()
default:
copitedValue = reflect.New(value.Type()).Elem()
}
set(copitedValue, value)
default: default:
copitedValue = reflect.New(dstType.Elem()).Elem() copitedValue = reflect.New(dstType.Elem()).Elem()
} }
@@ -189,7 +292,7 @@ func copyStruct(dst, src reflect.Value, depth int, opt *options) error {
name := toName(sf.Name, tag, opt) name := toName(sf.Name, tag, opt)
dstValue := fieldByName(dst, name, opt) // fieldByName(dst, name, opt) dstValue := fieldByName(dst, name, opt)
if !dstValue.IsValid() { if !dstValue.IsValid() {
continue continue
} }
@@ -206,18 +309,116 @@ func copyStruct(dst, src reflect.Value, depth int, opt *options) error {
return nil return nil
} }
func copyDefault(dst, src reflect.Value) error { func set(dst, src reflect.Value) error {
if src.Type().AssignableTo(dst.Type()) || src.Type().ConvertibleTo(dst.Type()) { if !src.IsValid() {
return ErrInvalidCopyFrom
}
if src.Kind() == reflect.Interface {
src = src.Elem()
}
if dst.Kind() == reflect.Pointer {
if dst.IsNil() {
if !dst.CanSet() {
return ErrInvalidCopyDestination
}
dst.Set(reflect.New(dst.Type().Elem()))
}
dst = dst.Elem()
}
if src.Type().AssignableTo(dst.Type()) {
dst.Set(src)
return nil
}
if src.Type().ConvertibleTo(dst.Type()) {
dst.Set(src.Convert(dst.Type())) dst.Set(src.Convert(dst.Type()))
return nil return nil
} }
switch dst.Kind() { if _, ok := dst.Interface().(time.Time); ok {
switch v := src.Interface().(type) {
case string:
if t, err := time.Parse(time.RFC3339, v); err == nil {
dst.Set(reflect.ValueOf(t))
}
case int64:
dst.Set(reflect.ValueOf(time.Unix(v, 0)))
}
return nil
}
switch dst.Kind() {
case reflect.String:
switch v := src.Interface().(type) {
case string:
dst.SetString(v)
case []byte:
dst.SetString(string(v))
default:
dst.SetString(fmt.Sprintf("%v", v))
}
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
switch v := src.Interface().(type) {
case int:
dst.SetInt(int64(v))
case int8:
dst.SetInt(int64(v))
case int16:
dst.SetInt(int64(v))
case int32:
dst.SetInt(int64(v))
case int64:
}
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
switch v := src.Interface().(type) {
case uint:
dst.SetUint(uint64(v))
case uint8:
dst.SetUint(uint64(v))
case uint16:
dst.SetUint(uint64(v))
case uint32:
dst.SetUint(uint64(v))
}
case reflect.Float32, reflect.Float64:
switch v := src.Interface().(type) {
case float32:
dst.SetFloat(float64(v))
case float64:
dst.SetFloat(v)
}
case reflect.Bool:
switch v := src.Interface().(type) {
case bool:
dst.SetBool(v)
}
default:
return ErrNotSupported
} }
return nil return nil
} }
func getFieldName(field reflect.StructField) string {
if tag := field.Tag.Get("json"); tag != "" {
if commaIndex := strings.Index(tag, ","); commaIndex != -1 {
name := tag[:commaIndex]
if name != "" {
return name
}
} else if tag != "" {
return tag
}
}
return field.Name
}
func toName(name string, tag *tagOption, opt *options) string { func toName(name string, tag *tagOption, opt *options) string {
if tag != nil && tag.Contains(tagToName) { if tag != nil && tag.Contains(tagToName) {
return tag.toname return tag.toname
+181 -3
View File
@@ -3,6 +3,7 @@ package copier
import ( import (
"fmt" "fmt"
"testing" "testing"
"time"
"github.com/charlienet/go-misc/json" "github.com/charlienet/go-misc/json"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@@ -14,14 +15,14 @@ func TestCopyMap(t *testing.T) {
Age int Age int
} }
src := map[string]*person{ src := map[string]any{
"John": { "John": person{
Name: "John", Name: "John",
Age: 30, Age: 30,
}, },
} }
dst := map[string]*person{} dst := map[string]person{}
assert.NoError(t, Copy(&dst, src)) assert.NoError(t, Copy(&dst, src))
@@ -30,6 +31,18 @@ func TestCopyMap(t *testing.T) {
fmt.Println(json.Struct2JsonIndent(dst)) fmt.Println(json.Struct2JsonIndent(dst))
} }
func TestMapAnyCopy(t *testing.T) {
src := map[string]any{
"department": "Engineering",
"level": 3,
}
dst := map[string]any{}
assert.NoError(t, Copy(&dst, src))
fmt.Println(src, dst)
}
func TestDiffStructMapCopy(t *testing.T) { func TestDiffStructMapCopy(t *testing.T) {
type person struct { type person struct {
Name string Name string
@@ -54,3 +67,168 @@ func TestDiffStructMapCopy(t *testing.T) {
fmt.Println(src, dst) fmt.Println(src, dst)
} }
func TestStructToMapCopy(t *testing.T) {
type person struct {
Name string
Age int
Address *Address
}
src := person{
Name: "John",
Age: 30,
Address: &Address{
City: "Beijing",
},
}
dst := map[string]any{}
assert.NoError(t, Copy(&dst, src, WithIgnoreEmpty()))
fmt.Println(src, dst)
println(json.Struct2JsonIndent(dst))
}
func TestAnonymousStructToMapCopy(t *testing.T) {
type person struct {
Name string
Age int
}
type person2 struct {
*person
Address *Address
}
src := person2{
person: &person{
Name: "John",
Age: 30,
},
Address: &Address{
City: "Beijing",
},
}
dst := map[string]any{}
assert.NoError(t, Copy(&dst, src, WithIgnoreEmpty()))
println(dst, json.Struct2JsonIndent(dst))
}
func TestMapToStructCopy(t *testing.T) {
type person struct {
Name string
Age int
Address *Address
}
src := map[string]any{
"Name": "John",
"Age": 30,
"Address": map[string]any{
"City": "Beijing",
},
}
dst := person{}
assert.NoError(t, Copy(&dst, src, WithIgnoreEmpty()))
fmt.Println(dst, json.Struct2JsonIndent(dst))
}
func TestMixMapToStruct(t *testing.T) {
type Address struct {
Street string `json:"street"`
City string `json:"city"`
Country string `json:"country"`
}
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
Height float64 `json:"height"`
IsStudent bool `json:"is_student"`
Birthday time.Time `json:"birthday"`
Tags []string `json:"tags"`
Scores []int `json:"scores"`
Address Address `json:"address"`
Metadata map[string]any `json:"metadata"`
PtrField *string `json:"ptr_field"`
}
src := map[string]any{
"name": "Alice",
"age": 25,
"height": 165.5,
"is_student": true,
"birthday": "1998-05-15T06:00:00Z",
"tags": []any{"developer", "golang", "backend"},
"scores": []any{95, 88, 92},
"address": map[string]any{
"street": "123 Main St",
"city": "Beijing",
"country": "China",
},
"metadata": map[string]any{
"department": "Engineering",
"level": 3,
"projects": []any{"projectA", "projectB"},
},
"ptr_field": "pointer value",
}
var dst Person
assert.NoError(t, Copy(&dst, src, WithIgnoreEmpty()))
fmt.Println(json.Struct2JsonIndent(dst))
}
func BenchmarkMapCopy(b *testing.B) {
type Address struct {
Street string `json:"street"`
City string `json:"city"`
Country string `json:"country"`
}
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
Height float64 `json:"height"`
IsStudent bool `json:"is_student"`
Birthday time.Time `json:"birthday"`
Tags []string `json:"tags"`
Scores []int `json:"scores"`
Address Address `json:"address"`
Metadata map[string]any `json:"metadata"`
PtrField *string `json:"ptr_field"`
}
src := map[string]any{
"name": "Alice",
"age": 25,
"height": 165.5,
"is_student": true,
"birthday": "1998-05-15T00:00:00Z",
"tags": []any{"developer", "golang", "backend"},
"scores": []any{95, 88, 92},
"address": map[string]any{
"street": "123 Main St",
"city": "Beijing",
"country": "China",
},
"metadata": map[string]any{
"department": "Engineering",
"level": 3,
"projects": []any{"projectA", "projectB"},
},
"ptr_field": "pointer value",
}
for b.Loop() {
var dst Person
assert.NoError(b, Copy(&dst, src, WithIgnoreEmpty()))
}
}
+8
View File
@@ -58,3 +58,11 @@ func TestPtrSliceCopy(t *testing.T) {
assert.NotSame(t, src[i], dst[i]) assert.NotSame(t, src[i], dst[i])
} }
} }
func TestAndSlice(t *testing.T) {
src := []any{"developer", "golang", "backend"}
dst := []string{}
assert.NoError(t, Copy(&dst, src))
assert.Equal(t, []string{"developer", "golang", "backend"}, dst)
}