設定ファイルとしての main.go

「設定ファイルとしての main.go」というポリシーを解説し、 そのポリシーを適用した自作のツールカタログ koron/gtc の事例を紹介します。

この記事は Go Advent Calendar 2017 (その1) 10日めの参加記事です。 なお Go Advent Calendar 2017 はその4まであります。

設定ファイルとしての main.go

golang を書かれる皆さんは、 golang でツールを書いた際にその設定ファイルはどうしているでしょうか? オーソドックスに JSON でしょうか? 可読性高く YAML? はたまた TOML でしょうか。

この記事では「設定ファイルとしての main.go」というポリシーを紹介します。 英語で書くと main.go as a configuration ですかね。 なので以下では仮に MaaC と表記します。 このポリシー MaaC は私が時々内々に提唱しているだけで、 他に提唱している人がいるかは寡聞にして知りませんが、 きっとすでに言っている人はいるはずです。 ちょっと難しそうに感じるかもしれません。 しかし内容としては非常に単純で、大事なのは 「もう設定ファイルも golang で書いちゃえば良いじゃんよ」 ということです。

では具体的に MaaC に基いた main.go のコードを見てみましょう。

package main

import (
    "os"
    "github.com/author/tool"
)

func main() {
    err := tool.Run(tool.Config{
        Foo: "my_favorite_foo_setting",
        Bar: "my_favorite_bar_setting",
        // TODO: ここにあなた好みの設定を追加する/書く
    }, os.Args...)
    if err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
}

このようにして書かれた main.go は go run で直接実行できますし、 go build でビルドしてから実行しても、 さらには go install$GOPATH/bin にインストールしてしまっても良いでしょう。 何れにせよ golang のビルドは十分に高速だからこそできる方法です。

では次に MaaC のために書かれたツールの内容を見てみましょう。 架空のパッケージ github.com/author/tool は 何かしらの超便利なツールを実装しているとします。 そして 2つの API を公開しています。 1つは設定用の tool.Config 構造体で、 もう1つはツール本体を実行する tool.Run 関数です。 場合によっては設定用に追加でいくらかの構造体や型、 そして定数を公開しているかもしれませんがここでは割愛します。 また Config 自体に Run メソッドを生やしても良いでしょうが、そちらも割愛。 ではAPIの定義を以下に擬似コードで示しましょう。

package tool

// Config is configuration of the tool.
type Config struct {
    Foo string
    Bar string
}

// Run runs the tool with config.
func Run(c Config, args ...string) error {
    // ... ここにツールの実装が入る
    return nil
}

見てわかる通りに非常にシンプルな公開APIです。 golang でツールを作って公開する際には main.go を提供して、 ユーザーが go get などで実行ファイルを容易に作れるようにするのが通常です。 しかし MaaC に従うなら、上記のようなシンプルな公開APIを用意・提供します。 もちろんデフォルトとしてその公開APIを使う形で main.go も提供するのはありでしょう。

ともあれツールがこのように設定と実行だけのシンプルな公開APIを提供することで、 そのユーザーは自分好みの設定を main.go として golang で記述できるようになります。

設定を main.go として各ユーザーが記述するメリットは大きくわけて3つあります。

1つめのメリットは一部の設定ではコンパイル時にチェックができることです。 例えば設定値として列挙型を使う場合、 その他の設定ファイル方式では実行時に不正な値かどうかをチェックすることになります。 しかもそのチェックロジックに相当するものは手で記述する必要があります。 しかし MaaC ならコンパイル時にチェックできます。 もちろん型定義だけなのでチェックロジックを書きメンテする必要はありません。 以下にその定義例を示します。

package tool

type Choice int

const (
   ChoiceA Choice = iota
   ChoiceB
   ChoiceC
)

type Config struct {
    // ...(中略)...

    Choice Choice

    // ...(後略)...
}

さらに以下ではその利用例、main.go での使われ方を示します。

package main

// ...(中略)...

