Go の errors パッケージ設計史: error chain から error tree へ¶
このテーマの狙い¶
errors パッケージの使い方紹介ではなく、標準ライブラリとしての設計判断を読む。
話の中心はこれ。
Go の
errorは、どこまで「中身をたどれる構造」として標準 API に露出すべきなのか。
この切り口にすると、既存の errors.Is / errors.As / errors.Join 入門とは被りにくい。
一方で、errors.Join の使い方だけに寄せると既存発表とかなり被る。
まず押さえる軌跡¶
| 時期 | 追加・議論 | 見るべきもの | 意味 |
|---|---|---|---|
| Go 1.13 | Unwrap() error, errors.Is, errors.As, %w |
Working with Errors in Go 1.13, Error Inspection proposal | error chain を標準化 |
| Go 1.20 | errors.Join, Unwrap() []error, 複数 %w |
#53435 | chain から tree へ |
| Go 1.20 後 | errors.Errors 的な分解 API |
#57358 | Join したなら取り出したい問題 |
| Go 1.24 以降の議論 | Iter / IterAs |
#66455 | Is / As では「全部見たい」に弱い |
| Go 1.26 | errors.AsType |
#51945 | As の ergonomics 改善 |
Go 1.13: error chain の標準化¶
Go 1.13 以前も、os.PathError のように「別の error を内包する error」は存在していた。
ただし標準的な取り出し方はなかった。
type QueryError struct {
Query string
Err error
}
func (e *QueryError) Error() string {
return e.Query + ": " + e.Err.Error()
}
呼び出し側が中身を見たいなら、具象型を知っている必要があった。
Go 1.13 で Unwrap() error という規約が入り、errors.Is / errors.As が error chain を探索するようになった。
func (e *QueryError) Unwrap() error {
return e.Err
}
if errors.Is(err, ErrPermission) {
// err 自体か、err が wrap しているどこかに ErrPermission がある
}
ここで重要なのは、Unwrap は単なるデバッグ用ではないこと。
errors.Is / errors.As がプログラム上の判断に使うための構造を作る。
ここでの設計判断¶
fmt.Errorf("%w", err) で wrap すると、内側の error は API 契約の一部になる。
例えば、DB 実装の都合で sql.ErrNoRows が出たときに %w で返すと、呼び出し側はこう書ける。
これは便利だが、パッケージ実装を将来差し替えにくくする。
つまり error の中身を露出することは、抽象化境界を露出することでもある。
発表ではここを強調するとよい。
wrap は「詳しいログを残す」ではなく、「呼び出し側に判定可能性を公開する」行為。
Go 1.20: error chain から error tree へ¶
Go 1.13 の Unwrap() error は直線的な chain だった。
しかし、複数の処理結果をひとつの error として返したい場面がある。
- 入力バリデーションで複数フィールドが失敗した
- 複数 goroutine の結果をまとめたい
- 処理本体の error と
Closeの error を両方返したい
Go 1.20 では errors.Join と Unwrap() []error が入った。
この時点で、error は chain ではなく tree になる。
errors.Is / errors.As は、この tree を深さ優先で見る。
なぜ難しいか¶
複数 error をまとめるだけなら簡単に見える。 しかし標準ライブラリでは次の契約を決める必要がある。
Unwrap() errorとUnwrap() []errorを同名にしてよいか- 両方実装した型はどう扱うか
errors.UnwrapはUnwrap() []errorを返すべきかerrors.Isは複数のうち1つでも一致すれば true でよいかerrors.Asは複数一致するとき、どれを返すかerrors.Joinはネストを flatten すべきか- error の表示順を仕様としてどこまで固定するか
ここが「標準パッケージとしての OSS」の面白いところ。 便利 API ではなく、エコシステム全体の互換ルールを決める話になる。
Join したなら分解したい問題¶
errors.Join が入ると自然にこう思う。
実際に #57358 では、errors.Join を戻す errors.Errors のような API が提案された。
この要望は自然。
Join できるなら、取り出せる API も欲しくなる。
ただし標準 API としては悩ましい。
としたとき、次を決める必要がある。
- plain error の場合は
[]error{err}を返すのか、nilを返すのか - 直下の子だけ返すのか、tree 全体を flatten するのか
nilerror をどう扱うのか- 返した slice は変更してよいのか
fmt.Errorfの複数%wとerrors.Joinの違いをどこまで見せるのか
つまり「分解 API」は小さく見えて、error tree の構造を標準 API の契約にする。 ここが重い。
Iter / IterAs: 全部見たい問題¶
errors.As は最初に見つかった型だけを返す。
しかし error tree の中に *CodeError が複数あるかもしれない。
この場合、最初の1つだけ見ても十分とは限らない。
#66455 は、error tree を iterator として走査する API を提案している。
for e := range errors.Iter(err) {
// tree 内の error を順に見る
}
for e := range errors.IterAs[*CodeError](err) {
// tree 内の *CodeError を全部見る
}
この提案の面白さは、errors.Is / errors.As の限界を認めているところ。
ただし、これも標準 API として入れると「error tree の走査順」をより明示的に契約することになる。
アンチ視点: 全部たどれることは本当に良いのか¶
error の中身を全部たどれる API があると便利そうに見える。
しかしアンチ的には、これは危うい。
理由は、error が「履歴」「ログ」「トレース」の代用品になりやすいから。
Go の errors が本来強いのは、次のような判定可能性。
一方で、error tree 全体をユーザーが自由に巡回し始めると、次の設計が曖昧になる。
- どの wrapper が API 契約で、どれが実装詳細なのか
- どの順序で見えるべきなのか
- 同じ error が複数回現れたらどうするのか
- ログに出すべき情報と、プログラムが分岐すべき情報の境界はどこか
つまり、発表の対立軸はこう置ける。
この対立が errors パッケージの深掘りポイント。
errors.AsType は補助テーマ¶
Go 1.26 の errors.AsType は、errors.As の generic 版。
をこう書ける。
これは使いやすいが、主役にすると Go 1.26 新機能紹介と被りやすい。
発表では、errors.As の awkwardness が Go 1.13 時点から残っていて、Go 1.26 で ergonomic に改善された、という補助線として使うのがよい。
発表構成案¶
タイトル案¶
Go の errors パッケージはなぜ error tree を採用したのか
10分構成¶
- Go 1.13 以前: error は値だが、内側の error を見る標準手段がなかった
- Go 1.13:
Unwrap,Is,As,%wで error chain が標準化された - wrap はログではなく API 契約である
- Go 1.20:
errors.Joinで chain から tree へ - 複数 error を標準化すると、分解・順序・flatten・探索の契約が問題になる
errors.Errors/Iterproposal が出る理由- アンチ視点: 全部たどれる API は
errorを履歴装置にしてしまう - まとめ:
errorsは「失敗の履歴」ではなく「判定可能性」を標準化している
参考資料¶
- Working with Errors in Go 1.13
- Proposal: Go 2 Error Inspection
- #53435 errors: add support for wrapping multiple errors
- #57358 proposal: errors: add Errors function to undo Join
- #66455 proposal: errors: add All and AllAs iterators
- #32405 proposal: errors: simplified error inspection
- #51945 errors: AsType