次なる`pkg/errors`を探して

エンジニアの宮原です。 今回はGoでスタックトレースを取得するライブラリ選定についての記事です。
この記事は 【Gophers Talk】スポンサー4社による合同LT & カンファレンス感想戦で発表したものです。 発表スライドはこちらから確認できます。

この記事の目的

この記事ではpkg/errorsからの移行先を探すための参考情報を提供することを目的とします。 Goのエラーハンドリングのやり方等についてこの記事では触れないこととします。

pkg/errors とはなにか

pkg/errorsとは、githubのREADMEを引用すると

Package errors provides simple error handling primitives.

とあり、直訳すると、「エラーハンドリングの基礎を提供するパッケージ」となります。 pkg/errorsを利用することで、Go本体にはないスタックトレースを簡単に実現できます。 また、Go1.20でJoinが追加されるまでは、標準errorsのスーパーセットとなっていたという点も特徴でした。

pkg/errors からなぜ移行するのか

重複する記述になってしまいますが、Go1.20で標準errorsに更新が入り、pkg/errorsとの間に差分が発生しました。 この差分は解消する見込みがありません。プロジェクトとしてPublic Archiveとなってしまっているからです。

Public Archiveとなった経緯についてすこし補足します。
前回の標準errorsの更新(Go1.13)で修正の元となったGo2 Draft Designs というドキュメントがあります。このドキュメントではいずれGo本体からスタックトレースが提供されることが示されました。 つまり、将来的にpkg/errorsが不要になることがほぼ確定しました。そのタイミングでpkg/errorsはメンテナンスモードになり、さらにすすんで2021年12月にPublic Archiveに至った、という経緯のようです。

移行先に求めるもの

3 点あります。

「移行のしやすさ」は、標準errors、pkg/errorsとの互換性です。 ここでいう互換性とは、「関数、メソッドのシグニチャが一致しているか」、「機能をもれなくカバーしているか」といった点を想定しています。 Go本体への追従のしやすさを高めることと、今回の移行作業のコストを抑えることにつながります。

スタックトレースのサポート」は pkg/errorsがカバーしていた範囲を引き継ぐ必要があるためです。スタックトレースは、バグ解消に直結する情報を含むので重要です。

「性能が大きく劣化しないこと」は速い方がいい、メモリフットプリントは小さいほどよい、というシンプルな理由です。

移行の選択肢

最終的な候補となったのは以下2つのライブラリでした。

これらを深く見ていく前に、検討したものの詳しい調査の対象外としたライブラリについて触れておきます。

一度候補となったものの、細かい調査の対象外としたもの

xerrorsについて。Goチームが提供していたライブラリです。レポジトリのREADMEから確認できますが、このライブラリはGo1.13までの橋渡しとしての位置付けであり、すでに役割を終えたと言えるでしょう。すでに大部分がdeprecatedにもなっており、これから採用するべきではないと判断しています。

morikuni/failureについて。エラーコードベースのハンドリングを前提としており、pkg/errorsとは使われ方が異なるので、今回は移行のハードルが高いと判断しました。以下リンクが参考になります。 https://future-architect.github.io/articles/20200522/ https://speakerdeck.com/morikuni/designing-errors?slide=33

標準ライブラリを使った自前実装について。スタックトレースにたいして固有の要件というのはなく、改めて開発するメリットが薄いため、除外としています。

ということで、再掲ですが、最終的な候補ふたつcockroachdb/errorsgoark/errsを「 移行のしやすさ」、「スタックトレースのサポート」、「性能が大きく劣化しないこと」の点で評価していきます。

移行先の評価

前述した要件について箇条書きで記載します。

cockroachdb/errorsの評価

  • 移行のしやすさ
    • pkg/errorsのスーパーセットとなっており、基本的にはパッケージを切り替えるのみで、ほとんど置き換え作業が終わる点は魅力的です。
    • Go1.20で実装されたJoinが未実装のため、標準errorsとは差分があるものの、issueで対応中のようでした。
  • スタックトレースのサポート
    • サポートされています。
  • 性能が大きく劣化しないこと

goark/errsの評価

  • 移行のしやすさ
    • pkg/errorsとは、New、Wrapなど一部の関数は互換性あります。
    • Go1.20で実装されたJoinが未実装のため、標準errorsとは差分があります。
  • スタックトレースのサポート
    • サポートされています。
  • 性能が大きく劣化しないこと

ベンチマーク

性能評価のために、簡易なベンチマークを取りました。
benchmarkのテストコード、結果はこちらです。
「ネストしたerror生成時の速度とフットプリント」、「スタックトレース出力時の速度とフットプリント」の2つのケースについて、結果を抜粋して見ていきます。

※以下の結果は、マイクロベンチマークであり、実際の環境では異なる結果となる可能性があることに注意してください。

ネストしたerror生成時の速度とフットプリント

package ns/op B/op allocs/op
pkg/errors 8240 304 3
cockroachdb/errors 8640 416 7
goark/errs 7885 648 7

cockroachdb/errorsは、速度約5%pt、メモリ使用量約37%pt悪化、
goark/errsは、速度約4.5%pt改善、メモリ使用量は約2倍に悪化
という結果となりました。

スタックトレース出力時の速度とフットプリント

package ns/op B/op allocs/op
pkg/errors 12849 3716 33
cockroachdb/errors 14867 17222 22
goark/errs 1896 1401 33

cockroachdb/errorsは、速度が約15%pt、メモリ使用量が約4.6倍に悪化、
goark/errsは、速度が約6倍改善、メモリ使用量は63%pt改善
という結果となりました。

最後に結果をまとめた表を示します。

評価まとめ

package 標準errorsとの互換性 pkg/errorsとの互換性 スタックトレースのサポート 性能
errors(比較用) - - -
pkg/errors (比較用) ❌※1※2 - ⭕️ baseline
cockroachdb/errors 🔺※1 ⭕️ ⭕️ 🔺
goark/errs 🔺 ※1 🔺 ⭕️ ⭕️
  • ※1: いずれもGo1.13時点でのIs, As, Unwrap対応済み. Go1.20時点でのJoinは未実装
  • ※2: 今後もサポートされる見込みがないため、相対的に悪い評価をつけている

記事のまとめ

今回は最終的にcockroachdb/errorsがpkg/errorsの移行先の本命と評価しています。
移行のしやすさ(互換性)の面では、pkg/errorsのスーパーセットとなっており、置き換えが容易である点を評価しました。 性能については悪化するものの、自社でのAPIサーバーとしてのユースケースでは、ネットワーク往復の時間が支配的であることから劣化は問題ない範囲であると判断しました。

最後に、今回の評価が唯一の正解ではありません。それぞれの文脈、ユースケースを踏まえたうえで、ベストな選択肢を検討することが重要です。 上記はライブラリ選定の一例として参考していただけると幸いです。