フューチャー技術ブログ

Go1.17で警告されるようになったerror#Is/As/Unwrap

The Gopher character is based on the Go mascot designed by Renee French.

始めに

TIG DXUnitの宮崎です。

2021/8/16にGo1.17がリリースされましたね。

Go 1.17連載第6回目ということで、Go Vetによる静的解析が強化され、errorを実装した構造体に対するIs/As/Unwrapのシグネチャチェックが実施されるようになったという小ネタを紹介します。

errors.Is/As/Unwrap に関してはGo Tips連載6: Error wrappingされた各クラウドSDKの独自型エラーを扱う記事で復習もできますので、なんだっけ? という方は参照いただけるとです。

なお、この記事では以下の表記ルールとしています。

  • errorsパッケージのIs/As/Unwrapメソッド
    = errors.Is/As/Unwrap
  • errorインタフェースを実装した構造体のIs/As/Unwrapメソッド
    = error#Is/As/Unwrap

TL;DR

  • errorインタフェースを実装し、かつIs/As/Unwrapメソッドを実装する場合、以下シグネチャ以外は警告されるようになった。
    • Is(error) bool
    • As(interface{}) bool
    • Unwrap() error
  • あくまで警告であり、ビルドも実行も可能
  • 挙動にも変更点はなし

error#Is/As/Unwrap is 何

Go1.17のリリースノートを引用すると以下の通り。

The vet tool now warns about methods named As, Is or Unwrap on types implementing the error interface that have a different signature than the one expected by the errors package.
The errors.{As,Is,Unwrap} functions expect such methods to implement either Is(error) bool, As(interface{}) bool, or Unwrap() error respectively.
The functions errors.{As,Is,Unwrap} will ignore methods with the same names but a different signature.

errorインタフェースを実装した時にIs/As/Unwrapのシグネチャが間違ってた時に怒ってくれるようになったらしいです。Is/As/Unwrapをなんとなーくしか捉えていなかったので、これを期にしっかり学んでみます。

そもそもerror

Goのerrorインタフェースについての復習から。ご存知の通り、Goにはtry/catch構文が存在しなく、errorインタフェースを実装した構造体を返却することで例外発生を表現します。errorインタフェースとはGoに組込まれているインタフェースで、具体的には以下の通り定義されています。

type error interface {
Error() string
}

Error()というメソッドを実装してstringを返せばなんでもerrorになれるということですね。シンプルですが、これだけではエラーとしての表現力が乏しく、実際使う場合は具体的にどの型のエラーなのか、どの型が発生源なのかを判別して挙動を分岐させたりします。

errors.Is/As/UnwrapはGo 1.13で導入されたerrorの階層化や型比較を実現するためのメソッドです。

errorに関する記述はソースを見るのが一番早いです。

errors.Unwrap/error#Unwrap

Go 1.13にて以下が追加されerrorの階層化ができるようになりました。

  • errors.Unwrap
  • fmt.Errorf()に、%w識別子が追加

fmt.Errorf()で階層化させてUnwrapで取り出すという流れですね。errors.Unwrapの実装は下記の通り。Unwrap() errorを実装しない構造体の場合はnilが返却されるようになってます。階層化を実現するための重要なメソッドなのにシグネチャ違いで実装されていると予期した通りに動かないので、Vetが気を効かせてくれるようになったみたいですね。

func Unwrap(err error) error {
u, ok := err.(interface {
Unwrap() error
})
if !ok {
return nil
}
return u.Unwrap()
}

errors.Is/error#Is

errors.Isは特定のエラーとの比較を、再起的に階層を掘って実施してくれます。
実装は以下の通り。

func Is(err, target error) bool {
if target == nil {
return err == target
}

isComparable := reflectlite.TypeOf(target).Comparable()
for {
if isComparable && err == target {
return true
}
if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) {
return true
}
// TODO: consider supporting target.Is(err). This would allow
// user-definable predicates, but also may allow for coping with sloppy
// APIs, thereby making it easier to get away with them.
if err = Unwrap(err); err == nil {
return false
}
}
}

やっていることは↓の通り。

  • targetが比較可能なら比較
  • errIs(err error) boolメソッドが実装されていればcall
  • errをUnwrapする。できなければfalse返却

Unwrapのときと同じ具合で、シグネチャを確認するような実装になっていますね。

errors.As/error#As

errors.Asはエラーに対する型アサーションを実施してくれます。
実装は以下の通り。

func As(err error, target interface{}) bool {
if target == nil {
panic("errors: target cannot be nil")
}
val := reflectlite.ValueOf(target)
typ := val.Type()
if typ.Kind() != reflectlite.Ptr || val.IsNil() {
panic("errors: target must be a non-nil pointer")
}
targetType := typ.Elem()
if targetType.Kind() != reflectlite.Interface && !targetType.Implements(errorType) {
panic("errors: *target must be interface or implement error")
}
for err != nil {
if reflectlite.TypeOf(err).AssignableTo(targetType) {
val.Elem().Set(reflectlite.ValueOf(err))
return true
}
if x, ok := err.(interface{ As(interface{}) bool }); ok && x.As(target) {
return true
}
err = Unwrap(err)
}
return false
}

やっていることは以下です。

  • targetが有効なポインタでなければエラー(errの型アサーション結果代入先であるため)
  • ループ
    • targetにerrが代入可能なら代入して終了
    • As(interface{}) boolメソッドがあればそれを呼ぶ。trueが帰ってくれば終了
    • Unwrapして次ループに入る

指定した型として扱えるまで階層を掘って試行してくれていますね。

ここでもシグネチャが大事になってきます。

警告されるようになった実装

リリースノートのサンプルに戻ります。

以下がVetによって警告されるようになったとのことですが、具体的に言うとIsのシグネチャが間違っていますね。errors.Isを有効に使用するにはIs(err error) boolとして実装する必要があります。

↓の実装だとどんなに頑張ってもerrors.Isfalseを返すので、何もないと貴重な時間を無駄にしたり、最悪の場合バグに気づかずリリースなんてことにもなりかねません。今回の修正でVetが怒ってくれるようになったので、そんな不幸なことが起きることが無くなったわけですね。

type MyError struct { hint string }
func (m MyError) Error() string { ... } // MyError implements error.
func (MyError) Is(target interface{}) bool { ... } // target is interface{} instead of error.
func Foo() bool {
x, y := MyError{"A"}, MyError{"B"}
return errors.Is(x, y) // returns false as x != y and MyError does not have an `Is(error) bool` function.
}

最後に

というわけでerror#Is/As/Unwrapネタでした。

少しだけ平和な世界に近づいたようです。

次回は連載最後で市川さんの記事です。