または私は如何にして例外するのを止めて golang を愛するようになったか

Java の finally よりも golang の defer のほうが筋が良さそうだ、 ということから考え始めた結果、 どうして私が golang を気に入ったのかがわかった気がしたので書いておきます。

ファイルをオープンし読み込みな処理で何かして終わったら閉じる、という関数を Java と golang で書き比べてみましょう。

Java で書くとこんな感じですね。


public static void readFile(String fname) throws IOException {
    InputStream s = null;
    try {
        s = FileInputStream(fname);

        //
        // Do something with "s".
        //
    } finally {
        if (s != null) {
            s.close();
        }
    }
}

Java本職からすれば文字コードとかバッファリングとか close() からの例外をどうすんだよっていうツッコミはありますが、まぁここでは必要最低限のコードってことで我慢してください。

これとほぼ等価な golang の関数はこんな感じになりますね。

func readFile(fname string) error {
    s, err := os.Open(fname)
    if err != nil {
        return err
    }
    defer s.Close()

    //
    // Do something with "s"
    //
}

Java のほうは finally で close() するという都合上、変数宣言を try の外に書かなければなりませんが、それを差し引いても golang のほうがスッキリ書けてるように見えます。それにはいくつかのポイントがあります。

1つにはJava側の finally の中の if 文 が邪魔なことでしょう。

} finally {
    if (s != null) {
        s.close();
    }
}

FileInputStream を作る際に例外が発生してしまった時、s が null のまま finally に突入するため、これが必要になっています。わざわざ例外機構を用いて大域脱出を試みているのに、そのあとで分岐が必要になるのには疑問を禁じえません。これは InputStream などに限れば JDK 1.7 (Java 7) 以降で try-with-resources により解決されてはいるものの、JDK 1.6 以前で書かれたソースは少なくなく、それを読む人に疑問を抱かせる期間がそれなりに続くんじゃないでしょうか。

対して golang は Open() の戻り値の err をチェックしてすぐに抜けてしまいます。

s, err := os.Open("foo.txt")
if err != nil {
    return err
}
defer s.Close()

そのため defer s.Close() が実行されることはなく、パッと見にも問題にならないと理解できます。

もう1つ重要な点として、golang のほうは Open() と Close() の呼び出しが対であると強調されること、を挙げられるでしょう。Open() したら defer で Close() することを保証してから、続く処理が記述されます。これをより一般化すると「ある関数 foo を呼んで成功したら、後で別の関数 bar を呼ぶことを約束する」となります。

err := foo()
if err != nil {
    return nil
}
defer bar()

golang でこのような記述を見かけたら、そのような決まりがあることが容易に見て取れるわけです。

ところで JDK 1.7 以降の try-with-resources は AutoCloseable か Closeable を実装していれば使えますが、これにはあくまでも close() という名前が付いています。つまり用途は限定、特殊化されています。ですから汎用的に「あるメソッド foo を読んで成功したら、後で別のメソッド bar を呼ぶことを約束する」には、未だに finally + if を使う必要があるのです。もちろん finally を回避するために AutoCloseable なラッパークラスを用意してっていうこともできますが、あまりに大げさでバカバカしく、無条件で golang の書き方のほうが良いと言いたくなります。

// このコードはコンセプトだけを示す目的で
// かなり適当に書いたのでコンパイルできないかも
final MyObj o;
try (AutoCloseable a = new AutoCloseable() {
    AutoCloseable() { o.foo(); }
    void close() { o.bar(); }
}) {
    // Do something with "o"
}

ここまで書いてみて1つのことに気が付きました。「Java の例外が発生しうるメソッドの呼び出しは分岐を隠蔽・内包している」ということに。

私の経験上、プログラミングの複雑さ・難しさの本質は分岐に(本当は加えてループにも)ある、と考えています。それゆえすべての分岐を明らかにし把握することが、真に頑強(ロバスト)なプログラム を作成するための最低必須条件だと考えています。また、いつかそのようなプログラムをコンスタントに書けるようになりたいとも。

しかし Java などの例外機構は分岐を隠してしまい、それの把握を困難にさせているのですね。それに対して golang はすべてのエラーハンドリングを詳(つまび)らかにしている。

なるほど。私が golang を気に入るわけです。