golangのライブラリでbson(gopkg.in/mgo.v2/bson)もjson(encoding/json)も基本的に同じ(もちろん、出力データフォーマットが構造化テキストかbinaryかという違いはありますが)挙動だろうと思っていましたが、やはり思い込みは良くないですね。
ライブラリの実装短いんだし読めよって話もありますが、それはまた次回
package main
import (
"encoding/json"
"fmt"
"gopkg.in/mgo.v2/bson"
//"labix.org/v2/mgo/bson"
)
type Person struct {
Name string
Metadata map[string]string
}
func main() {
person := Person{Name: "Yuki"}
fmt.Printf("Is person.Metadata null: %s\n", person.Metadata == nil)
bsonData, err := bson.Marshal(&person)
if err != nil {
panic(err)
}
personFromBson := Person{}
err = bson.Unmarshal(bsonData, &personFromBson)
if err != nil {
panic(err)
}
fmt.Printf("Bson: %q\n", bsonData)
fmt.Printf("Is personFromBson.Metadata null: %s\n", personFromBson.Metadata == nil)
jsonData, err := json.Marshal(&person)
if err != nil {
panic(err)
}
personFromJson := Person{}
err = json.Unmarshal(jsonData, &personFromJson)
if err != nil {
panic(err)
}
fmt.Printf("Json: %q\n", jsonData)
fmt.Printf("Is personFromJson.Metadata null: %s\n", personFromJson.Metadata == nil)
}
このプログラムでは、MetadataというattributeをもつPerson structを用意し、Metadataにnilを入れたままjsonとbsonでMarshalしております。でMarshalしたバイナリ, 構造化テキストをUnmarshalしその結果がnilかどうかを調べております
Yuki-no-MacBook-Pro-17795:gobson ukinau$ go run mgobson.go
Is person.Metadata null: %!s(bool=true)
Bson: "#\x00\x00\x00\x02name\x00\x05\x00\x00\x00Yuki\x00\x03metadata\x00\x05\x00\x00\x00\x00\x00"
Is personFromBson.Metadata null: %!s(bool=false)
Json: "{\"Name\":\"Yuki\",\"Metadata\":null}"
Is personFromJson.Metadata null: %!s(bool=true)
上記を見ての通り、
- bson の場合は、nil値を初期化してserialise
- json の場合は、nil値をnullとしてそのままserialise
していることがわかります。
ただ、上記の結果からのみだと、Data -> Marshal -> Unmarshal -> Dataした結果を見てどこかで、nil値がmapで初期化されていることしか確認できないので、Marshalで出力したbsonが本当にnullでなく空のmap相当の値が入っているいるのかわからないです。なので、
bsonで確かに、{} mapが初期化されていて、null値でないことを確かめる
specificationを読んで出力されたbsonを読み解いていこうと思います。
http://bsonspec.org/spec.html
まず、下記が今回出力されたbsonデータです。上の結果よりコピーしてます。一部16進数がUTF-8として文字評価されているので、#やらYukiやらmetadataという文字列が入っていますがこれらはUTF-8の文字列になります。
"#\x00\x00\x00\x02name\x00\x05\x00\x00\x00Yuki\x00\x03metadata\x00\x05\x00\x00\x00\x00\x00"
別記事で全体像を含めた解説をもう少し詳しく書きますが(まぁ短いspecificationをただ、長ったらしく説明するだけですが)、今回注目すべきは後半の下記の記述です
\x03metadata\x00\x05\x00\x00\x00\x00\x00"
まずbsonでは、mapの各キーとバリューのpairをelementと呼びそのelementにもいくつか種類がありelementの種類によって、接頭子に使う値や後続する置くべき値が違います。
\03
ここではmetadataが、\03から始まるので、bsonのelementのspecificationから該当するelementの種類を探すと
"\x03" e_name document Embedded document
この種類に該当することがわかります。
e_nameがキーの名前のUTF-8文字列 + \x00 を示しその後にdocument(golangでmap)が続くと決められているので、documentを示すbinary文字列(リトルエンディアンで最初4byteがdocumentのサイズ、5byte目がdocumentの終了を示す値\x00)が入っています。なので、golang のbson(gopkg.in/mgo.v2/bson) ライブラリはMarshal時に、mapを初期化してbsonデータにしていることがわかります。
bsonというdata formatにそもそもnull値がないんじゃないか?
とすると、次に疑うのがそもそもbsonというデータフォーマットがnull値をサポートしていないんじゃないか?
いえ、そんなことはありません。
"\x0A" e_name Null value
上記、specificationからの引用ですがきちんとnull値というelementのもあるのbsonとしてはnullを指定することはできます。
golangのbson(gopkg.in/mgo.v2/bson)でnull値を含むbsonをdecodeすると?
出力時(Marshal)にnull値を含むbsonを出力しないので、単純な興味として、Unmarshalしてnull値としてloadできるのかなというのを試したいと思います。
Bsonを下記のように変えてみます
"\x1e\x00\x00\x00\x02name\x00\x05\x00\x00\x00Yuki\x00\0Ametadata\x00\x00"
上記の説明の通り、\03から始まるdictionaryを示すelementだったものを\0Aから始まるnullを示すelementに変えて、全体のbyte数が変わったので、一番先頭の\x23から5byte引いて\x1eにしてみました。
これをMarshalして、Metadataがnullかどうかを調べると
package main
import (
"encoding/json"
"fmt"
"gopkg.in/mgo.v2/bson"
//"labix.org/v2/mgo/bson"
)
type Person struct {
Name string
Metadata map[string]string
}
func main() {
bsonData = []byte("\x1e\x00\x00\x00\x02name\x00\x05\x00\x00\x00Yuki\x00\x0Ametadata\x00\x00")
personFromBson := Person{}
err := bson.Unmarshal(bsonData, &personFromBson)
if err != nil {
panic(err)
}
fmt.Printf("Is personFromBson.Metadata null: %s\n", personFromBson.Metadata == nil)
}
結果
Yuki-no-MacBook-Pro-17795:gobson ukinau$ go run mgobson.go
Is personFromBson.Metadata null: %!s(bool=true)
となるので、きちんとnull値をUnmarshalできていることがわかります。
結論と疑問
bsonデータを直接見ても分かる通り、golangのbsonライブラリはnilをnullと解釈せず、各typeで初期化してserialiseするようです。これは意図しているのか、実装上そうなったのかはまだ追えてませんが、個人的な感想としては、jsonライブラリの挙動に合わせていった方がわかりやすいなーと思います。
golangの勉強も兼ねて、bsonのライブラリは今度読んで見たいと思います。