IT ニュース&コラム 2019/10/21 通巻799号 技術版 ソフトウェアデザイン館 Sage Plaisir 21  ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ■■ エラーが発生したときに例外を投げずに返り値としてエラーを返したときの問題 ■■ Go言語のFAQには、「我々は、処理構造を制御するための try-catch-finally 形式 の例外処理機構によって、コードが入り組んでしまうと考えています。」「Go言語 では戻り値として複数の値が返せるので、一般的なエラーハンドリングの場合、 戻り値といっしょにエラー情報を返すことができます。」と書いてあります。 これは、Java言語であれば、おそらく次のように正常系のコードに対して try ブロックを書かなければならないことを批判しているのでしょう。 File file; try { file = new File( "example.txt" ); } catch ( Throwable e ) { System.out.println( "File Not Found" );   } Go言語なら次のように書くことができます。 file, err := os.Open( "example.txt" ) if err != nil { fmt.Println( "file not found" ) } Java言語で 6行必要なコードが Go言語なら 4行で済みます。 なんとなくシンプルに なりましたね。 また、変数 err が os.Open 関数から発生したエラー情報を格納して いるということが、直感的に分かりやすいです。 しかし、エラーは呼び出すほとんどの関数から発生する可能性があるため、 実際にコードを書いていくと同じようなエラーチェックのコードが大量に発生して しまいます。 Go 言語でよく見かける以下のコードの大量発生です。 if err != nil { return err } Go言語では、一般的なエラーに対して panic を使うことを推奨していないので、 関数を呼び出すたびにエラーチェックのコードを書かなければなりません。 ランタイム ライブラリの開発者は関数を呼び出すコードをほとんど書かないので 分からないかもしれませんが、一般のアプリケーションの開発者はライブラリの 関数を呼び出すコードを書くことがほとんどなので、エラーチェックが毎回必要に なります。 以下の簡単なサンプルでさえそうです。 テキスト ファイルを1行ずつリードするコードで検証してみましょう。 例外処理を使った Java言語なら以下のように書きます。28行です。 ちなみに Python なら } だけの行が無くなるので、もっと少ない行数になります。 BufferedReader reader = null; try { reader = new BufferedReader( new FileReader( new File( "example.txt" ))); String line = null; for (;;) { line = reader.readLine(); if ( line == null ) { break; } SomethingOnInput( line ); SomethingOnInput( line ); } } catch ( Throwable e ) { System.out.println( e ); } finally { try { if ( reader != null ) { reader.close(); } } catch ( Throwable e ) { System.out.println( e ); } } 例外処理が使えない Go言語なら次のように書きます。31行です。 file, err := os.Open( "example.txt" ) if err != nil { fmt.Println( err ) return } defer func() { err2 := file.Close() if ( err == nil ) { err = err2; } }() scanner := bufio.NewScanner( file ) for { scanned := scanner.Scan() if !scanned { break } line := scanner.Text() err := SomethingOnInput( line ); if err != nil { fmt.Println( err ) return } err = SomethingOnInput( line ); if err != nil { fmt.Println( err ) return } } Go言語のFAQには、「Goの他の機能と相まってエラーハンドリングがすっきりした ものとなります。」と書かれていますが、上記のコードを見ると、複雑な部分と すっきりとした部分の場所の違いがあるぐらいで、プラスマイナスゼロといった ように見えます。 ただ、メインの処理を想定した SomethingOnInput 関数の呼び出しにおいては、 Java言語では正常系のコードしかないのに対し、Go言語では異常系のコードも混ざって しまい、しかも、正常系のコードよりも異常系のコードの行数が多いために、 主従関係が逆転して見えるため、理解が難しいコードになります。 メインの処理は 理解しやすいコードにしたいですよね。 Go言語の主張がどこから生まれているか想像してみたのですが、おそらく if err != nul のブロックの中の処理がそれぞれ異なるべきだと考えているのではないでしょうか。 Go言語のFAQには、「適切なエラーレポートとは、そのレポートされたエラーが直接的 かつ適切な内容で、プログラマが巨大なクラッシュトレースに対して行う調査を手助け します。」と書かれていることから推測されます。 しかし、適切なレポートをわざわざコードに書く必要があるケースは、ほとんどあり ません。 よく、メイン関数でエラーが起きた場所によって print する内容が異なる サンプルをよく見ますが、スタックトレースが取れるのであれば、冗長な情報なので、 実際の開発では、それぞれのエラーチェックの場所で print しないからです。 確かにスタックトレースが取れない環境では、関数内部のレポートだけでは適切な レポートではありません。 なので、異常系のテストケースとして、適切なレポートが 返っていなければ、投げられた例外を関数内部の途中で catch して適切なレポートに 改良して再度投げる必要があります。しかし、すべての関数呼び出しのエラーに対して 適切なレポートが書けるケースは、1割もありません。 それに、Go言語のFAQ に書かれた、「エラーハンドリングがすっきりしたもの」にするため には、エラーを返り値にする必要はありません。 例外処理のままでいいのです。 catch の対象となる try ブロックを、直前の文に対して暗黙的に設定するシンタックス シュガーにすればいいのです。 最初の Java言語のサンプルでは、以下のようになります。 File file = new File( "example.txt" ); catch ( Throwable e ) { System.out.println( "File Not Found" );   } 内部で発生したエラーのレポートを、呼び出し元で適切なレポートに改良して再度例外を 投げるときには、この構文が便利でしょう。 また、エラーを返り値で返すことは、エラーチェックを忘れるバグを埋め込んでしまう 可能性が出てきてしまいます。 Go言語では、2つ以上の返り値を返すときに 1つの 変数で返り値を受け取るときは、変数が 2つ必要であるというコンパイル エラーが 発生するため、エラーを無視するバグは埋め込まないと勘違いしている人が いるかもしれませんが、返り値がエラー 1つだけのときはコンパイル エラーにならず、 エラーを無視するバグを埋め込んでしまいます。 それを避けるため kisielk/errcheck パッケージがありますが、使っている人は少数だと思います。 使ってみると意外に 多くのエラーチェック漏れが見つかることに驚くでしょう。 返り値をきちんとチェック しないのが悪いのだと人を責めるには酷な状況です。 Go言語のFAQには、「しかも、ファイルを開けないといった、ごく一般的なエラーを さも特別なエラーであるかのように扱わせる傾向があります。」と書かれていますが、 一般的なエラーと特別なエラーの違い境界の説明がなく理解できません。 Go言語のFAQに、「それとは別に、Go言語にはエラーシグナルの発行機構と、 本当に例外的な状況から回復する機構があります。」と書いてあるので、おそらく前者は 返り値ですぐにエラーチェックするもの、後者は Go言語ランタイムで panic で実装されて いるメモリー不足や 0の割り算のエラーを指しているものと思われます。 前者であるか 後者であるかの判断基準は、Go言語のランタイム設計者の主観による判断としが思えません。 ファイルを開けないことは、返り値ですぐにエラーチェックするものとおそらく考えて いるのでしょうが、実際はそうとも限りません。 様々な処理を行う関数の中で、 ファイルが開けないときと、0の割り算が行われるとき、メモリー不足が発生したときに 何の違いがあるのでしょうか。 ユーザーがファイルを意識しないで内部でファイルが 開かれていることはよくあります。 内部で一般的なエラーが発生したのか特別なエラーが 発生したのかがユーザー側で判断のしようがなので、別々の機構があっては困るのです。 両方の機構のエラーハンドリングのコードを書かなければなりません。 ところで、関数仕様を決めるとき、エラーにすべきか返り値にすべきかの判断が難しいこと があります。 たとえば、データベースのレコードが見つからなかったときにエラーに すべきか見つかったかどうかを返すべきかです。 なぜ難しいのかというと、 アプリケーションが正常に処理を終了するのであればエラーにすべきではないですし、 アプリケーションが正常に処理を終了しないのであればエラーにすべきというのが 判断基準になるからです。 データベースのレコードが見つからなかったかどうかだけでは 判断できないからです。 たとえば、データベースのレコードが見つからなかったときには、別のテーブルから レコードを探すアプリケーションの場合は正常な処理ですが、別のテーブルから探さずに 期待した結果が得られない場合は異常な処理になります。 内部でエラーになる関数をアプリケーションが呼び出した場合、エラーハンドリング (エラー復帰)をすることで正常な処理にすることはできます。 しかし、このような コードをやたら嫌う人がいます。 嫌う人のほとんどは処理効率を問題にしますが、 catch ブロックが書かれるのはアプリケーションの呼び出し元だけであり、 内部では finally ブロックだけ書かれることがほとんどなので、Go 言語の defer に 相当する処理がほとんどなのです。 例外処理を C言語の longjump ではなく、 自動的に if err != nil { return } のアセンブリ コードを関数呼び出しごとに生成して、 Go 言語の defer と同じようなアセンブリ コードを出力すれば、処理効率の問題は解決します。 つまり、内部で例外を投げても、返り値で見つからないことを返しても、どちらのケース でも正常な処理にすることは可能なのです。 ですので、見つからないことが エラーであかエラーでないかを哲学的に正しいことを検証する必要はなく、axios の validateStatus のようにエラーにするかしないかを選べるようにする(ただし、 設定はグローバルなコンテキストではなく関数ローカルまたはスレッドローカルにすること) か、見つからないことがエラーとなるユースケースが多いかどうかという確率的な判断で よいと思います。 ただ、今の Go言語の実装では例外処理の処理効率が悪いというのは 公式のコメントどおりなのでしょう。 まとめると、返り値でエラーを返すことの問題は、大きく以下の2点になります。 ・エラーチェックを見逃す可能性が高いこと ・異常系のコードが主になり、理解が難しいコードになってしまうこと エキスパートであればこれらの問題を脳内で対処できるでしょう。 しかし、Go 言語の考えを支持するエキスパートの多くは、例外処理(panic)を嫌う あまり例外処理に関しては素人なのです。 Go言語の公式が panic を一般のエラーに 対して使うべきではないと言っている以上、仕方なく問題の生みやすいコードを書か なければ、Go言語のコミュニティでは認められないでしょう。 参考 http://golang.jp/go_faq - 例外(exception)がない理由は?、アサート(assert)がない理由は? https://tmrtmhr.info/tech/why-does-golang-not-have-exceptions/ https://h3poteto.hatenablog.com/entry/2015/12/11/221431 https://code.i-harness.com/ja-jp/q/6dd555 https://blog.amedama.jp/entry/2015/10/11/123535 https://opencredo.com/blogs/why-i-dont-like-error-handling-in-go/ ■■ 注目ニュース 一覧 ■■ ◇ アップル、香港の警察を追跡するアプリを削除。安全を脅かす。 https://japan.cnet.com/article/35143844/ … 警察を標的とした不意打ちを許さないか、中国共産党の批判に応じたのか。 ◇ SIE、次世代ゲーム機、プレイステーション 5 を2020年年末商戦期に発売へ。 https://japan.cnet.com/article/35143709/ … スマホは手元にあることで始めやすいが、PS5はロード時間をなくすことで始めやすい。 ◇ 災害時にSNSで助け合う10代。台風19号対応から学ぶこと。 https://japan.cnet.com/article/35144123/ … テレビやラジオ、自治体のホームページ、防災アプリだけが災害時の情報源ではない。 ◇ ウェザーニューズ×トヨタ、IoTとビッグデータで車両被害軽減を目指す共同研究。 https://japan.cnet.com/article/35143817/ … 気象庁から得られない情報も加えてより詳細で正確な天気予報へ。 ◇ 2018年の世界特許出願、中国が半数近くを占め圧倒的トップ。2位米国と3位日本は減少。 https://japan.cnet.com/article/35144107/ … 特許はハードに関わる必要があるため中国が有利だが、それでもパクリの中国ではなくなった。 ◇ 社員全員で仲間集めができるリファラル採用サービス Refcome Teams。 https://japan.cnet.com/article/35143871/ … 気の合いそうな仲間と仕事ができると楽しいけど固定化されてしまう。 ◇ アマゾンに行政指導。注文履歴の誤表示は11万件に影響。 https://japan.cnet.com/article/35143895/ … バグが多いアマゾン。 ◇ MS傘下GitHubのCEOが米移民当局との契約更新について説明。従業員は抗議。 https://japan.cnet.com/article/35143851/ … トランプ政権の移民に関する政策だが、輸出管理に似ている。 ◇ Amazonがデータベースの脱Oracle化をほぼ実現、75PB分のデータをAWSのDBサービスへ移行完了。 https://gigazine.net/news/20191016-amazon-migrate-75-petabyte-data/ … クラウドが主流になり、オラクルDBとJavaはレガシーになってきた。 ◇ Googleのクラウドゲームサービス Stadia は11月19日サービススタート。 https://gigazine.net/news/20191015-stadia-made-by-google-2019/ … 日本では来年以降あるかないか。 ■■ ソフトウェアデザイン館 Sage Plaisir 21 ■■ ホームページ >>> http://www.sage-p.com/ メルマガ >>> http://www.mag2.com/m/0000083983.html ブログ >>> http://blog.livedoor.jp/sage_p/ ツイッター >>> http://twitter.com/Ts_Neko ダウンロード >>> http://www.sage-p.com/freesoft.htm サポート掲示板 >>> http://www.sage-p.com/kg_ban09/z6037C8.cgi 東日本大震災 >>> http://www.sage-p.com/saigai.html メール >>> ts-neko◇sage-p.com ←◇を@に変えてください 緊急メールは件名に「うどんメール」を付けてください。 このメルマガの登録・解除 - http://www.mag2.com/m/0000083983.htm