🔎

Go言語の変わった書き方に驚き!リフレクションでエラーをこっそり変換するas関数を解説します

に公開

はじめに

この記事では下記のような特殊な書き方がなされた Go のコードについて解説していきます。

func as(err error, target any, targetVal reflectlite.Value, targetType reflectlite.Type) bool {
	for {
		if reflectlite.TypeOf(err).AssignableTo(targetType) {
			targetVal.Elem().Set(reflectlite.ValueOf(err))
			return true
		}
		if x, ok := err.(interface{ As(any) bool }); ok && x.As(target) {
			return true
		}
		switch x := err.(type) {
		case interface{ Unwrap() error }:
			err = x.Unwrap()
			if err == nil {
				return false
			}
		case interface{ Unwrap() []error }:
			for _, err := range x.Unwrap() {
				if err == nil {
					continue
				}
				if as(err, target, targetVal, targetType) {
					return true
				}
			}
			return false
		default:
			return false
		}
	}
}

1. なぜこんなコードを書いているのか気になる

この as 関数を初めて見たときは、「これほど妙なリフレクションを使ったエラーチェックは何を目的としているのだろう」と疑問が湧くかもしれません。しかも、エラーがインターフェイスをいくつも実装しているかどうかを順番に確認し、奥深くまで再帰的に探るようになっています。通常は errors.Aserrors.Unwrap で済むところを、あえて別のロジックを組んでいる背景が気になります。

このコードは、「エラーがどんな型でも、特定のターゲット型に代入可能かどうかをじわじわと確かめる」という点が特徴です。しかも、 interface{ Unwrap() []error } のような複数エラーを返す型まで考慮しているので、一つの err をいくつも探って最適な型に変換しようとします。ここまで手間をかけるのは、「あらゆるラップされたエラーを一括で判定し、意図した型へセットしたい」という明確な狙いがあると推測できます。いったいどんな仕組みなのか、次のセクションで中身を詳しく見ていきましょう。

2. この as 関数はどんな動きをしているのか

この as 関数は、渡されたエラーをさまざまなインターフェイスで確認しながら最終的に「targetType に代入できるエラーを発見したら targetVal にセットして true を返す」という流れになっています。以下のステップで進みます。

1. 同じ型に代入できる場合

reflectlite.TypeOf(err).AssignableTo(targetType) で、現在のエラーが指定の型へ代入可能かどうかを確認します。もし可能なら targetVal.Elem().Set(reflectlite.ValueOf(err)) を呼んで、値をターゲットに反映して true を返します。

2. As(any) bool インターフェイスを実装しているかどうか

エラーが interface{ As(any) bool } を実装しているなら x.As(target) を試します。これが成功したら true、失敗なら次へ進みます。

3. Unwrap() error があれば再帰的に次のエラーへ

エラーが interface{ Unwrap() error } を実装している場合は、 x.Unwrap() で奥のエラーを取り出し、再度同じ as 関数を呼びます。ここで nil が返れば打ち切り、そうでなければ新しいエラーをチェックします。

4. Unwrap() []error に対応

複数のエラーを返す型に対しては、返された配列をひとつずつ再帰処理し、いずれかが true を返せば合格とみなします。どれも合わないなら false です。

5. 最後まで合わなければ false

どの条件にも合致しないときは変換できないと判断し、 false を返します。

このように、一つのエラーに対して「代入可能かどうか」「 As(any) bool を持っているかどうか」「アンラップできるかどうか」を段階的にチェックして、最終的にはターゲット型にセットするかどうかを決定するしくみです。Go標準のエラーアンラップ手法を拡張し、リフレクションや複数エラーへの再帰を組み合わせることで、より柔軟なエラー変換を実現しているように見えます。

3. リフレクションとアンラップを融合した面白い書き方

この as 関数では、複数のアンラップパターンを試しながら、最終的にリフレクションで「型が合うかどうか」を判定しています。Goのエラーは Unwrap() によって内部のエラーを辿る手段が備わっており、さらにリフレクションを組み合わせることで、実行時にエラーの型やインターフェイス実装を動的にチェックできます。

