詩と創作・思索のひろば

ドキドキギュンギュンダイアリーです!!!

Fork me on GitHub

goiferr で Go のエラー処理コードを自動挿入する

Go 言語には例外機構が備わっておらず、関数や手続きのエラー的な状況を表すには、返り値を多値にして本来興味ある結果とともに error インターフェイスを返す、というのが一般的です。例をあげるまでもないですが、ファイルを開くという(失敗する可能性のある)処理を行うならこういう感じ:

f, err := os.Open(filename)
if err != nil {
    // handle err
}

何ごとも明に書き下すことを求める Go らしい仕様ですね。文句ある人も多そうですが、呼び出し側が異常系を意識せざるを得なくなるので、よい効果も大きいと思います。

先の例のように、エラーを発生させるような処理を呼び出したあとは err をチェックする、というのは最初に学ぶイディオムと言ってもよいくらいよく書くことになるコードですが、ほんとうに何度も書くことになるのでこれは面倒。

go-iferr

そこでもちろん、プログラムにコードを書かせる試み。

https://github.com/motemen/go-iferr

goiferr は何らかのエラーを受け取る処理の直後に、そのエラーの処理を行う「いつもの」コードをよしなに挿入してくれるツールです。go get でインストールできます。

go get -u github.com/motemen/go-iferr/cmd/goiferr

例を見るのが手っ取り早いでしょう:

package eg

import "os/exec"

func GoVersion() (string, error) {
    path, err := exec.LookPath("go")

    cmd := exec.Command(path, "version")
    b, err := cmd.Output()

    return string(b), nil
}

例えばこのような "go version" コマンドの出力を文字列として返すコードを書いたとします。あ、エラーチェックをサボっていて中途半端ですね。というかこのままではコンパイルも通らないのでは。

このソースコードを a.go という名前で保存し、goiferr に処理させます。

% goiferr a.go > b.go
main.go:31: error (ignored): a.go:6:8: err declared but not used
=== a.go
% diff -u a.go b.go
--- a.go        2015-12-18 13:06:27.000000000 +0900
+++ b.go        2015-12-18 13:06:43.000000000 +0900
@@ -4,9 +4,15 @@

 func GoVersion() (string, error) {
        path, err := exec.LookPath("go")
+       if err != nil {
+               return "", err
+       }

        cmd := exec.Command(path, "version")
        b, err := cmd.Output()
+       if err != nil {
+               return "", err
+       }

        return string(b), nil
 }

あら便利、err のチェックをよしなにおこなってくれました! よく見るエラー処理が追加されて、安心感がありますね。

引数にディレクトリやパッケージ名を与えると複数のファイルを対象に同じ処理を行ないます。-w オプションを与えるとファイルを直接上書きしますので、普通はこれを利用するのが便利です。

細かい挙動

エラー処理が挿入される箇所

goiferr は、error 型をもつ変数が左辺に登場する代入文それぞれに対して、代入文とその次の文との間が1行以上空いている場合、エラー処理を挿入します。つまり

x, err := getX()
x.Foo()

のようなソースには何も変更を加えませんが、

x, err := getX()

x.Foo()

となっているとエラー処理を挿入します。

エラー処理の内容

エラー処理は

if err != nil {
    ...
}

という形で、コンテキストによって if 文の内容が変わります。

まず当該の文が error を返り値に関数の中にある場合、err を返すような return 文になります。これは先ほどの GoVersion 関数の例にあるとおり。

また、スコープから log パッケージを参照できる場合には log.Fatalf(err) します。

 import "log"

 func main() {
        _, err := GoVersion()
+       if err != nil {
+               log.Fatal(err)
+       }
+
 }

t という *testing.T 型をもつ変数を参照できる場合にはテスト関数の中だとみなして、t.Fatalf(err) します。

 import "testing"

 func TestFoo(t *testing.T) {
        _, err := GoVersion()
+       if err != nil {
+               t.Fatal(err)
+       }
+
 }

以上のどの条件にも合致しなければ、panic(err.Error()) します。

 func main() {
        _, err := GoVersion()
+       if err != nil {
+               panic(err.Error())
+       }
+
 }

よくできてますね。


https://github.com/motemen/go-iferr

以上、Go を書く上で面倒なところを自動化する goiferr の紹介でした! 今のところ便利ですが、「とりあえず作ってみた」レベルなので以下のようにまだまだやるべきことは残っています。コンパイルの通らないコードを生成することもあるかと思いますが、おいおい直していくつもりです。

TODO

  • 関数の返り値が名前付きの場合の対処
  • err 以外の変数名を許可する
  • エラー処理のコードをカスタマイズ可能にする

はてなで一緒に働きませんか?