Journal

Goのエラーハンドリングを嫌いきれない理由

Goの `if err != nil` は、見た目だけで嫌われやすいです。たしかに画面に並ぶと少しくどいですし、最初は私も野暮ったく見えました。ただ、ファイル、通信、プロセスのような失敗が現実に起きる場所へ近づくほど、あの露骨さが効いてきます。

以前、Windows向けの小さなGo製CLIを人に渡したとき、CGOを有効にしたままビルドしていて別のPCで起動しなかったことがありました。そのとき痛感したのは、抽象化が美しいかどうかより、どこで失敗したかがすぐ読めるほうが大事だということです。配布したあとに効くのは、たいていそちらです。

失敗の場所が近い

Goのエラーハンドリングは、失敗した場所をそのままコードに残しやすいです。`os.Open` で失敗したのか、`json.Unmarshal` で壊れたのか、`exec.Command` が落ちたのかが、その場で読めます。その距離が短いだけで、あとから読む側はかなり助かります。

私は自分のPCのソフトウェアくらい自由に触りたいと思っています。設定ファイルがどこにあり、どの通信が怪しく、何を消せば元に戻るのかも、自分で追えるほうが落ち着きます。その手がかりは、きれいに隠されるより、少し不格好でも表に出ていたほうが追いやすいです。Goの `error` はその不格好さを隠しません。

たとえば設定ファイルを読むだけの小さな処理でも、素直に書くとこうなります。

func readConfig(path string) error {
    b, err := os.ReadFile(path)
    if err != nil {
        return fmt.Errorf("config %s: %w", path, err)
    }
    if len(b) == 0 {
        return errors.New("config is empty")
    }
    return nil
}

短いですし、どこで失敗したかも残ります。例外を遠くへ投げてあとで雰囲気で拾うより、このくらい直截なほうが私は信用できます。

配布の場面で効く

Goのコードがきれいに見える瞬間は色々ありますが、私がいちばん助けられるのは配布の直前と配布のあとです。単体バイナリで渡した道具が相手のWindowsで動かなかったとき、必要なのは美しい思想ではなく、いま何が壊れているかです。

その点で、`main` まで `error` を返して最後に `stderr` へ出す流れはかなり実務的です。ログを増やしすぎなくても、最低限の失敗理由を残せます。小さなCLIなら、それで十分な場面が多いです。

func main() {
    if err := readConfig("/work/missing.yaml"); err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
}

この書き方は派手ではありません。でも、あとで自分が困らないです。フレームワークの都合で見えなくなった失敗より、自分で読める一行のほうがずっと強いです。

最近はAIにボイラープレートや下調べを任せることが増えました。それ自体は悪くないですし、むしろ本質に時間を回せるので助かります。ただ、最後にどこで失敗するかまで丸ごと曖昧にされると、急に自分のコードではなくなります。Goのエラーハンドリングは、その境目をまだ人間の手元に残してくれます。

だから私は、Goの `if err != nil` を嫌いきれません。美しいからではありません。壊れたときに、ちゃんと自分のコードへ戻ってこられるからです。

Previous