半年前にGo言語で開発を行うチームにJoinし、初めてGo言語を書く経験をしました。
自分やチームメンバー向けのメモとして、調べたことをまとめておきます。
1. Goのエラーハンドリングを考える上での前提知識(ざっくり)
- Go言語には、他の言語にあるような例外(throw, catch...) の機能が存在しない。
- Go言語は簡潔さを重視した言語のため、基本的に「例外は使うべきではない」という思想に基づいて作られている
- その代わり、メソッドのエラーハンドリングをしたい場合は、主にメソッドの返り値として
error
型の変数を返すことが推奨される- 主要なライブラリもこのルールに基づいて作られている
- 例:
json.Marshal
- jsonへのParseに失敗した場合、2番目の返り値としてerrを返す。
-
err
がnil
でなければ処理中に問題が起きたと判断し、呼び出し元でエラー対応を行う必要がある
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 ... }
参考記事
- golang error handling (Go1.13) <- すごくいい記事でした
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
を作成する
- なお、スタックトレースを出力することはできない
サンプルコード
- 例えば、標準パッケージのoserrorは以下の実装をしている
- src/internal/oserror/errors.go
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
型になる
- フォーマット指定子が
- フォーマット指定子に従ってフォーマットし、errorを返す
- なお、スタックトレースを出力することはできない
サンプルコード
- フォーマット指定子を使うところでは
fmt.Errorf
を使っている様子 - src/database/sql/sql.go
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. 他の手段
- スタックトレースが欲しい場合は、 pkg/errros や xerrorsなどの外部ライブラリを使用する必要あり
- 参考
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メソッドを持たせることで、一致判定を上書くこともできる
サンプルコード
- errors.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
を返す
- 見つかった場合、
-
target
はnil
ではないpointer
である必要あり。
- error chainを辿り、target に代入可能な最初のerrを見つける。
-
-
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() }