Go to list of users who liked
Share on X(Twitter)
Share on Facebook
More than 5 years have passed since last update.
こちらはGo 5 Advent Calendar 2020の穴埋め記事です。
こんにちは、うえちょこ(uechoco)です。
DeNAから事業統合で株式会社Mobility Technologiesに転籍しまして、バックエンドエンジニアをしております。
前回も書きましたが、ええ、あの「GO」という名前のタクシー配車アプリを「Go」で作っている会社です 。
今回も業務で遭遇したネタです。
お題
任意の構造体を受け取って、json.Marshal() してログに出力する既存実装がありました。
funcoutputLog(eventIDstring,payloadinterface{})error{b,err:=json.Marshal(payload)iferr!=nil{returnerr}// ログ出力する(例)fmt.Printf("event:%s payload:%s",eventID,string(b))returnnil}引数のpayload は任意の構造体を指定できるので、User{...} とかProduct{...} とかOrder{...} とかなんでも指定することができました。
ところが、新しい機能が追加されるときに、今後はすべてのPayloadのJSONに一律で属性情報を含めてほしいと言われてしまいました。こんなイメージです:
- Before:
{"name":"田中","age":16}{"name":"特製味噌ラーメン","price":780}{"name":"さっきもらった指輪","paid_at":"2020-12-24T13:31:28Z"}
- After:
{"name":"田中","age":16,"_attribute":"v2"}{"name":"特製味噌ラーメン","price":780,"_attribute":"v2"}{"name":"さっきもらった指輪","paid_at":"2020-12-24T13:31:28Z","_attribute":"v2"}
最初はAttributedUserAttributedProduct といった構造体を新たに定義して値を入れようか、、、と思っていたのですが、「reflect使えば元の構造体にフィールド変数が1つ増えた無名の構造体をその場で作って値を詰めて返せるのでは?」と思い立って、作ってみました。
packagemainimport("encoding/json""fmt""reflect")typeEmptyLogPayloadstruct{UnderscoreAttributestring`json:"_attribute"`}funcNewEmptyLogPayload()EmptyLogPayload{returnEmptyLogPayload{UnderscoreAttribute:"v2",}}funcWrapLogPayload(srcinterface{})interface{}{ifsrc==nil{returnNewEmptyLogPayload()}fv:=reflect.ValueOf(src)ft:=fv.Type()iffv.Kind()==reflect.Ptr{iffv.IsNil(){returnNewEmptyLogPayload()}ft=ft.Elem()fv=fv.Elem()}switchft.Kind(){casereflect.Struct:returnforceInsertField(fv,ft)}panic(fmt.Sprintf("unsupported reflect Kind:%s",ft.Kind().String()))}funcforceInsertField(fvreflect.Value,ftreflect.Type)interface{}{// 構造体のフィールドの収集ep:=NewEmptyLogPayload()epRt:=reflect.TypeOf(ep)epNum:=epRt.NumField()num:=ft.NumField()structFields:=make([]reflect.StructField,0,num+epNum)fori:=0;i<num;i++{structFields=append(structFields,ft.Field(i))}fori:=0;i<epNum;i++{structFields=append(structFields,epRt.Field(i))}// 構造体型の生成newType:=reflect.StructOf(structFields)// 構造体の生成rv:=reflect.New(newType).Elem()// 値の移植transplantFields:=func(srcTypereflect.Type,fieldNumint,srcValuereflect.Value,destValuereflect.Value){fori:=0;i<fieldNum;i++{field:=srcType.Field(i)if!field.Anonymous{name:=field.NamesrcField:=srcValue.FieldByName(name)dstField:=destValue.FieldByName(name)ifsrcField.IsValid()&&dstField.IsValid(){ifsrcField.Type()==dstField.Type(){dstField.Set(srcField)}}}}}transplantFields(ft,num,fv,rv)transplantFields(epRt,epNum,reflect.ValueOf(ep),rv)returnrv.Interface()}処理の流れ
WrapLogPayload 関数では、元のpayloadが構造体であることをチェックします(今回は構造体以外を扱わない)。ポインタの場合はポインタの中身が構造体であることをチェックします。nilだけは例外的に空の構造体という扱いでOKしました。
フィールド変数を1つ増やす処理はforceInsertField 関数にあります。
ここでは元の構造体のreflect.StructFieldと増やしたいフィールド変数を内包した構造体のreflect.StructFieldを合体させます。その名の通り構造体のフィールド変数の情報がここに含まれています。タグ情報も含まれているので、まるごとコピーすることでJSON変換時のキー名も引き継がれます。
そしてreflect.StructOf(structFields) を呼び出すと新しい構造体型が出来上がります。その新しい構造体型の変数を作るのがreflect.New() です。
このままではゼロ値のままですので、元の構造体から値をコピーします。transplantFields 内部関数の部分です。フィールド変数名経由でフィールドを特定して値をコピーしていますが、もしかしたらもっといいやり方があるかもしれません。
実行サンプル
これを使ったサンプル実装がこちらです:https://play.golang.org/p/X3spg0AsPZk
funcmain(){typeUserstruct{Namestring`json:"name"`Ageint`json:"age"`}fmt.Printf("User: %+v\n",WrapLogPayload(User{Name:"田中",Age:16}))typeProductstruct{Namestring`json:"name"`Priceint`json:"price"`}fmt.Printf("Product: %+v\n",WrapLogPayload(&Product{Name:"特製味噌ラーメン",Price:780}))fmt.Printf("nil: %+v\n",WrapLogPayload(nil))b,_:=json.Marshal(WrapLogPayload(User{Name:"田中",Age:16}))fmt.Printf("json: %s\n",string(b))b,_=json.Marshal(WrapLogPayload(&Product{Name:"特製味噌ラーメン",Price:780}))fmt.Printf("json: %s\n",string(b))b,_=json.Marshal(WrapLogPayload(nil))fmt.Printf("json: %s\n",string(b))}出力結果
User: {Name:田中 Age:16 UnderscoreAttribute:v2}Product: {Name:特製味噌ラーメン Price:780 UnderscoreAttribute:v2}nil: {UnderscoreAttribute:v2}json: {"name":"田中","age":16,"_attribute":"v2"}json: {"name":"特製味噌ラーメン","price":780,"_attribute":"v2"}json: {"_attribute":"v2"}どの異なる構造体を渡しても、ちゃんとフィールド変数が1つ増えていますね。
Register as a new user and use Qiita more conveniently
- You get articles that match your needs
- You can efficiently read back useful information
- You can use dark theme