func main() {
    err := tool.Run(tool.Config{
        Choice: tool.ChoiceB,

        // ...(後略)...

こうすることで Config.Choice に意図した値のいずれか設定されたことは コンパイル時にチェックされ実行時には(ほぼ)なくなります。 (キャストで無理やり抜けられるので、ほぼ、と表現しました)

2つ目のメリットは細かくいくつにも分けて考えられるのですが、 大本はユーザーがそうして書いた main.go を github などのVCSに配置した際に生じます。 そうなれば自分用の設定を施した実行ファイルを、 golang (とgitなど) がインストールされてさえいれば go get できます。 以下はそのコマンド例です。

$ go get -v github.com/koron/mytool
$ mytool
# github.com/author/tool が自分好みの設定で実行される

さらに各種CIサービスを用いて代表的なプラットフォーム向けにビルドして、 自分好みの設定済みツールのバイナリを公開することもできるでしょう。

3つ目は簡単な説明ですが、自分好みの前処理や後処理を追加できることです。 前処理として環境に応じて設定をスマートに変更したり、 別のツールと組み合わせたり、 エラーハンドリングを工夫するだけでも良いでしょう。

以上 MaaC を採用して main.go を設定ファイルとすることには 幾つかのメリットがあることを解説しました。 次に実際にそれを適用したツールの事例を紹介します。

koron/gtc の紹介

koron/gtc は golang で書かれたツールを カタログで管理してインストール・更新・管理するためのツールです。

導入方法は他のツールと同様に go get -u github.com/koron/gtc するだけです。 あとの使い方は想像が付くとは思いますが gtc install gotags, gtc update gotags のように、 カタログに記載されたツールであればシンプルに管理できるようになります。 また gtc update -all ではカタログに記載があるインストール済みの全ツールを 更新することもできます。 なおこの方法には前回の更新から5日間は更新を自重する機能が付いています。 加えて gtc もデフォルトのカタログに含まれているので、 カタログ自体の更新も gtc により取得できるという形になっています。

この gtc の設定ポイントはカタログの存在です。 デフォルトのカタログには私がよく使うツールを厳選して、 かなり少なめに記載してあります。 なので実用上はユーザーごとに自分のツールを追加する=設定することが望まれます。

この設定には環境変数 GTC_CATALOG_FILE で JSON ファイルを指定することもできますが、 上で説明した MaaC を使うことをオススメしています。 以下は gtc の README にも記載してある設定ファイルの例です。

package main

import (
    "fmt"
    "os"

    "github.com/koron/gtc/gtcore"
)

func main() {
    err := gtcore.DefaultCatalog.Merge([]gtcore.Tool{
        {
            Path: "github.com/yourname/mygtc",
            Desc: "My own go tools catalog",
        },
        // TODO: ここにあなたの好きなツールを追加する
    }...).Run(os.Args)
    if err != nil {
        fmt.Println(err.Error())
        os.Exit(1)
    }
}

このような main.go を github.com/yourname/mygtc というリポジトリに格納しておき、 初回だけ go get github.com/yourname/mygtc さえすれば 以後は mygtc update -all で常に最新のツールを利用できるようになります。

また上の例ではデフォルトカタログに上書きする形で自分用のカタログを定義していましたが、 以下のように書けばデフォルトカタログを完全に無視して、 自分だけのカタログを定義・利用することもできます。

package main

// ...(中略)...

func main() {
    err := gtcore.NewCatalog([]gtcore.Tool{
        {
            Path: "github.com/yourname/mygtc",
            Desc: "My own go tools catalog",
        },
        // TODO: ここにあなたの好きなツールを追加する
    }...).Run(os.Args)

    // ...(後略)...

まとめ

最後に。

ここまで見てきたとおり koron/gtc は MaaC ポリシーの採用により、 ユーザーごとに好みのカタログを github (git)でスムーズに管理することができるようになっています。 MaaC 自体は決して万能の設定方法というわけではありませんが、 それでもいくつかの応用においては非常に有効であると考えられます。

よろしければ MaaC と koron/gtc を利用してみてください。