https://github.com/nabetani/marshaljsonf64
を書くに当たって気がついた go と json.Marshal / json.Unmarshal の仕様をいくつか紹介する。
組み込み型を埋め込む
package main
import "fmt"
type t0 struct {
int
}
func main() {
v0 := t0{int: 123} // "int:" で初期化する
//v0 = 456 // エラー
v0.int = 456 // こう書く
fmt.Println(v0) //=> "{456}"
}
type foo int
と type foo struct{ int }
は似ている。
後者を使うべき理由に心当たりはない。
ポインタを埋め込む
package main
type t10 struct {
*int
}
type t11 struct {
**int // "unexpected *, expecting name" というエラーになる
}
type pint *int
type t12 struct {
pint // "embedded type cannot be a pointer" というエラーになる
}
func main() {
vi := 1
v10 := t10{int: &vi} // "int:" で初期化する
*v10.int = 3 // こう書く。「*v10=3」とは書けない
}
t10
が ok で、 t12
がエラーなのは、かなり思いがけなかった。
両方埋め込む
package main
type t20 struct {
int
*int // "duplicate field int" というエラーになる
}
まあそうだよね。
配列・スライス・map を埋め込む
package main
type t30 struct {
[1]int // "unexpected [, expecting field name or embedded type"
}
type t31 struct {
[]int // "unexpected [, expecting field name or embedded type"
}
type t32 struct {
map[int]int // "syntax error: unexpected map, expecting field name or embedded type"
}
type a [1]int
type s []int
type m map[int]int
type t30x struct{ a } // ok
type t31x struct{ s } // ok
type t32x struct{ m } // ok
func main() {
v30 := t30x{a: [1]int{12}}
v30[0] = 123 // "invalid operation: v30[0] (type t30x does not support indexing)"
v30.a[0] = 456
v31 := t31x{s: []int{34}}
v31[0] = 345 // "invalid operation: v31[0] (type t31x does not support indexing)"
v31.s[0] = 678
v32 := t32x{m: map[int]int{56: 78}}
v32[0] = 99 // invalid operation: v32[0] (type t32x does not support indexing)
v32.m[0] = 99
}
そのままでは埋め込めない。アクセスする名前がなくて困るからだと思う。
名前をつけると埋め込める。
indexing はメソッドじゃないのでそのままでは呼べない。
何重にも埋め込む
package main
type t40 struct{ foo int }
type t41 struct{ t40 } // そのまま埋め込む
type t42 struct{ *t41 } // ポインタを埋め込む
func main() {
v40 := t40{1}
v41 := t41{v40}
t42 := t42{&v41}
t42.foo = 3 // 何段階も下れる
}
ポインタ埋め込みとそのまま埋め込みを混在させることができる。
混在していても一気に一番奥のメンバにアクセスできる
自分を埋め込む
ポインタを埋め込めるので、自分自身を埋め込むことができる。
package main
type T50 struct {
*T50
Foo string
}
func main() {
a := T50{T50: nil, Foo: "a"}
b := T50{T50: nil, Foo: "b"}
c := T50{T50: nil, Foo: "c"}
a.T50 = &a
b.T50 = &c
c.T50 = &b
}
有意義な使いみちの心当たりはない。
埋め込みとJSON ― 名前の重複
埋め込み構造体を JSON にすると、そのままフラットに展開される。
じゃあ、同じメンバ名を持つ構造体を埋め込んだらどうなるか。
- 実行時エラーになる(好ましい)
- 最初に見つかった方だけが出る(まあまあ酷い仕様)
- 同名のキーで出てきてしまう(かなり酷い仕様)
のいずれかだと予想したんだけど、予想は外れた。
package main
import (
"encoding/json"
"fmt"
)
type t70 struct {
Foo string
Bar string
}
type t71 struct {
Foo string
Baz string
}
type t72 struct {
t70
t71
}
type t70x struct {
t70
}
type t71x struct {
t71
}
type t74 struct {
t70x
t71
}
func asjson(i interface{}) {
j, e := json.Marshal(i)
if e != nil {
fmt.Println("err:", e)
return
}
fmt.Println("json:", string(j))
}
func main() {
v70 := t70{"70.foo", "70.bar"}
v71 := t71{"71.foo", "71.baz"}
asjson(v70)
//=> json: {"Foo":"70.foo","Bar":"70.bar"}
asjson(v71)
//=> json: {"Foo":"71.foo","Baz":"71.baz"}
asjson(t72{v70, v71})
//=> json: {"Bar":"70.bar","Baz":"71.baz"}
asjson(t74{t70x{v70}, v71})
//=> json: {"Bar":"70.bar","Foo":"71.foo","Baz":"71.baz"}
}
正解は、
- 同名のフィールドがある場合はそのフィールドは無視される(かなり酷い仕様)
だった。しかも t74
の例のとおり
- ただし、深さが違う場合は浅い方優先
だとおもう。
びっくりした。
ちなみに。
- Unmarshal でも無視される。
- 一方に
json:"Foo"
を付与すると、そっちが勝って、ついてないほうが無視される。 - 両方に
json:"Foo"
を付与すると、両方無視される。
埋め込みとJSON ― 名前がつく場合とつかない場合
package main
import (
"encoding/json"
"fmt"
)
type T80 []int
type T81 struct{ Foo int }
type T82 struct{ Bar int }
type T83 struct{ Baz int }
type T84 T83
type T85 struct {
T80
T81
T82 `json:"T82"`
T84
}
func asjson(i interface{}) {
j, e := json.Marshal(i)
if e != nil {
fmt.Println("err:", e)
return
}
fmt.Println("json:", string(j))
}
func main() {
v := T85{
T80: T80{1, 2},
T81: T81{34},
T82: T82{56},
T84: T84{78},
}
asjson(v)
//=> json: {"T80":[1,2],"Foo":34,"T82":{"Bar":56},"Baz":78}
}
int や スライスのような型を埋め込む場合と、構造体を埋め込む場合で動作が違う。
- int や スライスのような型は、型名がキー名になる
-
json:"hoge"
のようなものがついていれば、そのキー名がつく - それ以外なら、親構造体のメンバにそのまま展開される
となる。
埋め込みとJSON ― ポインタを埋め込む場合
package main
import (
"encoding/json"
"fmt"
"strings"
)
type T90 struct {
Foo int
Bar int
}
type T91 struct {
Baz int
Qux int
}
type T92 struct {
*T90
*T91
}
func asjson(i interface{}) {
j, e := json.Marshal(i)
if e != nil {
fmt.Println("err:", e)
return
}
fmt.Println("json:", string(j))
}
func t92FromJSON(j string) {
v := T92{}
e := json.Unmarshal([]byte(j), &v)
if e != nil {
fmt.Println("err:", e)
return
}
fmt.Println(strings.Replace(fmt.Sprintf("val: %_", v), "%!_", "", -1))
}
func main() {
asjson(T92{T90: &T90{12, 34}, T91: nil})
//=> json: {"Foo":12,"Bar":34}
asjson(T92{T90: &T90{12, 34}, T91: &T91{56, 78}})
//=> json: {"Foo":12,"Bar":34,"Baz":56,"Qux":78}
t92FromJSON(`{"Foo":12}`)
//=> val: {(*main.T90=&{12 0}) (*main.T91=<nil>)}
t92FromJSON(`{"Foo":12,"Baz":34}`)
//=> val: {(*main.T90=&{12 0}) (*main.T91=&{34 0})}
}
- 埋め込まれているポインタが nil だと、そのフィールドは Marshal されない。
- Unmarshal 時には、必要に応じてオブジェクトが作られる。
ということになっているようだ。
埋め込まれているオブジェクトが nil かどうかで JSON のキーの数が変わるのはちょっと意外だった。
最後に
まだ書いてないこともちょっとあるんだけど、今日のところはこれぐらいで。