Golangでのバッチ処理 個人的ベストプラクティス

author potpro(ぼとぷろ)
2019/03/05

Golangでのバッチ処理 個人的プラクティス

Go言語でバッチを書く機会があった為、そこで個人的に行なった・意識した事をまあ振り返り形式が描いていく記事です。

割と書いてることは堅いかなと思います。そんなに新しいことを書いている感じもないです。

これを使えば多分間違いないだろうという定番ライブラリの紹介みたいな記事です。

CLIインターフェイス / フレームワーク

cliを自分で実装するとなると割と大変だったりします。主に文法の解析。 なのでこれに関してはurfave/cliに任せておけばOKかと。

urfave/cli Star数10000超えてますし、特に不満もなく何でも使えるしカスタマイズもできるので、 今のところはこれが最良かと。

しっかりと標準出力/入力に依存せず、独立した構成になっているのでテストも容易です。 ほぼケースを網羅したテストのあるコードを書いてみる。言語設定で文章が変わる簡単なツール。

package main

import (
    "errors"
    "fmt"
    "os"

    "github.com/urfave/cli"
)

func run(args []string) error {
    app := cli.NewApp()
    app.Flags = []cli.Flag{
        cli.StringFlag{
            Name:  "lang",
            Value: "english",
        },
    }
    app.Action = func(c *cli.Context) error {
        if c.String("lang") == "english" {
            fmt.Println("Hello World!")
            return nil
        }
        if c.String("lang") == "japanese" {
            fmt.Println("こんにちわ、世界!")
            return nil
        }
        return errors.New("Not Support language")
    }

    return app.Run(args)
}

func main() {
    run(os.Args)
}
func TestRunJapanese(t *testing.T) {
    args := os.Args[0:1]
    args = append(args, "--lang")
    args = append(args, "japanese")
    err := run(args)
    if err != nil {
        t.Errorf("invalid")
    }
}

func TestRunEnglish(t *testing.T) {
    args := os.Args[0:1]
    args = append(args, "--lang")
    args = append(args, "english")
    err := run(args)
    if err != nil {
        t.Errorf("invalid")
    }
}

func TestRunNotExist(t *testing.T) {
    args := os.Args[0:1]
    args = append(args, "--lang")
    args = append(args, "spanish")
    err := run(args)
    if err == nil {
        t.Errorf("invalid")
    }
}

まさに神ツールです。これのおかげでgoでCLIツールを作るのがはかどり過ぎる。

外部APIやデータベース接続時のリトライ処理

まあこれはGo言語に限った話ではないですが・・・

外部のAPIに接続をして処理するというのは、100%成功するとは限りません。

バッチを目視で動かすタイプならいいのですが、基本的にバッチの目的としては深夜に動かしたり、または定期的に動かすものが多いと思われます。

自分で作ろうと思ったのですが、goのエコシステムは強く、ライブラリも揃っています。

今回色々調べていていい感じだなと思ったのは、matryer/tryavast/retry-go です。

matryer/try

matryer/try

このパッケージはREADMEを見るとわかりますが、指定した回数を指定した秒ごとにリトライしてくれる簡単なライブラリです。 作りは非常にシンプルです。errが起きればリトライするというだけです。

var value string
err := try.Do(func(attempt int) (bool, error) {
  var err error
  value, err = SomeFunction()
  return attempt < 5, err // try 5 times
})
if err != nil {
  log.Fatalln("error:", err)
}

ほとんどの場合は事足りるかなと思います。自分もこれを採用しました。

avast/retry-go

avast/retry-go

こっちもほぼ同じなんですが、こっちはリトライするごとに時間が倍に増えていくメカニズムです。 リトライ回数が増えるごとに、1秒 -> 2秒 -> 4秒 -> 8秒 -> 16秒... サーバーがダウンしてるときなんかは1秒ごとにリトライしても意味ないので、こちらのやり方も合理的な気もします。

ロギング

最近全部標準出力もエラー出力もすべて捨てていたバッチに嫌気がさしていたように、バッチとはいえロギングの必要はあります。 ロギングといっても問題なくログを出せれば構わないので、標準のLogパッケージでもいいのですが、正直Go言語のLogは正直ちゃんとロギングするのであれば少し役不足です。 INFO/WARNNING/ERRORみたいな区分もなく、色分けなんかも出来ません。

そのためいろんなログ拡張ライブラリが存在しますが、sirupsen/logrusがいい感じかなと思いました。 logrusはログの区分訳や自動色付け、フォーマットの必要なしに変数をいい感じにログで表示する、JSONでログを表示する、 など大体のほしい機能はそろっている印象です。 また標準のログパッケージを拡張する形なので、既に動いているものにも簡単に導入できます。

以上、個人的メモ。バッチというものは新しい機能はいらないのだ・・・