Goにおけるライブラリのカスタマイズ性の活用
Go5 Advent Calendar 2019 12日目のエントリです。
ライブラリにはカスタマイズ可能なインターフェースが提供されている場合があります。それを活用することで、ライブラリが提供している標準の関数では実現できない機能や処理を実装することができます。
本エントリでは、Goの標準ライブラリであるflag
パッケージとencoding/json
パッケージを例にして紹介します。
flag
flag
パッケージではflag.Var()
によって、値のパース処理と変数へのバインド処理をカスタマイズしたコマンドラインフラグを定義することができます。
文字列や数値などを対応するGoの型の変数にバインドするだけであれば、string型の変数にバインドするflag.String()
、int型の変数にバインドするflag.Int()
などのflag
パッケージで提供されている関数を使えばよいのですが、スライスや独自型の変数にバインドしたい場合は、flag.Var()
を使う必要があります。
flag.Var()
は、第1引数にflag.Value
インターフェースを実装した独自型の値を渡します。flag.Value
インターフェースは次のように定義されています。
type Value interface { String() string Set(string) error }
https://golang.org/pkg/flag/#Value
ポイントはSet()
です。Set()
はflag.Parse()
を呼び出したときに内部的に呼び出され、コマンドフラグで渡された値を入力として、それをレシーバ自身にセットする関数です。ここにカスタマイズした処理を実装していきます。
ひとつ例を挙げます。-tag key:val
のように:
区切りのKey-Value形式の値をとって、それをKey
、Value
をそれぞれフィールドとしてもつ構造体にバインドするコマンドラインフラグを実装してみます。
以下がソースコードです。Go Playground上で動作確認できるように、flag.FlagSet
を利用している点に注意してください。
package main import ( "errors" "flag" "fmt" "strings" ) type Tag struct { Key string Value string } func (t *Tag) String() string { return fmt.Sprintf("%s:%s", t.Key, t.Value) } func (t *Tag) Set(val string) error { if !strings.Contains(val, ":") { return errors.New("':' must be contain") } kv := strings.Split(val, ":") if kv[1] == "" || len(kv) != 2 { return errors.New("must be specified in 'key:value' format") } t.Key, t.Value = kv[0], kv[1] return nil } func main() { var tag Tag fs := flag.NewFlagSet("ExampleValue", flag.ExitOnError) fs.Var(&tag, "tag", "tag") fs.Parse([]string{"-tag", "hoge:fuga"}) fmt.Printf("Key: %s, Value: %s\n", tag.Key, tag.Value) }
https://play.golang.org/p/dCwpA8zFxVt
-tag
フラグで渡された値をバインドするためのフラグ型としてTag
構造体を定義しました。これは、flag.Value
インターフェースを実装しています。
Set()
では入力値の検証を行い、問題ない場合はレシーバ自身にその値をセットします。入力値の検証では、「:
が含まれているか」、「hoge::fuga
のような不正な形式になっていないか」をチェックしています。
Tag
型の変数であるtag
をfs.Var()
の第1引数に渡してコマンドラインフラグを定義しています。このあと、fs.Parse()
を呼び出したタイミングで、内部的にTag
構造体のSet()
が呼び出されます。ここでエラーが返された場合は、コマンドラインフラグのパースエラーとして次のようなエラーが表示されます。
invalid value "hoge::fuga" for flag -tag: must be specified in 'key:value' format
encoding/json
encoding/json
パッケージには、Goの値をJSONデータにするjson.Marshal()
とJSONデータをGoの値として取り扱うためのjson.Unmarshal()
があります。
ここではjson.Unmarshal()
に着目して、json.Unmarshaler
インターフェースを実装した型を利用することで、JSONデータからGoへの変換処理をカスタマイズする例を紹介します。
先程と同様に値がkey:val
形式であるJSONのフィールド(Strings)を、Goの独自型にバインドするとします。
json.Unmarshal()
は、第2引数にinterface()
を渡すことができます。これがJSONデータをバインドする先の変数になります。
json.Unmarshal()
は第2引数の値がjson.Unmarshaler
インターフェースを実装している場合にUnmarshalJSON()
を呼び出します。UnmarshalJSON()
で任意のJSONデータの入力を受けて自身の型に変換するように実装することで、先程のflag
パッケージの例と同じように独自型の変数へのバインドができます。
以下がソースコードです。
package main import ( "encoding/json" "errors" "fmt" "log" "strings" ) type Tag struct { Key string Value string } func (t *Tag) UnmarshalJSON(b []byte) error { val := strings.Trim(string(b), `"`) if !strings.Contains(val, ":") { return errors.New("':' must be contain") } kv := strings.Split(val, ":") if kv[1] == "" || len(kv) != 2 { return errors.New("must be specified in 'key:value' format") } t.Key, t.Value = kv[0], kv[1] return nil } type Obj struct { Name string `json:"name"` CustomTag Tag `json:"custom_tag"` } func main() { blob := `{"name": "dummy object", "custom_tag": "hoge:fuga"}` var obj Obj if err := json.Unmarshal([]byte(blob), &obj); err != nil { log.Fatal("error: ", err) } fmt.Printf("Key: %s, Value: %s\n", obj.CustomTag.Key, obj.CustomTag.Value) }
https://play.golang.org/p/R-o_LBY6HHf
JSONデータのkey:val
形式のフィールドをバインドする先の型としてTag
構造体を定義しました。
Tag
はUnmarshaler
インターフェースを実装しています。入力値の検証とレシーバ自身に値をセットするという点は先程のflag
の例と変わりませんので、UnmarshalJSON()
の処理は、flag
のサンプルコードにおけるSet()
とほぼ同じになっています。
まとめ
いずれも特定のインターフェースを実装した型を用意して、そのインターフェースのメソッドにカスタマイズした処理を実装するというパターンでした。
カスタマイズしたい部分のみを自分で実装して、その他の処理はライブラリに任せることができるのがよい点ではないかと思います。