コンテンツにスキップ

Goプロポーザル deep dive 候補: errors と型チェッカー

結論

説明ネタとしての扱いやすさは、以下の順。

候補 主資料 難易度 向いている説明
errors.AsType #51945 Go proposal の読み方、標準ライブラリ API 設計、ジェネリクス導入後の ergonomic 改善
errors.Join / error tree #53435 中〜難 error wrapping の設計、互換性、標準ライブラリに入れる理由
errors.Iter / IterAs #66455 errors.Is / As で足りないケース、error tree の全探索 API 設計
型構築と cycle detection Go Blog, #75883 Go コンパイラ内部、型チェッカー、不完全型、循環参照検出

まず説明するなら errors.AsType がよい。 理由は、現場での errors.As の書きにくさから入れるので読者が追いやすく、proposal issue の問題設定も短い。

一方で「難易度の話」をちゃんとしたいなら、型チェッカー記事はかなり良い。 ただし proposal issue 単体ではなく、Go 1.26 の内部改善ブログと関連 bug issue を読む形になる。

候補1: errors.AsType

主 issue: errors: AsType (As with type parameters) #51945

Go 1.26 release notes: errors.AsType

何の話か

従来の errors.As は、対象のエラー型を先に変数として宣言し、そのアドレスを渡す必要がある。

var myErr *MyCustomError
if errors.As(err, &myErr) {
    // myErr を使う
}

Go 1.26 の errors.AsType[E error] は、これをジェネリクスで書けるようにした API。

if myErr, ok := errors.AsType[*MyCustomError](err); ok {
    // myErr を使う
}

何がうれしいか

  • 変数のスコープを if ブロックに閉じ込められる
  • &target のような pointer-to-pointer っぽい見た目を避けられる
  • target any を受ける errors.As より型安全にしやすい
  • release notes では、型安全・高速・多くの場合に使いやすい、と説明されている

注意点

errors.As は「error を実装する型」だけでなく、任意の interface 型にもマッチできる。 一方、errors.AsType[E error] は型パラメータ Eerror を満たす必要がある。

つまり、単なる interface{ A() } を探すなら errors.As が必要。 AsType でやるなら interface { error; A() } のように error も含める必要がある。

難易度

中。

アプリケーションエンジニア向けには「errors.As の面倒さを generic API で改善した」で説明できる。 ただし深掘りするなら、以下を押さえる必要がある。

  • error wrapping は単なる型アサーションではなく、error tree を探索する
  • errors.As は custom As(any) bool メソッドも尊重する
  • AsTypeAs の置き換えではなく、よくある形を安全・簡潔にする API
  • 標準ライブラリ API は名前、互換性、既存 API との重複をかなり慎重に見る

候補2: errors.Join と error tree

主 issue: errors: add support for wrapping multiple errors #53435

Go 1.20 release notes: errors.Join

何の話か

Go 1.13 で Unwrap() errorerrors.Is / errors.As が入り、error wrapping が標準化された。 その後、複数の error を1つの error として扱いたい需要が残った。

Go 1.20 では以下が入った。

  • errors.Join(errs ...error)
  • fmt.Errorf で複数の %w
  • Unwrap() []error
  • errors.Is / errors.As が error chain ではなく error tree を探索する考え方

難しいところ

「複数 error をまとめるだけ」なら簡単に見える。 しかし標準ライブラリに入れるには、次の設計判断が必要になる。

  • Unwrap() errorUnwrap() []error をどう共存させるか
  • errors.Is は複数の子のどれかに一致すれば true でよいか
  • errors.As は複数マッチする場合にどれを返すか
  • errors.Join はネストした join error を flatten すべきか
  • 個別 error を取り出す公式 API をどこまで用意すべきか

この周辺で、あとから以下の proposal も出ている。

  • #57358: errors.Join を「戻す」ための errors.Errors
  • #66455: error tree 全体を iterator で走査する Iter / IterAs

難易度

中〜難。

errors.Join 自体の使い方は簡単。 しかし proposal を読む題材としては、標準ライブラリが「便利関数」を入れるだけでは済まないことを説明できる。 特に As が「最初に見つかった型」を返す以上、複数 error の tree で「全部見たい」需要が残る、という話が深い。

