フォロー

>なぜGoでerrors.Newを都度呼び出すことが推奨されないのか
zenn.dev/dimdim1996/articles/0

長文にチャレンジ。

拙文「Go のエラーハンドリング」でも書いたが
zenn.dev/spiegel/books/error-h

Go のエラーハンドリングの戦術は以下の4つに分類できる。

1. nil との比較(エラーの有無を評価)
2. インスタンスの同一性
3. タイプ・アサーションによってエラーの内部構造を取得する
4. エラーメッセージ(Error() メソッドの返り値)を解析する

このうち3番目と4番目の難易度が高いのは直感的に分かるだろう。
3番目はその型の内部構造を知っている必要があるし4番目はあからさまにバッドノウハウ(今どきで言うならアンチパターン)である。
なので,エラーを吐く場合にはできるだけ1番目または2番目のパターンに落とし込めるようにするのがよい。

もうひとつ。
Go では,エラーは Error 型に boxing される。スマートポインタと言ったほうが分かりやすい人もいるかも知れない。
そのため,エラー同士の比較では等価性(equality)ではなく(インスタンスオブジェクトの)同一性の評価となる(実質的にポインタ値の比較なので)。
ここで errors.New() 関数の価値が出てくるわけだ。
この関数を使ってあらかじめ sentinel error を作成しておくことで最初に挙げた4つの戦術のうち「インスタンスの同一性」がやりやすくなる。

さらに Go 1.13 からはエラーを構造化できるようになり errors.Is() 関数でエラーの原因を遡って「インスタンスの同一性」評価ができるようになった。
これにより errors.New() 関数の役割は sentinel error の生成に特化できるようになった。
これが go-err113 の意味である。

とはいえ Go 1.13 以前のコードでは errors.New() 関数にそこまでの意味はない。
特に fmt.Errof() 関数と比較すれば errors.New() 関数は圧倒的にコストが低い。
エラーの有無を評価を評価するだけでいいなら,関数の return のタイミングで errors.New() でエラーを生成しても全然 OK なわけだ。
古いコードではそういう戦術をとっている場合も多いだろう。

コンパイルエラーはともかく lint のルールは絶対ではない。
あるコードに対して lint をかける場合に「そのルールは本当に必要か?」検討するべきだ。
その上で「errors.New() 関数の役割は sentinel error の生成に特化」ことをプロジェクトのルールとするなら lint で積極的に評価スべきだし,そうでないならいっそルールから外すことも検討したほうがいいだろう。

...ここまでで1,100文字とちょっとか。全然余裕だなw 5,000文字とかショートショートが書けるぜwww

ログインして会話に参加
Fedibird

様々な目的に使える、日本の汎用マストドンサーバーです。安定した利用環境と、多数の独自機能を提供しています。