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` を嫌いきれません。美しいからではありません。壊れたときに、ちゃんと自分のコードへ戻ってこられるからです。