Go の構造体とポインタと omitempty の話

(下記は go 1.3.1 での話で、異なるバージョンだと動作も違う可能性があるので注意)

結論

Go で encoding/xml を使って XML のエンコーディングを行っている時に、

  • ある特定のタグの子要素ないし属性が一つでもある場合は、そのタグを出力する
  • 上記のいずれもない場合は、タグは出力しない

を実現したい場合は、単純に要素のポインタ型を使って omitempty は使わないのが正解

詳細

上記のケースを最初は単純に

package main

import (
	"encoding/xml"
	"fmt"
	"log"
)

type Foo struct {
	Str string
}

type Bar struct {
	Foo Foo `xml:",omitempty"`
}

func main() {
	bar := Bar{} // Foo は空
	b, err := xml.MarshalIndent(bar, "", "  ")
	if err != nil {
		log.Fatalln(err)
	}
	fmt.Println(string(b)) // <Bar></Bar> と出力されてほしい
	// Output:
	// <Bar>
	//   <Foo>
	//     <Str></Str>
	//   </Foo>
	// </Bar>
}

としてみたのだけど、うまくいかなかった。理由は encoding/xml のドキュメントによると

  • a field with a tag including the “omitempty” option is omitted if the field value is empty. The empty values are false, 0, any nil pointer or interface value, and any array, slice, map, or string of length zero.

ということで、空の構造体と比較してはくれないためのよう。ここで対象が nil ポインタなら空と扱う、と書いてあるので、

package main

import (
	"encoding/xml"
	"fmt"
	"log"
)

type Foo struct {
	Str string
}

type Bar struct {
	Foo *Foo `xml:",omitempty"` // omitempty はあってもなくてもよい
}

func main() {
	bar := Bar{} // Foo は nil
	b, err := xml.MarshalIndent(bar, "", "  ")
	if err != nil {
		log.Fatalln(err)
	}
	fmt.Println(string(b)) // <Bar></Bar> と出力されてほしい
	// Output:
	// <Bar></Bar>

	bar.Foo = new(Foo)
	b, err := xml.MarshalIndent(bar, "", "  ")
	if err != nil {
		log.Fatalln(err)
	}
	fmt.Println(string(b))
	// Output:
	// <Bar>
	//   <Foo>
	//     <Str></Str>
	//   </Foo>
	// </Bar>
}

と、 Foo のポインタを構造体に含めるようにしてみるとうまく動く。もちろん要素がある場合も問題なく動く。さらに、この場合は別に omitempty なしでも意図通りの動作になる

ところが、たとえば構造体の要素が int で、

  • int の要素が 0 である場合は、要素があるとみなして出力する(つまり omitempty を無視する)
  • int の要素そのものが存在しない時は出力しない

の場合は、同じようにやっても期待通りにはならない。 int が 0 の場合は omitempty の対象として扱ってしまう

package main

import (
	"encoding/xml"
	"fmt"
	"log"
)

type Foo struct {
	Bar *int `xml:",omitempty"`
}

func main() {
	foo := Foo{}
	b, err := xml.MarshalIndent(foo, "", "  ")
	if err != nil {
		log.Fatalln(err)
	}
	fmt.Println(string(b))
	// Output:
	// <Foo></Foo>

	foo.Bar = new(int)
	*(foo.Bar) = 0
	b, err = xml.MarshalIndent(foo, "", "  ")
	if err != nil {
		log.Fatalln(err)
	}
	fmt.Println(string(b))
	// Output:
	// <Foo></Foo>
}

この場合は、要素の型をポインタ型にしつつ、素直に omitempty を外すか、ポインタのポインタを使うと意図通りの結果が得られるようだ

package main

import (
	"encoding/xml"
	"fmt"
	"log"
)

type Foo struct {
	Bar *int
	Baz **int `xml:",omitempty"`
}

func main() {
	foo := Foo{}
	b, err := xml.MarshalIndent(foo, "", "  ")
	if err != nil {
		log.Fatalln(err)
	}
	fmt.Println(string(b))
	// Output:
	// <Foo></Foo>

	foo.Bar = new(int)
	*(foo.Bar) = 0
	tmp := new(int)
	*tmp = 0
	foo.Baz = &tmp
	b, err = xml.MarshalIndent(foo, "", "  ")
	if err != nil {
		log.Fatalln(err)
	}
	fmt.Println(string(b))
	// Output:
	// <Foo>
	//   <Bar>0</Bar>
	//   <Baz>0</Baz>
	// </Foo>
}

いろいろとややこしい