goで文字列ベースのenumの定義はこんな感じが良いかもしれない

文字列ベースのenumの定義で良さそうなコードができたのでメモ(iotaを使った数値がベースのものではない)。

文字列ベースのもの

個人的には、特に速度やメモリーなどのことを気にしない場合には、文字列ベースのもののほうが取り回しやすくて良いと思っている。

以下の実装はfmt.Stringerに対応しているし、JSONからの読み込みにも対応している。

pkg/errorsを使っているが、そろそろxerrorsでも良いかもしれない。個人的には、予期せぬ値でエラーになったときに、どんな値だったのかが明らかになっていてほしい。そんなわけでwrapしている。errors.Cause()越しにbaseのエラー値にアクセスできると便利なこともありそうということでErrInvalidOpTypeを定義している。

以下はAdd, Mul, Subの3つの可能性があるようなenumの定義の例。

package enums

import (
    "encoding/json"
    "fmt"
    "strings"

    "github.com/pkg/errors"
)

type Op string

const (
    OpAdd Op = "Add"
    OpSub    = "Sub"
    OpMul    = "Mul"
)

var ErrInvalidOpType = fmt.Errorf("invalid op type")

func (v Op) String() string {
    return string(v)
}

func (v Op) Valid() error {
    switch v {
    case OpAdd, OpSub, OpMul:
        return nil
    default:
        return errors.Wrapf(ErrInvalidOpType, "get %s", v)
    }
}

func (v *Op) UnmarshalJSON(b []byte) error {
    *v = Op(strings.Trim(string(b), `"`))
    return v.Valid()
}

func ParseOp(s string) (v Op, err error) {
    v = Op(s)
    err = v.Valid()
    return
}

ちなみに、UnmarshalJSON()メソッドを定義しなくても、正しい値だけがやってくる場合には動作はするのだけれど。バリデーションも入れたいので。ここの部分。

func (v *Op) UnmarshalJSON(b []byte) error {
    *v = Op(strings.Trim(string(b), `"`))
    return v.Valid()
}

iotaベースのもの

内部で使うだけのものなら、iotaベースのもので十分だと思う。こういう感じで定義するのが楽かもしれない。

import "fmt"

type Op uint8

const (
    OpAdd Op = iota + 1
    OpSub
    OpMul
)

var (
    _ops = [...]string{"", "Add", "Sub", "Mul"}
)

func (v Op) String() string {
    return _ops[v]
}

(String()の中に直接_ops部分を書いてしまっても良い感じに最適化されたりするんだろうか(調べていない))

参考

参考にしたのはものは以下のもの。IsValid()がerrorを返すのは気持ち悪いので Valid()にした。 ParseOp()strconvに合わせたインターフェイスなのでこういう名前で良いと思う。あるいはMustOpのようなものも欲しくなることがあるかもしれない。

iotaベースのものならuberのものが参考になる。iotaを使ったときのゼロ値を気にしようということで。

gist