フューチャー技術ブログ

Go Tips連載6: Error wrappingされた各クラウドSDKの独自型エラーを扱う

Go Tips連載の第6弾です。

はじめに

TIG DXユニットの真野です。先週のこの記事ぶりの投稿になります。

フューチャー社内には「Go相談室」というチャットルームがあり、そこでGoに関連する疑問を投げたら、大体1日くらいで強い人が解決してくれるという神対応が行われています。そこでAWSやGCPの独自エラーをError warppingされた時にどうやってハンドリングすればよいの?と聞いた時にやり取りした内容をまとめました。

背景

Go1.13からfmt.Errorf 関数に %wという新しい構文が追加サポートされたことは、ご存知の方が多いと思います。

利用方法は、%w (pkg/errorsの時と異なりコロンは不要だし末尾じゃなくてもOK) と一緒に fmt.Errorf を用いることで、コンテキストに合わせた情報をメッセージに追加できます。

%wを使った例
func main() {
if err := AnyFunc(); err != nil {
// 2009/11/10 23:00:00 main process: any func: strconv.Atoi: parsing "ABC": invalid syntax
log.Fatalf("main process: %v", err)
}
}

func AnyFunc() error {
// 何かしらの処理
if err != nil {
return fmt.Errorf("any func: %w", err)
}
return nil
}

また、error種別ごとに処理を分けたい場合で、Sentinel errorを判定する場合は、 errorsパッケージに追加された errors.Is でWrapの判定できます。逆に言うとWrapされている場合、今まで通りの if err == ErrNotFound { といった構文では判定できなくなるので、既存コードへの導入時は呼び出し元と合わせてリライトが必要です。

SentinelErrorをWrapしたときのハンドリング
var ErrNotFound = errors.New("not found")

func main() {
if err := AnyFunc(); err != nil {
if errors.Is(err, ErrNotFound) {
// ErrorNotFound時のエラーハンドリング
} else {
// その他の場合のエラーハンドリング
}
}
}

func AnyFunc() error {
// 何かしらの処理
if err != nil {
return fmt.Errorf("any func: %w", ErrNotFound) // Wrap
}
return nil
}

この場合はシンプルで良いのですが、AWS SDK for GoなどのerrorをWrapした時に呼び出し側で判定をしたい時、どうすればよいのかが直接的な内容が見当たらなかったのでここにまとめておきたいと思います。

Handling Errors in the AWS SDK for Go

ドキュメントを読むと例えば、AWSのErorrハンドリングは以下のように、awserr.Error というインターフェースで表現されており、一度errを型アサーションしてから内部的なエラーコードに応じてハンドリングすることになっています。

AWS-SDKの通常版エラーハンドリング
if err != nil {
if aerr, ok := err.(awserr.Error); ok {
switch aerr.Code() {
case dynamodb.ErrCodeConditionalCheckFailedException:
// エラーハンドリング
case dynamodb.ErrCodeProvisionedThroughputExceededException:
// エラーハンドリング
default:
// エラーハンドリング
}
} else {
// エラーハンドリング
}
}

これをWrapされたときは、呼び出し元で単純に型アサーションを行ってもうまく判定できません。

NGなケース
func AnyFunc() error {
// 何かしらのAWS SDKを利用したコード
if err != nil {
return fmt.Errorf("aws operation: %w", err)
}
}

func main() {
if err := AnyFunc(); err != nil {
if aerr, ok := err.(awserr.Error); ok { // 🆖型アサーションでは判定できない
// AWS操作エラー特有のエラーハンドリング
} else {
// その他のエラーハンドリング
}
}
}

※Go Playgroundでサンプルを載せようと思いましたが、importでTimeoutになったので諦めました

対応方法

この awserr.Error を満たすerrorをWrapしたときはどうすべきかというと、 errors.As を用います。errors.As を代入用の変数とともに利用するとうまくいきます。

OKなコード
if err := AnyFunc(); err != nil {
var aerr awserr.Error
if ok := errors.As(err, &aerr); ok {
switch aerr.Code() {
case dynamodb.ErrCodeConditionalCheckFailedException:
// 何かしらのエラーハンドリング
case dynamodb.ErrCodeProvisionedThroughputExceededException:
// 何かしらのエラーハンドリング
default:
// エラーハンドリング
}
} else {
// その他のエラーハンドリング
}
}

例として愚直にif分岐をすべて網羅するように書きましたが、早期returnを活用すると、よりネストが浅く見通しが良いコードにできると思います。

GCP SDKの場合

しばしば以下のエラーを返すことが多いとのことです。

https://godoc.org/google.golang.org/api/googleapi#Error

if e, ok := err.(*googleapi.Error); ok {
if e.Code == 409 { ... }
}

もしこれらのerrorをWrapする場合は、同様に errors.As で判定します。(実際は後述する各サービスごとに宣言されているSentinel errorで判断することが多いと思います)

if err := AnyFunc(); err != nil {
var gerr *googleapi.Error
if ok := errors.As(err, &gerr); ok {
switch gerr.Code() {
case 409:
// 何かしらのエラーハンドリング
default:
// 何かしらのエラーハンドリング
}
} else {
// その他のエラーハンドリング
}
}

一方で、StorageなどはSentinel errorを返します。

StorageのSentinelError
var (
// ErrBucketNotExist indicates that the bucket does not exist.
ErrBucketNotExist = errors.New("storage: bucket doesn't exist")
// ErrObjectNotExist indicates that the object does not exist.
ErrObjectNotExist = errors.New("storage: object doesn't exist")
)

errorを返すAPIを利用してWrapした場合は errors.Is で判定します。

// Storageに対して何かしらアクセスする処理
if err := AnyFunc(); err != nil {
if ok := errors.Is(err, storage.ErrObjectNotExist); ok {
// 何かしらのエラーハンドリング
} else {
// その他のエラーハンドリング
}
}

どのAPIがどういったerrorを返しうるかは、各GoDocに書いてありますので、個別のハンドリングが必要な場合は確認することになると思います。

Stacktraceの出力について

https://play.golang.org/p/NAYR7XySCdW にサンプルコードを載せましたが、 %w構文を用いたfmtパッケージではStacktraceが出力されません。もし、Stacktraceが必要な場合は fmt.Errorfではなく xerrors.Errorf を用いてWrapします。

シビアに性能が求められない、例えばBackendのWebAPIをGoで実装する場合は、 xerrorsパッケージを利用した方が、2020/01/26 時点では良さそうです。

xerrorsを使った例
import (
"fmt"
"golang.org/x/xerrors"
)

func main() {
if err := Func(); err != nil {
fmt.Printf("stacktrace: %+v", err)
}
}

func Func() error {
if err := FuncInternal(); err != nil {
return xerrors.Errorf("anyFunc %w - internal failed", err)
}
return nil
}

func FuncInternal() error {
return xerrors.Errorf("any error")
}

これを実行するとStacktraceが出力されました。

Stacktrace出力例
stacktrace: anyFunc any error - internal failed:
main.Func
/tmp/sandbox921242282/prog.go:16
- any error:
main.FuncInternal
/tmp/sandbox921242282/prog.go:22

ちなみに、xerrorsでWrapされたエラーでも、errors.Is, errors.Asで判定できました。(混在すると少し気持ち悪いですが)

まとめ

  • Sentinel errorの場合は、errors.Is で、独自Error型を宣言している場合は、 errors.As を利用してハンドリングする
  • Stacktrace情報が必要な場合は、xerrorsパッケージを利用する
  • xerrorsでWrapしても errors.Is, errors.As で扱える