Go が try-catch による例外送出の代わりに明示的なエラー返却と panic-recover を採用した理由

Goは意図的にtry-catchをサポートせず、panic-recover構文のみをサポートしている。これはtry-catchによるエラー処理に慣れた多くの開発者から不満を買っている。では、なぜtry-catchを取り入れなかったのだろうか。それはtry-catchが多くの問題点を抱えているからである。
try-catch-finally
トライキャッチ(try-catch)構文は、プログラム実行中(ランタイム)に発生しうるエラー状況や例外状況を処理するための構文である。また、finally構文は、例外の発生有無にかかわらず必ず実行されるべきコードを記述する。
エラー処理と責任
80年代と90年代には、エラー処理は非常に単純であった。エラーメッセージは「フロッピーディスクの容量不足」「ドライブにフロッピーディスクがない」「フロッピーディスクへの書き込み権限がない」がすべてであり、当時の開発者はエラーが発生すると、そのエラーをエラー処理の地点までthrowして共通化して処理していた。このような状況では、try-catch構文は効率的に機能していた。
しかし、時が経つにつれて状況は変化した。ビジネスロジックは複雑になり、データベースとトランザクションが登場し、ネットワークを通じて数多くのAPIを呼び出し、膨大な数のリクエストメッセージを解釈しなければならなくなった。さらに、並行プログラミングの登場により、メインではない別のスレッドでエラーを処理しなければならない場合さえ生じた。
エラーはもはや一箇所で処理できないほど複雑になり、ある一箇所がすべてのエラーの責任を負うことができなくなった。ここで、try-catchに深刻な問題が生じる。
try-catchの責任転嫁
try-catchは、端的に言えば、エラーを発生させた主体が、エラー発生に対する責任(後始末)を誰かに押し付ける方法である。その押し付け先は、catch構文であることもあれば、自身の親メソッドであることもあり、自身の親の親の親の親の……誰かであることもある。言い換えれば、エラー処理が増えて複雑になる世界において、try-catchが選んだ方法とはまさに「誰かがやってくれるだろう」なのである。次のコードを見てみよう。
1try {
2 data = readFile("hello.txt");
3 structuredData = parseData(data);
4 insertDBStatus(structuredData[1]);
5 startHTTPServer(structuredData[2]);
6} catch (Exception e) {
7 e.printStackTrace();
8}
上のコードの問題点は、printStackTraceを処理する主体が何であるかも明確でなく、どのコードでエラーが発生したのかも分からなくなることである。さらに、try文の中にロジックが増えるほど、問題はいっそう深刻になる。しかし皮肉なことに、開発が複雑になればなるほど、開発者は責任を転嫁するtry-catch構文に依存するようになり、エラー処理について考えることをやめ、責任意識も薄れ、結局はエラーと例外処理の本質を忘れてしまった。では、golangはこの問題をどのように解決したのだろうか。
panic, recover
Goの長所の一つは、開発者を悪い道に導かず、良い開発者へと育てるための様々な仕組みが存在することであろう。panic-recoverもその例として挙げられる。
パニックとリカバーは一見するとtry-catchと違いがないように見えるが、問題が発生したときに責任を外部へ転嫁しない点で異なる。Goにおいてパニックが起きた場合、その値を外部に投げるのではなく、パニックが発生した位置で解決しなければならない。これは開発者にエラー処理の責任を与え、そのエラーをどこで、誰が、どのように処理すべきかについて、より深く考えるよう促す。利用者に自律性を保証しながらも、考える余地を残しているのである。
また、panicという単語の選定も非常に優れていると言える。try-recoverと異なり、panic-recoverは単語そのものによって、例外状況ではなく明白なエラー状況においてのみ使用すべきだという圧力を開発者に与える。自然と開発者はrecoverを濫用せず、必要な場所でのみ使用するようになる。これはGoで簡潔なコードを書くうえで大いに役立つ。