候補3: 型構築と cycle detection

X 元ネタ: turbofish の投稿

リンク先: Type Construction and Cycle Detection

関連 proposal: spec: remove cycle restriction for type parameters #75883

関連 bug:

  • #75918: invalid cyclic program involving unsafe.Sizeof で panic
  • #76383: unsafe.Sizeof の結果が不正
  • #76384: unsafe.Sizeof で type checker panic
  • #76478: value type の式で unsafe.Sizeof が panic

何の話か

Go の型チェッカーは、AST を見ながら内部の型表現を作る。 これが type construction。

単純な例なら簡単。

type T []U
type U *int

T の underlying type を作るには []U が必要で、U を作るには *int が必要。 依存関係をたどって、下から完成させていけばよい。

問題は再帰型。

type T []U
type U *T

この場合、T を作っている途中でまた T に戻る。 ただし *T のようにポインタ越しなら、T の中身を今すぐ覗かなくてもよい。 未完成の T を指しておき、あとで全体が完成すれば成立する。

どこから難しくなるか

配列型の長さは型の一部で、コンパイル時定数でなければならない。 その長さ計算で unsafe.Sizeof が絡むと、型の中身を覗く必要が出る。

type T [unsafe.Sizeof(T{})]int

この例では、T の長さを知るために T{} のサイズが必要。 しかし T{} のサイズを知るには、T の完成が必要。

つまり依存関係がこうなる。

T を完成させたい
  -> 配列長が必要
    -> unsafe.Sizeof(T{}) が必要
      -> T のサイズが必要
        -> T が完成している必要がある

これは正当な再帰型ではなく、解けない循環。 型チェッカーは panic するのではなく、cycle error として報告しなければならない。

Go 1.26 の改善の要点

ブログの要点は、未完成な型を持つ値が「下流」に流れてから壊れるのを待つのではなく、未完成値が生まれる「上流」で止める、という整理。

例:

  • conversion: T(42)
  • function call: f()
  • type assertion: i.(T)
  • channel receive: <-ch
  • map access: m[k]
  • dereference: *new(T)

これらが未完成な型の値を作るなら、その時点で検出する。 結果として、型構築アルゴリズムを単純化しつつ、cycle detection の精度を上げられる。

proposal として見るなら

X のリンク先ブログそのものは proposal issue ではない。 proposal issue として扱うなら #75883 が近い。

Go 1.26 では、generic type が型パラメータリスト内で自分自身を参照する制限が外れた。

type Adder[A Adder[A]] interface {
    Add(A) A
}

この proposal は「型チェッカーがもう扱えるなら、不要な仕様制限を消そう」という話。 背景には #68162 のような、妥当に見える generic interface が invalid recursive type として拒否されていた問題がある。

難易度

難。

難しさの中心は、Go の表面文法ではなく、型チェッカー内部の不変条件にある。

  • defined type と underlying type の違い
  • 型が「完成している」とは何か
  • 再帰型でも許される循環と、許されない循環の違い
  • unsafe.Sizeof や配列長のように、型構築中に型を deconstruct する処理
  • 「未完成型への参照」は許せても、「未完成型を覗く」は許せないという線引き

この題材は、Go を使う人向けの「新機能紹介」よりも、コンパイラや型システムの読み物として深い。 説明するなら、まず type T []U; type U *T のような許される再帰型を出し、そのあと type T [unsafe.Sizeof(T{})]int で壊れる循環を出すと理解しやすい。

使い分け

短めにまとめるなら errors.AsType。 proposal issue を読む練習としてちょうどよく、Go 1.26 の新標準 API としても実用に近い。

深掘り回にするなら errors.Join から error tree、さらに errors.Iter の未採択 proposal へ進むとよい。 「なぜ便利そうなのにすぐ入らないのか」を説明できる。

難易度の話を主役にするなら、型構築と cycle detection。 これは proposal の採否よりも、「簡単に見える言語仕様の裏で、コンパイラがどんな状態管理をしているか」を理解する題材として強い。