【Go】structをJSONにする時、ゼロ値を含む/含まないを切り替える方法と注意点 #golang
inabaです。
Go言語でstructをJSONにする時、ゼロ値を含まない場合はタグにomitempty
をつけますよね。
type stampCard struct { Stamp int `json:"stamp,omitempty"` }
しかし、同じstructを使っている時にゼロ値を含む/含まないを切り替えたい時があります。
具体的には下記のようなstructです。
type stampCard struct { InStampRally bool `json:"in_stamp_rally"` Stamp int `json:"stamp,omitempty"` }
上記のstructではInStampRally
がtrueの時だけStamp
の値をJSONに含めたいです。
しかし、このstructのままだとInStampRally
がtrueでもStamp
が0なら含まれません。
package main import ( "encoding/json" "fmt" log "github.com/Sirupsen/logrus" ) type stampCard struct { InStampRally bool `json:"in_stamp_rally"` Stamp int `json:"stamp,omitempty"` } func main() { sc := stampCard{InStampRally: true, Stamp: 0} jsc, err := json.Marshal(pc) if err != nil { log.Error(err) } fmt.Println(string(jsc)) // {"in_stamp_rally":true} }
解決策
解決策はゼロ値を含む/含まないを切り替えたい値をポインタ化します。
type stampCard struct { InStampRally bool `json:"in_stamp_rally"` Stamp *int `json:"stamp,omitempty"` }
これで
という切り替えができます。
package main import ( "encoding/json" "fmt" log "github.com/Sirupsen/logrus" ) type stampCard struct { InStampRally bool `json:"in_stamp_rally"` Stamp *int `json:"stamp,omitempty"` } func main() { scs := []stampCard{ {InStampRally: false}, {InStampRally: true}, } for i, sc := range scs { if sc.InStampRally { scs[i].Stamp = intPointer(0) } } jscs, err := json.Marshal(scs) if err != nil { log.Error(err) } fmt.Println(string(jscs)) // [{"in_stamp_rally":false},{"in_stamp_rally":true,"stamp":0}] } func intPointer(v int) *int { return &v }
上記サンプルコードではstampCard
のスライスをループさせてInStampRally
がtrueなら値が0のintポインターをStamp
に代入しています。
こうすることで下記のようにomitempty
が指定されていてもゼロ値をJSONに出力することができます。
[ { "in_stamp_rally": false }, { "in_stamp_rally": true, "stamp": 0 } ]
注意点
rangeループのvalueからポインターを取ると、ループでvalueの値が変わってもポインターの値は同じなので、注意しないと意図しない値が入ってしまいます。
(ポインターの値が同じ理由は、ループのたびに新しいメモリを確保しないためでしょう。)
DBから取得した値をループさせてJSON化する時等、注意が必要です。
例として、下記は順番に0, 1, 2, 3
のスタンプ数のスタンプカードができる事を期待しているコードです。
package main import ( "encoding/json" "fmt" log "github.com/Sirupsen/logrus" ) type stampCard struct { InStampRally bool `json:"in_stamp_rally"` Stamp *int `json:"stamp,omitempty"` } func main() { stamps := []int{0, 1, 2, 3} var scs []stampCard for _, stamp := range stamps { log.Infof("%p", &stamp) sc := stampCard{InStampRally: true, Stamp: &stamp} scs = append(scs, sc) } jscs, err := json.Marshal(scs) if err != nil { log.Error(err) } fmt.Println(string(jscs)) }
しかし、上記の結果は下記になります。
$ go run main.go | jq INFO[0000] 0xc42000a3d0 INFO[0000] 0xc42000a3d0 INFO[0000] 0xc42000a3d0 INFO[0000] 0xc42000a3d0 [ { "in_stamp_rally": true, "stamp": 3 }, { "in_stamp_rally": true, "stamp": 3 }, { "in_stamp_rally": true, "stamp": 3 }, { "in_stamp_rally": true, "stamp": 3 } ]
まず、ループ中のvalueのポインター値を表示していますが、すべて同じになっています。
その後のJSONは全てのstamp
が3になってしまいました。
全ての値が最後の値に書き換わってしまっています。
修正後のコード
ループのvalueを一旦新しい変数に入れてその変数のポインターを取れば正確な値をJSONに出力できます。
package main import ( "encoding/json" "fmt" log "github.com/Sirupsen/logrus" ) type stampCard struct { InStampRally bool `json:"in_stamp_rally"` Stamp *int `json:"stamp,omitempty"` } func main() { stamps := []int{0, 1, 2, 3} var scs []stampCard for _, stamp := range stamps { tmp := stamp // important!! sc := stampCard{InStampRally: true, Stamp: &tmp} scs = append(scs, sc) } jscs, err := json.Marshal(scs) if err != nil { log.Error(err) } fmt.Println(string(jscs)) }
結果:
[ { "in_stamp_rally": true, "stamp": 0 }, { "in_stamp_rally": true, "stamp": 1 }, { "in_stamp_rally": true, "stamp": 2 }, { "in_stamp_rally": true, "stamp": 3 } ]
今日はここまで。