2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

アプリ実装者のためのGo1.13からのエラーとの付き合い方

Last updated at Posted at 2020-02-13

はじめに

Go1.13から機能追加されたエラーに関してキャッチアップがまだできていないアプリケーション開発者のための記事です。

TL;DR

アプリケーション実装者は以下をすることで嬉しさを得られます。

  • fmt.Errorf("%w", err) または Unwrap() errorを実装したエラー型をカスタムエラーにする。
  • errors.Is関数、errors.As関数を使いエラーハンドリングの受け口を実装する。

Go1.12までのバージョンとの実装方法の比較はGo 1.13時代のエラー実装者のお作法の記事が非常に参考になります。

環境

$ go version
go version go1.13.1 darwin/amd64

ライブラリが提供するエラーとの付き合い方

ライブラリが返すエラー値をfmt.Errorf("%w", err)でラップすればerrors.Is関数で中身のライブラリ側エラー値を判定できる

以下のコードをあるGoのライブラリが提供するものだとします。

lib.go
package main // 本来は package lib な形式

import (
	"errors"
	"time"
)

var (
	ErrParse = errors.New("lib: parse error")
)

type LibTime time.Time

func ParseLibFunc(text string) (LibTime, error) {
	t, err := time.Parse(time.RFC3339, text)
	if err != nil {
		return LibTime{}, ErrParse // ライブラリが持つエラー型の値を返す
	}
	return LibTime(t), nil
}

このライブラリの関数ParseLibFuncを利用するアプリケーションを作っているとします。

ここではserviceというアプリの関数がライブラリ関数を呼び、ライブラリ関数から返ったエラーをGo1.13からのfmt.Errorf("%w", err)の記法でラップしながらエラーを呼び元であるmain関数に返します。ここではmain関数はアプリコードのエラーハンドリングを担当する関数とします。

main.go
package main

import (
	"errors"
	"fmt"

	// "XXX/User/lib" // 本来はこのようにライブラリをインポートする
)

func service(text string) error {
	_, err := ParseLibFunc(text)
	if err != nil {
		return fmt.Errorf("service error with '%w'", err) // Unwrapを実装したエラーを返す
	}
	return nil
}

func main() {
	if err := service("2020/02/14"); err != nil {
		// errors.Is 関数を使ってライブラリ提供のエラーかを判定
		if errors.Is(err, ErrParse) {
			// handle error
		}

		// 以下検証用

		fmt.Println(err) // service error with 'lib: parse error'

		// errors.Unwrap 関数でライブラリ関数を取り出す()
		wrappedErr := errors.Unwrap(err)
		fmt.Println(wrappedErr) // lib: parse error

		fmt.Println(errors.Is(wrappedErr, ErrParse)) // true
		fmt.Println(errors.Is(err, ErrParse))        // true
	}
}

service関数で新たなエラー型の値にしていますがfmt.Errorf("%w", err)とラップしているので、errors.Is関数を使うことでライブラリのエラー値の判定が可能になっています。

また、errors.Unwrap関数によりラップされた1つ分内側のエラー値を得ることができます。errors.Is関数は内部でerrors.Unwrapを繰り返し呼び出すことで判定を実現しています。

ライブラリが独自の公開エラー型の値を返すときは、ラップしてもerrors.As関数で本来のエラー値を得られる

lib.go
package main // 本来は package lib な形式

import (
	"errors"
	"time"
)

// LibError はライブラリの独自エラー型
type LibError struct {
	kind     string
	orgError error
}

func (l *LibError) Error() string {
	return "error occured in Lib"
}

func (l *LibError) Kind() string {
	return l.kind
}

type LibTime time.Time

func ParseLibFunc(text string) (LibTime, error) {
	t, err := time.Parse(time.RFC3339, text)
	if err != nil {
		return LibTime{}, &LibError{kind: "Parse", orgError: err} // 独自エラー型の値を返す
	}
	return LibTime(t), nil
}
main.go
package main

import (
	"errors"
	"fmt"
)

func service(text string) error {
	_, err := ParseLibFunc(text)
	if err != nil {
		return fmt.Errorf("service error with '%w'", err) // Unwrapを実装したエラーを返す
	}
	fmt.Println("service finished successfully")
	return err
}

func main() {
	if err := service("2020/02/14"); err != nil {
		var e *LibError
		// errors.As 関数を使うことでライブラリの独自エラー型の本来の値を得ることができる
		if errors.As(err, e) {
			fmt.Println(e)        // error occurred in Lib
			fmt.Println(e.Kind()) // Parse
		}
	}
}

errors.As関数を使うことでライブラリ独自エラー型の値が実装するKind()メソッドを実行することができています。

アプリコード内のカスタムエラーはUnwrap() errorを実装しよう

上記の例ではservice関数はfmt.Errorf("%w", err)でUnwrapが実装されたエラー値を返していましたがアプリ内での独自エラー型を実装する場合はGo1.13時代以降はUnwrapメソッドを実装することが理想になります。

main.goの一部

// AppError はアプリコード側の独自エラー型
type AppError struct {
	orgErr error
	code   string
}

func (e AppError) Error() string {
	return fmt.Sprintf("code: %s, msg: app error occurred", e.code)
}

func (e AppError) Unwrap() error {
	return e.orgErr
}

func service(text string) error {
	_, err := ParseLibFunc(text)
	if err != nil {
		return AppError{orgErr: err, code: "00A"}
	}
	return err
}

func main() {
	if err := service("2020/02/14"); err != nil {
		fmt.Printf("%T\n", err)           // main.AppError
		fmt.Println(err)                  // code: 00A, msg: app error occurred
		wrappedErr := errors.Unwrap(err)
		fmt.Printf("%T\n", err)           // *main.LibError
		fmt.Println(wrappedErr)           // error occured in Lib

		fmt.Println(errors.Is(err, ErrParse)) // true
}

上記のようにUnwrap() errorのメソッドを実装すればアプリケーションのエラーハンドリングにてIs関数とAs関数をライブラリ提供のエラー値に対して適用することができます。

参考

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?