search
LoginSignup
2
Help us understand the problem. What are the problem?

posted at

updated at

Go言語のエラーハンドリング(2022年版)

半年前にGo言語で開発を行うチームにJoinし、初めてGo言語を書く経験をしました。
自分やチームメンバー向けのメモとして、調べたことをまとめておきます。

1. Goのエラーハンドリングを考える上での前提知識(ざっくり)

  • Go言語には、他の言語にあるような例外(throw, catch...) の機能が存在しない。
    • Go言語は簡潔さを重視した言語のため、基本的に「例外は使うべきではない」という思想に基づいて作られている
  • その代わり、メソッドのエラーハンドリングをしたい場合は、主にメソッドの返り値として error 型の変数を返すことが推奨される
    • 主要なライブラリもこのルールに基づいて作られている
    • 例: json.Marshal
      • jsonへのParseに失敗した場合、2番目の返り値としてerrを返す。
      • errnil でなければ処理中に問題が起きたと判断し、呼び出し元でエラー対応を行う必要がある
s, err := json.Marshal(input)
if err != nil {
	panic(err)
}
  • とはいえ、複雑なエラーハンドリングに対応するには上記だけだと難しいため、Go言語のバージョンが上がるにつれ、「エラーのWrap」などのエラーハンドリング関連機能の強化が行われている
  • そのため、Goのバージョンによって推奨される書き方が異なることがある。過去のドキュメントを読む場合は注意が必要。
    • xerrors
      • Go公式がメンテナンスをしていた外部ライブラリ。2019.09のGo v1.13のリリース時に、機能の一部(errors.As, errors.Is, errors.Unwrapなど)が、標準errorsとfmtに取り込まれた。

参考記事

2. Goにおけるerrorとは

  • 以下の interface型 のこと。
type error interface {
    Error() string
}
  • つまり、 Error() というメソッドさえ持っていれば、どんな Struct であっても error 型を満たす値として扱うことができる
  • 例えばカスタムエラー *MyError は、下記のコードで定義できる
type MyError struct {
    // other fields
}
func (e *MyError) Error() string { return ... }

参考記事

3. error chain

  • 複雑なエラーハンドリングに対応するために、Go 1.13から追加されたのが error chain(エラーチェーン) という考え方です。
    • 例えば、孫メソッドでエラーが起きた時、子メソッドはそのエラーをWrapして、親メソッドに返す・・・ということができます。
func 子メソッド() error {
    ...
    if output, err := 孫メソッド(); err == nil {
        return fmt.Errorf("孫メソッドでエラーが起こりました 詳細:%w", err) //型は*fmt.wrapError
    }
    ...
}

func 親メソッド() {
    err := 子メソッド()
    // ここで errors.Is()や errors.As() を用いてエラー判定をする
}
  • Wrapされたerror(*fmt.wrapError)は、 Unwrap() を呼び出すことで、元のエラーを呼び出すことができます。
    • Unwrapを繰り返し呼び出すことで得られる一連のエラーのことをerror chain(エラーチェーン)と呼びます。

参考

4. エラーの生成

4-1. errors.New() エラーを新規作成する時の標準関数

説明

  • 公式ドキュメント errors.New()
    • func New(text string) error
    • 与えられたtextをもとに新しい error を作成する
  • なお、スタックトレースを出力することはできない

サンプルコード

package oserror

import "errors"

var (
	ErrInvalid    = errors.New("invalid argument")
	ErrPermission = errors.New("permission denied")
	ErrExist      = errors.New("file already exists")
	ErrNotExist   = errors.New("file does not exist")
	ErrClosed     = errors.New("file already closed")
)

4-2. fmt.Errorf() エラーを生成 or Wrapする時に用いる標準関数

説明

  • 公式ドキュメント fmt.Errorf
    • フォーマット指定子に従ってフォーマットし、errorを返す
      • フォーマット指定子が %w を含む場合、返されたerrorはUnwrapメソッドを実装します。
      • この場合、返り値は*fmt.wrapError 型になる
  • なお、スタックトレースを出力することはできない

サンプルコード

	if rs.lastcols == nil {
		return errors.New("sql: Scan called without calling Next")
	}
	if len(dest) != len(rs.lastcols) {
		return fmt.Errorf("sql: expected %d destination arguments in Scan, not %d", len(rs.lastcols), len(dest))
	}

4-3. 他の手段

5. エラーの判定

5-1. err != nil エラーがあるかないか判定

  • エラーの型判定をしたいのでなければ、この書き方で問題ありません。
val, err := myFunction( args... );
if err != nil {
  // エラーハンドリング
}

5-2. errors.Is エラーを値レベルで比較し、一致判定

説明

  • 公式ドキュメント errors.Is
    • func errors.Is(err, target error) bool
    • error chainを辿り、いずれかのエラーが対象と一致するかどうかを判定
  • 値レベルの比較のため、errorの型が同じでも値が異なる場合は一致判定されない
    • 例えば、以下の APIError を使用した場合、HTTPStatusCode が異なれば一致判定されない
type APIError struct {
	HTTPStatusCode int
}

func (e *APIError) Error() string {
	return fmt.Sprintf("...")
}
  • errors.New() で作られたエラーなど、値を持たない場合は問題なく判定できる様子
  • カスタムエラー(APIError)自体にIsメソッドを持たせることで、一致判定を上書くこともできる

サンプルコード

if _, err := os.Open("non-existing"); err != nil {
	if errors.Is(err, fs.ErrNotExist) {
		fmt.Println("file does not exist")
	} else {
		fmt.Println(err)
	}
}

ちなみに package fs のエラーの実装(リンク

src/io/fs/fs.go
var (
	ErrNotExist   = errNotExist()   // "file does not exist"
)
func errNotExist() error   { return oserror.ErrNotExist } // errors.New("file already exists")

参考記事

5-3. errors.As 比較対象を型レベルで比較し、データを取り出す

説明

  • 公式ドキュメント errors.As
    • func errors.As(err error, target interface{}) bool
      • error chainを辿り、target に代入可能な最初のerrを見つける。
        • 見つかった場合、 target をそのエラー値に設定してtrueを返す
        • 見つからなかった場合は falseを返す
      • targetnil ではない pointer である必要あり。
  • errors.Is とかなり記法が異なるので注意。特に第二引数にpointerを渡す点が最初はつまづきやすい

サンプルコード

if _, err := os.Open("non-existing"); err != nil {
	var pathError *fs.PathError
	if errors.As(err, &pathError) {
		fmt.Println("Failed at path:", pathError.Path)
	} else {
		fmt.Println(err)
	}
}

ちなみに package fs のエラーの実装(リンク

src/io/fs/fs.go
type PathError struct {
	Op   string
	Path string
	Err  error
}

func (e *PathError) Error() string { return e.Op + " " + e.Path + ": " + e.Err.Error() }

参考記事:

他参考記事

Register as a new user and use Qiita more conveniently

  1. You can follow users and tags
  2. you can stock useful information
  3. You can make editorial suggestions for articles
What you can do with signing up
2
Help us understand the problem. What are the problem?