Applibot Advent Calender 11日目の記事になります。
前日は @yucchiy さんの「社内イベント・インターンのゲームAIコンテストの裏側について解説」という記事でした!
#1. はじめに
6月からアプリボットでサーバサイドエンジニアとして内定者バイトをさせていただいています。
業務の一環として、Goのエラーハンドリング、特に1.13前後での変化についての勉強会を担当しました。(社員の方々の前で説明するのは緊張しました笑)
今回はその勉強会で扱った知識について記事にしていきます。
初投稿で緊張していますが、よろしくお願いします!
#2. Goにおけるエラー
Goは他のメジャーな言語とは異なり、Errorインターフェースを満たしたerror型が存在します。
type error interface {
Error() string
}
これを返り値の1つとして受け取ることでエラー処理を実現していています。
正常に動作している場合はerrorがnilとなり、逆にnil以外であれば関数内でエラーが発生していることがわかります。
他の言語では、関数から特殊な値(-1など)が返ってきた場合はエラー、といったやり方で表すことが多いので、Goのこの仕様はスマートで分かりやすいなと思っています。
##エラーの作成
エラー情報の生成には、errors.New
関数やfmt.Errorf
関数などを用います。
func sample1(f int) error {
if f < 1 {
return errors.New("fが1以上ではありません")
}
return nil
}
func sample2(f int) error {
if f < 1 {
return fmt.Errorf("fが1以上ではありません %d", f)
}
return nil
}
errors.New
関数は文字列をエラーとして扱うだけであり、fmt.Errorf
関数は通常のfmt.Printf
などのようにフォーマット指定子を使用することができます。
##エラーの拡張
上記の関数以外でも、Error() string
のメソッドを実装していれば(Errorインターフェースを満たしていれば)問題ないです。そのため、エラーに対して様々な情報を付与させることができます。
例えば以下のjsonパッケージでは、構造体にOffsetフィールドを追加することで、エラーの発生位置の保管を可能にしています。
type SyntaxError struct {
msg string // description of error
Offset int64 // error occurred after reading Offset bytes
}
func (e *SyntaxError) Error() string { return e.msg }
またインターフェースであるため、エラーに対して新規にメソッドを追加することも可能です。
##命名規則
Go言語ではErrorインターフェースを満たす構造体を実装する際に、以下の命名規則を適用することが推奨されています。
// 1.先頭に`err` または `Err` をつける
type errHogehoge {}
type ErrHogehoge {}
// 2. 末尾に `Error` をつける
type hogehogeError {}
エラーに関係しない構造体にこれらの命名規則を適用させてしまうと紛らわしくなってしまうため、チーム内で確認を取りながら気をつけましょう。
#3. Go1.13によって変化したエラーハンドリング
##エラーのラップ化
###ラップ化
Go1.13以降では、エラーをラップ(構造化)することができるようになりました。ラップというのは、エラーをエラーで、さながらお菓子を包み紙でラッピングするように包むことができるイメージです。
fmt.Errorf
関数内で**%w**を用いることで、エラー情報をラップすることができます。
fmt.Errorf("accessing DB: %w", err)
###Unwrapメソッドの追加
エラーのラップ化に伴い、それらの内部を確認するためのerrors.Unwrap
メソッドが実装されました。errors.Unwrap
メソッドを用いることで、ラップされたエラーの1つ内側にアクセスすることができます。
###ラップ化するメリット・デメリット
エラーラップするメリットとして、複数のエラーをまとめて保持できるという点があります。例えば、過去一定数のエラーを記録しておき利用するといったように様々な箇所で柔軟にエラー情報を取り扱えます。また、各階層での抽象度に合わせたエラーを利用することができます。
デメリットとしては、今後エラー情報に変更が生じた際に、それを利用している実装部分に不具合が発生してしまう可能性がある点です。
err := gaibu.SampleErrFunc()
if errors.Is(err, gaibu.SampleErr) {
// SampleErr発生時の処理
// SampleErrFuncに変更があった場合に影響が出てしまう
}
このコードでは、SampleErrFunc()
を実行し、エラー型としてSampleErrをラップする処理を行っています。もし、これを利用しているプロジェクトが異なるライブラリを採用する(関数を同一の処理を行う他のものに置き換える)場合、受け取るエラー情報が変わってしまうため、ラップされていたエラーが元のエラー情報と一致しません。そのため、ラップされたSampleErrを利用していた多くの部分に変更を加える必要があります。
そのような事態を回避するには、変更が予想されるエラーをラップせずに用いることが有効となります。
##エラーの比較
###errors.Isメソッド
Go1.13以前においては、特定のエラーが発生したかどうかを確かめたいときには、以下の例のように比較を行っていました。
var ErrExample = errors.New("example alert")
if err == ErrExample {
// example alert 発生時の処理
}
しかしGo1.13以降でErrExampleがラップされている場合、 単純な比較ではラップされたエラーと比較ができないため、errの中を再帰的に確認する必要があります。
Go1.13以降では比較を行う場合にerrors.Is
メソッドを用いることで、ラップ構造を意識せず簡単に実装することができます。
(メソッドの内部では再帰的にUnwrapが行われているようです。)
var ErrExample = errors.New("example alert")
if errors.Is(err, ErrExample) {
// example alert 発生時の処理
}
###errors.Asメソッド
コードを書き進める中で、特定のエラーの時のみ処理を変更したい場面が存在します。
Go1.13以前では、以下の例のようにエラーの型検証を行っています。
type SampleError struct {
Name string
}
func (e *SampleError) Error() string { return e.Name }
if e, ok := err.(*SampleError); ok {
// SampleError発生時の処理
}
Go1.13以降では型の検証を行う場合にerrors.As
を用いることができます。
こちらもerrors.Is
と同様に、ラップ構造を意識せず実装することができます。
type SampleError struct {
Name string
}
func (e *SampleError) Error() string { return e.Name }
if errors.As(err, &SampleError) {
// SampleError発生時の処理
}
#4.まとめと感想
今回はGoにおけるエラーハンドリングを紹介しました。プロジェクトの規模が大きくなっても柔軟に対応していけるよう様々な拡充がされていますね。
この勉強会のあと、実際のコードの中でどのように実装されているのかを確認してみました。詳しい内容はここに書くことができませんが、様々な場所で用いられていました。
業務の中でもこれらの知識をしっかりと反映させていけるよう頑張っていきます!
#参考サイト
以下のサイトの情報やサンプルコードを参考にさせていただきました。
#おわりに
Applibot Advent Calendar 2020 11日目の記事でした!
明日は @mrpc さんです!