The Go Blog
[ On | No ] エラー処理のための構文サポート
Goに関する最も古く、最も根強い不満の一つは、エラー処理の冗長性です。私たちは皆、このコードパターンに非常に(苦痛なほどに、と言う人もいるでしょう)精通しています。
x, err := call()
if err != nil {
// handle err
}
if err != nilというテストは非常に普及しており、他のコードをかき消してしまうほどです。これは通常、多くのAPI呼び出しを行うプログラムで発生し、エラー処理が初歩的で、単にエラーが返されるだけの場合です。一部のプログラムでは、次のようなコードになってしまいます。
func printSum(a, b string) error {
x, err := strconv.Atoi(a)
if err != nil {
return err
}
y, err := strconv.Atoi(b)
if err != nil {
return err
}
fmt.Println("result:", x + y)
return nil
}
この関数本体の10行のコードのうち、実際の作業を行っているのは4行(呼び出しと最後の2行)だけに見えます。残りの6行はノイズのように感じられます。冗長性は現実のものであり、エラー処理に関する不満が長年、毎年恒例のユーザー調査のトップになっているのも不思議ではありません。(一時的に、ジェネリクスの欠如がエラー処理に関する不満を上回っていましたが、Goがジェネリクスをサポートするようになった今、エラー処理が再びトップに返り咲きました。)
Goチームはコミュニティのフィードバックを真剣に受け止めており、何年もの間、Goコミュニティからの意見も取り入れながら、この問題の解決策を模索してきました。
Goチームによる最初の明確な試みは2018年に遡り、Russ Coxが当時Go 2と呼んでいた取り組みの一環として、この問題を正式に記述しました。彼はMarcel van Lohuizenによるドラフト設計に基づいた可能な解決策を概説しました。この設計はcheckとhandleメカニズムに基づいており、かなり包括的なものでした。ドラフトには、他の言語で採用されているアプローチとの比較を含む、代替ソリューションの詳細な分析が含まれています。特定のエラー処理のアイデアが以前に検討されたかどうか疑問に思っている場合は、この文書を読んでください!
// printSum implementation using the proposed check/handle mechanism.
func printSum(a, b string) error {
handle err { return err }
x := check strconv.Atoi(a)
y := check strconv.Atoi(b)
fmt.Println("result:", x + y)
return nil
}
checkとhandleのアプローチは複雑すぎると判断され、約1年後の2019年には、はるかに単純化され、今では悪名高いtry提案がそれに続きました。これはcheckとhandleのアイデアに基づいていましたが、check擬似キーワードがtry組み込み関数になり、handle部分は省略されました。try組み込み関数の影響を調べるために、既存のエラー処理コードをtryを使って書き換える簡単なツール(tryhard)を作成しました。この提案は、GitHubのissueで900件近くのコメントが寄せられ、激しく議論されました。
// printSum implementation using the proposed try mechanism.
func printSum(a, b string) error {
// use a defer statement to augment errors before returning
x := try(strconv.Atoi(a))
y := try(strconv.Atoi(b))
fmt.Println("result:", x + y)
return nil
}
しかし、`try`はエラーが発生した場合に囲む関数から戻ることで制御フローに影響を与え、潜在的に深くネストされた式からそれを実行するため、この制御フローが見えないようになりました。このため、この提案は多くの人にとって受け入れがたく、この提案に多大な投資をしたにもかかわらず、この取り組みも断念することにしました。今にして思えば、新しいキーワードを導入する方が良かったかもしれません。`go.mod`ファイルやファイル固有のディレクティブを介して言語バージョンを細かく制御できるようになった今なら、それが可能です。`try`の使用を代入と文に限定していれば、他の懸念の一部は軽減されたかもしれません。Jimmy Frascheによる最近の提案は、基本的に元の`check`と`handle`の設計に戻り、その設計の欠点の一部に対処しており、その方向性を追求しています。
`try`提案の余波は、Russ Coxによる一連のブログ記事「Go提案プロセスについて考える」を含む多くの自己反省につながりました。一つの結論は、コミュニティからのフィードバックの余地がほとんどなく、「脅威的な」実装スケジュールで、ほとんど完成した提案を提示したことで、より良い結果を得るチャンスを減らしてしまった可能性が高いということでした。「Go提案プロセス:大規模な変更」によると、「今にして思えば、`try`は私たちが発表した新しい設計が、実装スケジュールを伴う提案ではなく、第2ドラフト設計であるべきだったほど大きな変更でした。」しかし、この場合のプロセスとコミュニケーションの失敗の可能性とは関係なく、この提案に対するユーザーの感情は非常に否定的でした。
当時、私たちはより良い解決策を持っていなかったため、数年間、エラー処理のための構文変更を追求しませんでした。しかし、コミュニティの多くの人々が触発され、エラー処理に関する提案が着実に寄せられました。それらの多くは互いに非常に似ており、興味深いものもあれば、理解不能なもの、実現不可能なものもありました。拡大する状況を把握するために、その1年後、Ian Lance Taylorは、エラー処理の改善のための提案された変更の現状をまとめた傘立てのissueを作成しました。関連するフィードバック、議論、記事を収集するためにGo Wikiが作成されました。独立して、他の人々が長年にわたる多くのエラー処理提案を追跡し始めました。たとえば、Sean K. H. Liaoの「go error handling proposals」に関するブログ記事で、それらすべての膨大な量を見るのは驚くべきことです。
エラー処理の冗長性に関する不満は根強く(Go Developer Survey 2024 H1 結果を参照)、そのため、Goチーム内部での一連の洗練された提案の後、Ian Lance Taylorは2024年に「`?`を使ったエラー処理のボイラープレート削減」を発表しました。今回は、Rustで実装されている構成、具体的には`?`演算子から借用するというアイデアでした。既存のメカニズムを確立された記法で利用し、長年にわたって学んだことを考慮に入れることで、ついに進展を遂げられるという希望がありました。プログラマーに`?`を使ったGoコードを見せる小規模な非公式ユーザー調査では、参加者の圧倒的多数がコードの意味を正しく推測し、これが私たちに再度挑戦するようさらに確信させました。変更の影響を確認するために、Ianは通常のGoコードを提案された新しい構文を使用するコードに変換するツールを作成し、コンパイラでもこの機能を試作しました。
// printSum implementation using the proposed "?" statements.
func printSum(a, b string) error {
x := strconv.Atoi(a) ?
y := strconv.Atoi(b) ?
fmt.Println("result:", x + y)
return nil
}
残念ながら、他のエラー処理のアイデアと同様に、この新しい提案もすぐにコメントと、個人の好みに基づく些細な調整の多くの提案で溢れかえりました。Ianは提案を閉じ、議論を促進し、さらなるフィードバックを収集するために、その内容を議論に移動しました。わずかに修正されたバージョンは少し好意的に受け止められましたが、広範な支持を得るには至りませんでした。
Goチームによる3つの本格的な提案と、文字通り数百(!)ものコミュニティ提案(そのほとんどが同じテーマのバリエーション)が、十分な(ましてや圧倒的な)支持を得られずに失敗し、長年の試行錯誤を経て、私たちは今、次のような疑問に直面しています。どう進めるべきか?そもそも進めるべきなのか?
私たちはそうではないと考えています。
より正確に言えば、私たちは少なくとも当面の間、*構文上の問題*を解決しようとするのをやめるべきです。提案プロセスは、この決定の正当性を説明しています。
提案プロセスの目標は、タイムリーに結果に関する一般的な合意に達することです。提案レビューが課題トラッカーでの議論において一般的な合意を特定できない場合、通常の結果は提案が却下されることです。
さらに、
提案のレビューで一般的な合意が特定できない場合でも、その提案をきっぱりと却下すべきではないことが明らかであるということもあり得ます。[...] もし提案レビューグループが合意も提案の次のステップも特定できない場合、今後の進め方に関する決定はGoアーキテクト[...]に委ねられ、彼らが議論をレビューし、彼らの間で合意に達することを目指します。
エラー処理の提案はいずれも合意に近づくことはなく、すべて却下されました。GoogleのGoチームの最も上級のメンバーでさえ、*現時点では*最善の進め方について満場一致で同意していません(おそらくいつか変わるでしょう)。しかし、強い合意がなければ、合理的に前進することはできません。
現状維持を支持する正当な議論があります。
-
Goが早期にエラー処理に特化した糖衣構文を導入していれば、今日それを巡って議論する人はほとんどいなかったでしょう。しかし、私たちは15年が経過し、機会は過ぎ去り、Goには、時には冗長に見えるかもしれませんが、エラーを処理するための全く問題ない方法があります。
-
別の角度から見ると、今日完璧な解決策にたどり着いたと仮定してみましょう。それを言語に組み込むことは、単に不満を持つユーザーグループ(変更を望むグループ)から別のグループ(現状維持を好むグループ)へと移行するだけでしょう。私たちは、ジェネリクスを言語に追加することを決定したときも同様の状況にありましたが、重要な違いがありました。今日では誰もジェネリクスを使用することを強制されておらず、優れたジェネリクスライブラリは、型推論のおかげで、ユーザーがそれがジェネリックであるという事実をほとんど無視できるように書かれています。これに対し、エラー処理のための新しい構文構造が言語に追加された場合、事実上誰もがそれを使用し始める必要があり、さもなければ彼らのコードは慣用的なものではなくなってしまうでしょう。
-
追加の構文を追加しないことは、Goの設計規則の1つである「同じことをする複数の方法を提供しない」と一致しています。この規則には、トラフィックの多い分野では例外があります。代入が思い浮かびます。皮肉なことに、短い変数宣言(
:=)で変数を**再宣言**する機能は、エラー処理のために生じた問題に対処するために導入されました。再宣言がなければ、エラーチェックのシーケンスでは、各チェックに対して異なる名前のerr変数(または追加の個別の変数宣言)が必要になります。当時、より良い解決策は、エラー処理のためにより多くの構文サポートを提供することだったかもしれません。そうすれば、再宣言ルールは不要になり、それとともに様々な関連する複雑さもなくなっていたでしょう。 -
実際のエラー処理コードに戻ると、エラーが実際に**処理**される場合、冗長性は背景に消えます。良いエラー処理には、エラーに追加情報が必要となることがよくあります。例えば、ユーザー調査で繰り返されるコメントは、エラーに関連するスタックトレースの欠如についてです。これは、拡張されたエラーを生成して返すサポート関数で対処できます。この(確かに不自然な)例では、ボイラープレートの相対量ははるかに小さいです。
func printSum(a, b string) error { x, err := strconv.Atoi(a) if err != nil { return fmt.Errorf("invalid integer: %q", a) } y, err := strconv.Atoi(b) if err != nil { return fmt.Errorf("invalid integer: %q", b) } fmt.Println("result:", x + y) return nil } -
新しい標準ライブラリの機能も、Rob Pikeの2015年のブログ記事「エラーは値である」の趣旨に沿って、エラー処理の定型文を減らすのに役立ちます。たとえば、場合によっては
cmp.Orを使用して、一連のエラーを一度に処理することができます。func printSum(a, b string) error { x, err1 := strconv.Atoi(a) y, err2 := strconv.Atoi(b) if err := cmp.Or(err1, err2); err != nil { return err } fmt.Println("result:", x+y) return nil } -
コードを書く、読む、デバッグすることは、いずれも全く異なる活動です。繰り返しエラーチェックを書くのは面倒かもしれませんが、今日のIDEは、LLM(大規模言語モデル)の支援を受けた強力なコード補完を提供しています。基本的なエラーチェックを書くことは、これらのツールにとって簡単です。冗長性はコードを読むときに最も明らかになりますが、ここでもツールが役立つ可能性があります。例えば、Go言語設定のあるIDEは、エラー処理コードを非表示にする切り替えスイッチを提供できます。そのようなスイッチは、関数本体などの他のコードセクションについてはすでに存在します。
-
エラー処理コードをデバッグする際、`println`を素早く追加したり、デバッガでブレークポイントを設定するための専用の行やソースロケーションがあったりすると便利です。専用の`if`文がすでに存在する場合は簡単です。しかし、すべてのエラー処理ロジックが`check`、`try`、または`?`の背後に隠されている場合、コードを最初に通常の`if`文に変更する必要があるかもしれません。これはデバッグを複雑にし、微妙なバグを引き起こす可能性さえあります。
-
実用的な考慮事項もあります。エラー処理の新しい構文アイデアを考案するのは安価です。そのため、コミュニティから多数の提案が乱立しています。しかし、精査に耐える優れた解決策を考案するのはそうではありません。言語変更を適切に設計し、実際に実装するには、協調的な努力が必要です。本当のコストはその後にかかります。変更が必要なすべてのコード、更新が必要なドキュメント、調整が必要なツールです。これらすべてを考慮すると、言語変更は非常に高価であり、Goチームは比較的小さく、対処すべき他の多くの優先事項があります。(これらの後者の点は変更される可能性があります。優先事項は変更される可能性があり、チームの規模も増減する可能性があります。)
-
最後に、私たちの一部は最近Google Cloud Next 2025に参加する機会があり、Goチームのブースがあり、小さなGoミートアップも開催しました。質問する機会があったGoユーザーは皆、エラー処理を改善するために言語を変更すべきではないと主張しました。多くの人が、Goに特定のエラー処理サポートがないことは、そのサポートがある他の言語から来たばかりのときに最も顕著であると述べました。流暢になり、より慣用的なGoコードを書くにつれて、この問題ははるかに重要でなくなります。これはもちろん、代表的な数とは言えない人々の集合ですが、GitHubで見かける人々とは異なる人々の集合かもしれませんし、彼らのフィードバックは新たなデータポイントとなります。
もちろん、変更を支持する正当な議論もあります。
-
より良いエラー処理サポートの欠如は、ユーザー調査で最も不満として挙げられ続けています。もしGoチームがユーザーからのフィードバックを本当に真剣に受け止めているのであれば、いずれこれについて何らかの対処をするべきでしょう。(ただし、言語変更に対する圧倒的な支持があるようには見えませんが。)
-
おそらく、文字数を減らすことだけに集中するのは誤りかもしれません。より良いアプローチは、ボイラープレート(
err != nil)を削除しながらも、キーワードを使ってデフォルトのエラー処理を非常に目立たせることかもしれません。このようなアプローチは、読者(コードレビューア!)がエラーが処理されていることを「二度見することなく」確認しやすくし、コードの品質と安全性を向上させる可能性があります。これは、checkとhandleの始まりに戻ることを意味します。 -
問題がエラーチェックの単純な構文上の冗長性にあるのか、それとも優れたエラー処理の冗長性(APIの有用な一部であり、開発者とエンドユーザーの両方にとって意味のあるエラーを構築すること)にあるのか、私たちは本当にわかっていません。これは、さらに深く研究したいと考えていることです。
これまでのところ、エラー処理に取り組む試みはどれも十分な支持を得られていません。現状を正直に評価するならば、私たちは問題に対する共通の理解も持っておらず、そもそも問題が存在することに全員が同意しているわけでもない、と認めざるを得ません。これを念頭に置き、私たちは以下の現実的な決定を下します。
当面の間、Goチームはエラー処理のための構文言語変更の追求を停止します。また、主にエラー処理の構文に関わる、現在オープンおよび incoming のすべての提案を、さらなる調査なしにクローズします。
コミュニティは、これらの問題を検討し、議論し、討論するために多大な努力を払ってきました。これはエラー処理の構文に何ら変更をもたらさなかったかもしれませんが、これらの努力はGo言語と私たちのプロセスに他の多くの改善をもたらしました。おそらく、将来のある時点で、エラー処理についてより明確な展望が開けるでしょう。それまでは、この素晴らしい情熱を、Goをすべての人にとってより良くするための新しい機会に集中できることを楽しみにしています。
ありがとうございます!
次の記事: ジェネリックインターフェース
前の記事: Go 暗号セキュリティ監査
ブログインデックス