shiimaxx's blog

最も愛を大切に

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形式の値をとって、それをKeyValueをそれぞれフィールドとしてもつ構造体にバインドするコマンドラインフラグを実装してみます。

以下がソースコードです。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 型の変数であるtagfs.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構造体を定義しました。
TagUnmarshalerインターフェースを実装しています。入力値の検証とレシーバ自身に値をセットするという点は先程のflagの例と変わりませんので、UnmarshalJSON()の処理は、flagのサンプルコードにおけるSet()とほぼ同じになっています。

まとめ

いずれも特定のインターフェースを実装した型を用意して、そのインターフェースのメソッドにカスタマイズした処理を実装するというパターンでした。
カスタマイズしたい部分のみを自分で実装して、その他の処理はライブラリに任せることができるのがよい点ではないかと思います。

​​

参考文献