2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

私の知らない go 言語: 構造体の埋め込み

Last updated at Posted at 2019-10-03

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 inttype 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 のキーの数が変わるのはちょっと意外だった。

最後に

まだ書いてないこともちょっとあるんだけど、今日のところはこれぐらいで。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?