[{"data":1,"prerenderedAt":91},["ShallowReactive",2],{"content:\u002Fja\u002Ffiddling\u002Fgolang-deepcopy-between-different-type":3,"surround:\u002Fja\u002Ffiddling\u002Fgolang-deepcopy-between-different-type":80},{"id":4,"title":5,"authorship":6,"body":7,"categories":52,"date":54,"description":55,"draft":56,"extension":57,"image":58,"meta":59,"navigation":61,"path":62,"permalink":63,"published":63,"readingTime":64,"recommend":63,"references":63,"seo":69,"seoDescription":70,"seoTitle":63,"sitemap":71,"stem":72,"tags":73,"type":78,"__hash__":79},"content_ja\u002Fposts\u002Ffiddling\u002Fgolang-deepcopy-between-different-type.md","Goで異なる型の構造体をディープコピーする","human-only",{"type":8,"value":9,"toc":49},"minimark",[10,14,17,20,23,34,37,40,43,46],[11,12,13],"p",{},"最近はシステムのリファクタリングで大忙しです。ブログもすっかり放置してしまいました。",[11,15,16],{},"その中で、かなり厄介な問題に出会いました。レイヤードアーキテクチャにおける、階層ごとのエンティティの相互変換です。商品を例にすると、ビュー層には商品VO、ドメイン層には商品entity、あるいはDO（domain object）、永続化層にはデータベースのエンティティに対応する商品POがある、といった具合です。",[11,18,19],{},"これらの構造は大抵よく似ていて、ほぼ同じ、あるいは完全に同じものもあります。ただ、あるフィールドが一方ではポインター型で、もう一方では非ポインター型という微妙な違いがあると、直接キャストできません。そこで変換用のassemblerメソッドを大量に書くことになりますが、複雑な構造になるとまさに地獄です。本質的にはディープコピーなのに、型が違うというだけで処理できないわけです。",[11,21,22],{},"この特殊なケースを少し汎用的に扱える変換処理を、リフレクションで作れないかと考えました。そうして午後いっぱいかけて生まれたのが、次のコードです。",[24,25,31],"pre",{"className":26,"code":28,"language":29,"meta":30},[27],"language-go","func Copy(src interface{}, dstType interface{}) interface{} {\n    if src == nil {\n        return nil\n    }\n    cpy := reflect.New(reflect.TypeOf(dstType)).Elem()\n    copyRecursive(reflect.ValueOf(src), cpy)\n    return cpy.Interface()\n}\n \nfunc copyRecursive(src, dst reflect.Value) {\n    switch src.Kind() {\n    case reflect.Ptr:\n        originValue := src.Elem()\n        if !originValue.IsValid() {\n            return\n        }\n        \u002F\u002F 允许 src 为 ptr 而 dst 为非 ptr\n        if dst.Kind() == reflect.Ptr {\n            dst.Set(reflect.New(dst.Type().Elem()))\n            copyRecursive(originValue, dst.Elem())\n        } else {\n            dst.Set(reflect.New(dst.Type()).Elem())\n            copyRecursive(originValue, dst)\n        }\n    case reflect.Interface:\n        if src.IsNil() {\n            return\n        }\n        originValue := src.Elem()\n        copyValue := reflect.New(dst.Type().Elem()).Elem()\n        copyRecursive(originValue, copyValue)\n        dst.Set(copyValue)\n    case reflect.Struct:\n        \u002F\u002F time.Time 需要特殊处理\n        t, ok := src.Interface().(time.Time)\n        if ok {\n            dst.Set(reflect.ValueOf(t))\n            return\n        }\n        if dst.Kind() == reflect.Ptr {\n            \u002F\u002F 目标类型是指针而源类型不是指针\n            copyValue := reflect.New(dst.Type().Elem()).Elem()\n            copyRecursive(src, copyValue)\n            dst.Set(copyValue.Addr())\n            return\n        }\n        for i := 0; i \u003C dst.NumField(); i++ {\n            if dst.Type().Field(i).PkgPath != \"\" {\n                \u002F\u002F 不可导出的字段不拷贝\n                continue\n            }\n            field := src.FieldByName(dst.Type().Field(i).Name)\n            if !field.IsValid() {\n                \u002F\u002F 源字段不存在，忽略（目标自动零值）\n                continue\n            }\n            copyRecursive(field, dst.Field(i))\n        }\n    case reflect.Slice:\n        if src.IsNil() {\n            return\n        }\n        dst.Set(reflect.MakeSlice(dst.Type(), src.Len(), src.Cap()))\n        for i := 0; i \u003C src.Len(); i++ {\n            copyRecursive(src.Index(i), dst.Index(i))\n        }\n    case reflect.Map:\n        if src.IsNil() {\n            return\n        }\n        dst.Set(reflect.MakeMap(dst.Type()))\n        for _, key := range src.MapKeys() {\n            value := src.MapIndex(key)\n            copyValue := reflect.New(dst.Type().Elem()).Elem()\n            copyRecursive(value, copyValue)\n            copyKey := Copy(key.Interface(), reflect.New(dst.Type().Key()).Elem().Interface())\n            dst.SetMapIndex(reflect.ValueOf(copyKey), copyValue)\n        }\n    default:\n        \u002F\u002F 源类型是基础类型\n        \u002F\u002F 类型不一致但底层类型一致的基本类型，需要强转\n        if dst.Kind() == reflect.Ptr {\n            \u002F\u002F 目标类型是指针而源类型不是指针\n            copyValue := reflect.New(dst.Type().Elem()).Elem()\n            copyRecursive(src, copyValue)\n            dst.Set(copyValue.Addr())\n            return\n        }\n        dst.Set(src.Convert(dst.Type()))\n    }\n}\n","go","",[32,33,28],"code",{"__ignoreMap":30},[11,35,36],{},"中心となるのはcopyRecursiveです。構造体、Slice、Mapのディープコピーに対応し、ポインター型から非ポインター型、その逆のコピーもできます。唯一の条件は、構造体をコピーするとき、コピー先の全フィールドに対して、同名かつ基になる型が同じフィールドがコピー元に存在することです。これで再帰的なディープコピーができます。",[11,38,39],{},"コードの仕組みを細かく説明するつもりはありませんが、これだけは言わせてください。",[11,41,42],{},"リフレクション、マジですごい。",[44,45],"hr",{},[11,47,48],{},"20220820 update：コピー先に、コピー元にはないフィールドが含まれる場合にも対応しました。一致するフィールドがない場合、コピー先のフィールドにはゼロ値を設定します。ポインターならnil、構造体なら空の構造体です。",{"title":30,"searchDepth":50,"depth":50,"links":51},4,[],[53],"fiddling","2022-08-15 01:05:01","システムのリファクタリングで、階層ごとのエンティティ変換に苦労しました。ビュー層の商品VO、ドメイン層のentity、永続化層のPOは似ていても、型のわずかな違いで直接変換できず、ディープコピーが面倒になります。そこでリフレクションを使った汎用的な変換処理を実装し、大量のassemblerメソッドを減らして、保守性と柔軟性を高めることにしました。",false,"md","https:\u002F\u002Fblog-img.774352199.xyz\u002FBlhm0I.webp",{"slots":60},{},true,"\u002Ffiddling\u002Fgolang-deepcopy-between-different-type",null,{"text":65,"minutes":66,"time":67,"words":68},"5 min read",4.14,248400,828,{"title":5,"description":55},"レイヤー間のエンティティ変換をGoのリフレクションで実装。異なる構造体、Slice、Map、ポインターを再帰的にコピーし、コピー元にないフィールドにはゼロ値を設定します。",{"loc":62},"posts\u002Ffiddling\u002Fgolang-deepcopy-between-different-type",[74,75,76,77],"Go","リフレクション","ディープコピー","構造体変換","tech","uNZua2VuX5LRanZEhVxJ6Ph1f51lgXLGErMbJZVfqEg",[81,86],{"title":82,"path":83,"stem":84,"date":85,"type":78,"children":-1},"Javaのthisキーワードでコンパイル時の定数伝播が効かなくなる問題","\u002Ffiddling\u002Fthis-in-javac-string-concat","posts\u002Ffiddling\u002Fthis-in-javac-string-concat","2022-04-16 00:01:28",{"title":87,"path":88,"stem":89,"date":90,"type":78,"children":-1},"失敗に終わったプロジェクト：楽しくなかった春節の7日間","\u002Ffiddling\u002Fgo-os","posts\u002Ffiddling\u002Fgo-os","2023-02-02 23:24:55",1789914057764]