排他なフィールドとinvalidなzero valueでハマった話

goで排他なフィールドとinvalidなzero valueではまった。 (短く言うならポインターにするべきところでポインターになっていなかったという話)

排他なフィールド

排他なフィールドを持っているstructと言うのはこういう形。

// T :
type T int

//
const (
    TX = T(0)
    TY = T(1)
)

// A :
type A struct {
    T T
    X X
    Y Y
}

// X :
type X struct {
}

// Y :
type Y struct {
}

Aというstructは、Xに値が入る場合もあるし。Yに値が入る場合もある。 どちらに値が入るかはTの値で決まる。

以下の値は、Xに値が入るもの

main.A{
  T: 0,
  X: main.X{},
  Y: main.Y{},
}

invalidなzero value

invalidなzero valueというのは初期状態で何かの操作を行うとエラーになるようなもののこと。

例えば、bson.Marshalするときに、bson.ObjectIdという型の値を使うことがあるのだけれど。 このbson.ObjectIdのzero valueはinvalid。適切なものにするには bson.NewObjectId() にしないとだめ。

例えばこういう型があったとして

// O :
type O struct {
    ID bson.ObjectId `bson:"_id"`
}

正常な値

正常に初期化した値はbson.Marshalが動く。

   {
        o := O{ID: bson.NewObjectId()}
        fmt.Println("initialized", o)
        b, err := bson.Marshal(o)
        if err != nil {
            log.Fatal("ng", err)
        }
        fmt.Println("ok", b)
    }

大丈夫。

initialized {ObjectIdHex("594c9af90ccee09cd63ea023")}
ok [22 0 0 0 7 95 105 100 0 89 76 154 249 12 206 224 156 214 62 160 35 0]

不正な値

zero valueは不正なのでbson.Marshalはだめ。

   {
        // error is occured
        var o O
        fmt.Println("zero", o)
        b, err := bson.Marshal(o)
        if err != nil {
            log.Fatal(err)
        }
        fmt.Println("ok", b)
    }

だめ。

zero {ObjectIdHex("")}
2017/06/23 13:37:13 ObjectIDs must be exactly 12 bytes long (got 0)

排他なフィールドとinvalidなzero value

排他なフィールドとinvalidなzero valueと言うのは先ほどの2つが組み合わさった場合の話。

先程のAの型(XとYが排他なフィールド)が以下の様に変わった場合に

// A :
type A struct {
    T T
    X X
    Y Y
}

// X :
type X struct {
    ID bson.ObjectId `bson:"_id"`
}

// Y :
type Y struct {
    V int `bson:"v"`
}

Yの値だけが入った値(正常なもののつもり)をbson.Marshalしようとするとエラーになる。 理由は単純でA.Xのzero valueが不正な値なため。

a := A{T: TY, Y: Y{V: 10}} // 正常な値のつもり

// aの値
main.A{
  T: 0,
  X: main.X{
    ID: ObjectIdHex(""), // 不正なzero value
  },
  Y: main.Y{
    V: 10,
  },
}
// ObjectIDs must be exactly 12 bytes long (got 0)

Xはポインターになっている必要がある。