1. 複数のアンラップ

もし Unwrap() error を実装しているなら次の一つを辿り、 Unwrap() []error を実装しているなら複数のエラーを順番に検証します。これにより、どれだけ複雑にラップされたエラーでも深部を探し当てることができます。

2. リフレクションで型を判定

再帰的にアンラップして得たエラーが、 reflectlite.TypeOf(err).AssignableTo(targetType) で合致するなら最終的に targetVal にセットします。同じエラーであっても、アンラップの途中経過によっては型が異なる可能性があり、この切り替えを動的に行うところが独特です。

3. Go標準のエラー処理を拡張

標準ライブラリの errors.As は、単一のエラーを辿る仕組みしかありませんが、この関数では「複数エラーを含むインターフェイス」まで考慮しています。実行時に型を柔軟に判定できるため、より拡張的なエラー変換やインターフェイス実装チェックを可能にしているのが面白い点です。

こうした“アンラップ × リフレクション” の組み合わせは、一般的な Go コードとしては珍しいですが、入り組んだエラー状態をまとめて判定したい場面では有効な書き方といえます。

4. 実際どんな場面で使うのか

このような再帰的アンラップとリフレクションを組み合わせた仕組みは、複雑にラップされたエラーを一括して扱いたいときに有用です。以下では代表的な場面をいくつか挙げます。

1. ラップされまくったエラーをまとめて変換したい場合

異なるライブラリ同士が重ねてラップしたエラーを、最終的に自作の型で受け取って処理したいときが考えられます。この関数を使えば、複数のレイヤーをはがしながら、いずれかが意図したインターフェイスや型に合致すれば、それをターゲットとしてセットします。たとえばロギングで詳細を取得したり、特定のカスタムエラーとして再処理したりするケースです。

2. 多数のエラーを一度に探りたい場合

interface{ Unwrap() []error } を想定しているため、複数のエラーを持つコンテナのような型を扱うシーンにも応用できます。もし複数エラーの中に目的の型が混在していても、まとめて再帰的に調べられるので、1回の呼び出しで全エラーを統合的に検索できます。

3. 特定の型にのみ適用したい変換処理

リフレクションで AssignableTo を確認しているため、「具体的な型にのみ適用可能」という柔軟性を保ちつつ、多彩なインターフェイス実装をチェックできます。標準ライブラリの errors.As はシンプルですが、この関数なら「自作の As(any) bool が定義されているか」など追加の条件を含む書き方もできます。

要するに、実行時に多様なエラー構造をまとめて探索し、もし合うものがあれば別の型として取り出したい、というニーズがあるなら、こうした“再帰的アンラップとリフレクション”のコンボは役立ちます。コードとしては一見ヘンテコに見えますが、入り組んだエラーの世界では案外実践的な手段です。

5. まとめ:意外と理にかなったテクニック

この as 関数は、ひと目見ると奇妙なリフレクション処理と再帰的アンラップが詰め込まれていて、最初は理解しにくいかもしれません。しかし、複数のエラーを同時に検索して特定の型へ変換できるかどうかを調べる場面を想定すると、意外と理にかなっています。複数のライブラリでラップされたエラーをまとめて確認しながら、自分が欲しい型やインターフェイスを見つけたら、すぐにセットして使えるという利点があります。

  • 柔軟に型を判定するため、実行時の状況に応じてエラーを変換しやすくなります。
  • 再帰的にアンラップするので、どれほど深くラップされたエラーでも条件に合うものを探し当てることができます。
  • 標準の errors.As に比べて多少強引な方法ではありますが、複数エラーを扱うなど拡張的な要件を満たすための発想として興味深いです。

実践的なコードベースでこのような関数を見つけたら、まずは「何を目指しているのか」を整理してください。見た目が特殊でも、目的がはっきりしていれば、かえって有用なテクニックになり得ます。大規模なエラーハンドリングや独自の変換処理が求められる状況では、こうした発想が役に立つかもしれません。

Discussion