この記事はTech KAYAC Advent Calendar 2019の3日目の記事です。
新卒サーバサイドエンジニアの宮村 紅葉です! 普段はGoでゲームサーバをゴリゴリ書いています。
はじめに
GoにはEmbeddingと呼ばれる機能があります(日本語では「埋め込み」と書かれることが多いと思いますが、この記事ではEmbeddingと記述します)。このEmbeddingは便利ですが「複数の構造体を使ってEmbeddingした際に同名のフィールドが存在する」と思わぬ罠にハマります(私は最近ハマった笑)。ハマったからにはネタにせねば!ということで書いていきます!!
Embedding
同名フィールドを明示的に参照
まずEmbeddingして埋め込んだ構造体のフィールドを参照してみましょう。以降は S1
S2
Embedding
構造体を前提に説明します。なお説明のためにあえて DuplicatedKey
という同名のフィールドを S1
S2
に持たせて、構造体 Embedding
にEmbeddingしています(ダジャレみたいになってしまった笑)。
以下のように参照すると、問題なく参照できていますね。 同名フィールドである DuplicatedKey
フィールドを参照する場合でも、 S1
S2
どちらのものかを「明示的に」指定すれば参照できます。
https://play.golang.org/p/9FqLGVPEm-L
type S1 struct { DuplicatedKey string V1 int } type S2 struct { DuplicatedKey string V2 int } type Embedding struct { *S1 *S2 } func main() { s1 := &S1{ DuplicatedKey: "s1key", V1: 1, } s2 := &S2{ DuplicatedKey: "s2key", V2: 2, } e := &Embedding{ s1, s2, } fmt.Println("---s1を明示的に指定---") fmt.Printf("e.S1: %+v\n", e.S1) fmt.Printf("e.S1.V1: %+v\n", e.S1.V1) fmt.Printf("e.S1.duplicatedKey: %+v\n", e.S1.DuplicatedKey) fmt.Println("---s2を明示的に指定---") fmt.Printf("e.S2: %+v\n", e.S2) fmt.Printf("e.S2.V2: %+v\n", e.S2.V2) fmt.Printf("e.S2.DuplicatedKey: %+v\n", e.S2.DuplicatedKey) fmt.Println("---明示的に指定しない---") fmt.Printf("e.V1: %+v\n", e.V1) fmt.Printf("e.V2: %+v\n", e.V2) }
---s1を明示的に指定--- e.S1: &{DuplicatedKey:s1key V1:1} e.S1.V1: 1 e.S1.duplicatedKey: s1key ---s2を明示的に指定--- e.S2: &{DuplicatedKey:s2key V2:2} e.S2.V2: 2 e.S2.DuplicatedKey: s2key ---明示的に指定しない--- e.V1: 1 e.V2: 2
同名フィールドを明示的に参照しないとどうなるか
では先ほどの例に対して、以下のように書いてみたらどうなるでしょうか?
https://play.golang.org/p/WNaS_wYmB77
fmt.Println("---名前が衝突しているフィールド---") fmt.Printf("e.DuplicatedKey: %+v\n", e.DuplicatedKey)
結果はエラーになります。
./prog.go:37:40: ambiguous selector e.DuplicatedKey
考えてみれば当たり前で、プログラム側からすると「どっちの DuplicatedKey
フィールドを呼び出したいの?」となるわけです。
公式ドキュメントをみてみる
ちなみにGoの公式でも、同名フィールドが存在する場合の Embedding
の挙動について書かれています
Effective Go - The Go Programming Language
Embedding types introduces the problem of name conflicts but the rules to resolve them are simple. First, a field or method X hides any other item X in a more deeply nested part of the type. If log.Logger contained a field or method called Command, the Command field of Job would dominate it.
Second, if the same name appears at the same nesting level, it is usually an error; it would be erroneous to embed log.Logger if the Job struct contained another field or method called Logger. However, if the duplicate name is never mentioned in the program outside the type definition, it is OK. This qualification provides some protection against changes made to types embedded from outside; there is no problem if a field is added that conflicts with another field in another subtype if neither field is ever used.
同名フィールドのネストが異なる場合はよりネストが深い方のフィールドは無視されるみたいです。そして(こちらが今回の場合ですが)同名フィールドのネストが同一である場合、参照時にエラーになるとのこと(これは先ほど確認した挙動と同じですね)。しかし However, if the duplicate name is never mentioned in the program outside the type definition, it is OK
と書かれているように「参照されない場合はエラーにならない」という挙動のようです(最初の例でも参照しなければエラーになりませんでしたね)。
json.Marshal するとどうなるか
「参照しなければエラーにならないのだから何も問題ないのでは?」と思われた方も多いと思います。同名フィールドを参照してしまってもエラーになるので気づくだろうと思いますよね?自分が参照するならば・・・ですが。
さて、先ほどの構造体を json.Marshal
するとこうなります。
https://play.golang.org/p/DRQBmBTzrom
fmt.Printf("e.S1: %+v\n", e.S1) fmt.Printf("e.S2: %+v\n", e.S2) fmt.Println("---json.Marshal -> DuplicatedKeyが消える---") b, err := json.Marshal(e) if err != nil { log.Println(err) return } fmt.Printf("json.Marshal(e):%s\n", b) fmt.Println("---json.Unmarshal -> 消えたまま---") e2 := &Embedding{} if err := json.Unmarshal(b, e2); err != nil { log.Println(err) return } fmt.Printf("e2.S1:%+v\n", e2.S1) fmt.Printf("e2.S2:%+v\n", e2.S2)
e.S1: &{DuplicatedKey:s1key V1:1} e.S2: &{DuplicatedKey:s2key V2:2} ---json.Marshal -> DuplicatedKeyが消える--- json.Marshal(e):{"V1":1,"V2":2} ---json.Unmarshal -> 消えたまま--- e2.S1:&{DuplicatedKey: V1:1} e2.S2:&{DuplicatedKey: V2:2}
おわかりいただけただろうか・・・。そうですね。 json.Marshal
した時点で同名のフィールドである DuplicatedKey
の情報がゴッソリ抜け落ちています。おそらく json.Marshal
時に同名フィールドを参照して、エラーになった際にそのフィールドを無視する挙動になっているのではないかと思います。しかも json.Marshal
メソッド自体は エラーを返していません。テストで気づくだろうと思われるかもしれませんが、「構造体を json.Marshal
した後に json.Unmarshal
して元に戻るかのテスト」ってあまり書かないと思います。
対策
S1
S2
にタグをつける
何かしら区別できればいいので、json:""
タグを使って json.Marshal
メソッドが識別できるようにしてあげれば良さそうです。試してみるとうまくいっていますね。
https://play.golang.org/p/llq-zwdfWqw
type Embedding struct { *S1 `json:"s1"` // タグをつける *S2 `json:"s2"` // タグをつける }
fmt.Printf("e.S1: %+v\n", e.S1) fmt.Printf("e.S2: %+v\n", e.S2) fmt.Println("---json.Marshal---") b, err := json.Marshal(e) if err != nil { log.Println(err) return } fmt.Printf("json.Marshal(e):%s\n", b) fmt.Println("---json.Unmarshal---") e2 := &Embedding{} if err := json.Unmarshal(b, e2); err != nil { log.Println(err) return } fmt.Printf("e2.S1:%+v\n", e2.S1) fmt.Printf("e2.S2:%+v\n", e2.S2)
e.S1: &{DuplicatedKey:s1key V1:1} e.S2: &{DuplicatedKey:s2key V2:2} ---json.Marshal--- json.Marshal(e):{"s1":{"DuplicatedKey":"s1key","V1":1},"s2":{"DuplicatedKey":"s2key","V2":2}} ---json.Unmarshal--- e2.S1:&{DuplicatedKey:s1key V1:1} e2.S2:&{DuplicatedKey:s2key V2:2}
json.Marshal(e):{"s1":{"DuplicatedKey":"s1key","V1":1},"s2":{"DuplicatedKey":"s2key","V2":2}}
という風に Marshal してくれていて、 DuplicatedKey
が消えずに残っていますね!
DuplicatedKey
に別名でタグをつける
S1
S2
にタグをつけるとjsonの構造が変化します。すでに運用しているコードで「これだと困る! 」という場合には DuplicatedKey
にそれぞれ「別の名前」でタグをつけてあげるという手があります。
https://play.golang.org/p/I1iVoS6ZjwJ
type S1 struct { DuplicatedKey string `json:"duplicatedKey1"` // タグ名を変える V1 int `json:"v1"` } type S2 struct { DuplicatedKey string `json:"duplicatedKey2"` // タグ名を変える V2 int `json:"v2"` }
fmt.Printf("e.S1: %+v\n", e.S1) fmt.Printf("e.S2: %+v\n", e.S2) fmt.Println("---json.Marshal---") b, err := json.Marshal(e) if err != nil { log.Println(err) return } fmt.Printf("json.Marshal(e):%s\n", b) fmt.Println("---json.Unmarshal---") e2 := &Embedding{} if err := json.Unmarshal(b, e2); err != nil { log.Println(err) return } fmt.Printf("e2.S1:%+v\n", e2.S1) fmt.Printf("e2.S2:%+v\n", e2.S2)
e.S1: &{DuplicatedKey:s1key V1:1} e.S2: &{DuplicatedKey:s2key V2:2} ---json.Marshal--- json.Marshal(e):{"duplicatedKey1":"s1key","v1":1,"duplicatedKey2":"s2key","v2":2} ---json.Unmarshal--- e2.S1:&{DuplicatedKey:s1key V1:1} e2.S2:&{DuplicatedKey:s2key V2:2}
S1
に json:"duplicatedKey1"
、 S2
に json:"duplicatedKey2"
と別名のタグをつけました。こうすると json.Marshal(e):{"duplicatedKey1":"s1key","v1":1,"duplicatedKey2":"s2key","v2":2}
となって、jsonの構造を崩すことなく対処することができるのでオススメです。ただし同じ名前のタグをつけても意味がないので気をつけましょう(自動生成してるとやりがち)。
おわりに
まとめると「同名フィールドを持つ構造体をEmbeddingすると json.Marshal
時に消える。対策としては適切にタグをつけること。」となります。みなさんも2019年締めくくる前に、プロジェクトでEmbeddingを行なっている箇所を見直してみると、事故の原因を未然に防いで安心して新年を迎えられるかもしれません笑
ちなみに今回は確認していませんが、他の Marshal
系メソッドでも同様の挙動になるかもしれないので気になる方は試してみてくださいね。
私の記事は以上です!テックブログ初めて書いたのでドキドキですが楽しんでもらえたらとっても嬉しいです!
次回は(次回も?笑)テックブログでおなじみ!平山さんの記事です。こちらもよろしくお願いします!