Go言語の標準ライブラリを使ったエラー処理のまとめ

Go言語の標準エラー処理が気になったので調べてみました。他のプログラミング言語におけるtry-catch
のような例外処理機構とは異なり、Go言語はエラーを明示的な戻り値として扱うという独自のアプローチを採用しています。本記事では、Go言語の標準ライブラリが提供するエラー処理の基本的な使い方を、いくつかのパターンに分けて解説します。
はじめに
Go言語におけるエラー処理は、C++などの言語とは違います。「戻り値でエラーを返す」っていうのはどうなの?と思っていましたが、実際に調べてみると、かなり使いやすいエラーハンドリングになっていることがわかりました。
本記事では、Goの基本的なエラーハンドリングについてサンプルプログラムで確認しながら解説します。
エラーハンドリング
エラーは戻り値として扱う
先ほど書いたように、Go言語ではエラーを戻り値として返すのが基本となります。`try-catch`のような方法がないので、エラーは戻り値で返すしかありません。
基本は、操作が成功した場合はnil
が返され、失敗した場合はnil
ではないerror
値を返します。
呼び出し側では、このerror
戻り値を確認し、nil
でない場合に適切なエラー処理を実装します。
このパターンが、Go言語のコードのエラーハンドリングになります。
以下、簡単な例です。
package main
import (
"errors"
"fmt"
)
// divideはaをbで割った商と余りを返す
func divide(a, b int) (int, error) {
if b == 0 {
// エラーが発生した場合、nilではないerror値を返す
return 0, errors.New("Division by zero: ゼロで割ることはできません")
}
// 成功した場合、結果とnilを返す
return a / b, nil
}
func main() {
// エラーが発生しないケース
fmt.Print("Case 1: ")
result, err := divide(10, 2)
if err != nil {
// エラーが発生した場合はここに到達する
fmt.Println("エラー:", err)
return
}
fmt.Print("OK.")
fmt.Println("結果:", result) // 結果: 5
fmt.Println("-------------------------")
fmt.Print("Case 2: ")
// エラーが発生するケース
result, err = divide(10, 0)
if err != nil {
// エラーが発生した場合はここに到達する
fmt.Println("エラー:", err) // エラー: ゼロで割ることはできません
return
}
fmt.Println("結果:", result)
}
Case 1: OK.結果: 5
-------------------------
Case 2: エラー: Division by zero: ゼロで割ることはできません
この例では、0除算が発生する場合はエラーを返します。エラーを呼び出し側で処理することでエラーを適切に扱うことができます。
2. エラーの生成
先ほどエラーを発生させるために使ったerrors.New()
以外にも、エラーを生成する関数が用意されています。ここでは、エラーを生成する関数を2つ紹介します
errors.New()
:エラー生成
errors.New
関数は、静的なエラーメッセージを持つ新しいエラーを生成する関数です。
errors.New
で生成したエラーを変数として定義すると以下のようなコードを記述することが可能です。このような定義を「センチネルエラー (Sentinel Error)」と呼ぶみたいです。
これを使うと、呼び出し側はerrors.Is
関数を用いてエラーの種類を確認することができるようになります。
以下、サンプルコードです
package main
import (
"errors"
"fmt"
)
// ErrNotFoundを定義
var ErrNotFound = errors.New("Item not found")
// findItem はスライスからアイテムを探します。
func findItem(items []string, target string) (string, error) {
for _, item := range items {
if item == target {
return item, nil
}
}
// 見つからない場合は ErrNotFound を返す
return "", ErrNotFound
}
func main() {
data := []string{"apple", "banana", "cherry"}
// 成功するケース
item, err := findItem(data, "banana")
if err != nil {
fmt.Println("エラー:", err)
return
}
fmt.Println("見つかったアイテム:", item) // 見つかったアイテム: banana
fmt.Println("---------------------------------")
// 失敗するケース (アイテムが見つからない)
item, err = findItem(data, "grape")
if err != nil {
// errors.Is を使って、返されたエラーが ErrNotFound であるかを確認
if errors.Is(err, ErrNotFound) {
fmt.Println("エラー: アイテムが見つかりませんでした。", "err =", err)
} else {
fmt.Println("その他のエラー:", err)
}
return
}
fmt.Println("見つかったアイテム:", item)
}
見つかったアイテム: banana
---------------------------------
エラー: アイテムが見つかりませんでした。 err = Item not found
fmt.Errorf()
:フォーマット付きエラー生成
fmt.Errorf
関数は、fmt.Printf
と同様にフォーマット文字列と引数を用いて、動的なエラーメッセージを作成する関数です。エラーメッセージに具体的な変数の値などを含めることができるので、エラーの種類によってはこちらの方が便利です。
package main
import "fmt"
var users []string
// addUserはユーザーをusersに追加する
func AddUser(username string) error {
if len(username) < 5 {
// 動的な情報 (username) を含むエラーメッセージを作成
return fmt.Errorf("ユーザー名 '%s' は短すぎます。5文字以上必要です。", username)
}
fmt.Printf("ユーザー '%s' を追加しました。\n", username)
users = append(users, username)
return nil
}
func main() {
// 成功するケース
err := AddUser("hogehoge")
if err != nil {
fmt.Println("エラー:", err)
}
fmt.Println("--------------------")
// 失敗するケース (ユーザー名が短い)
err = AddUser("hoge")
if err != nil {
fmt.Println("エラー:", err) // エラー: ユーザー名 'go' は短すぎます。5文字以上必要です。
}
// usersを表示
fmt.Println("--------------------")
fmt.Println("ユーザー:", users)
}
ユーザー 'hogehoge' を追加しました。
--------------------
エラー: ユーザー名 'hoge' は短すぎます。5文字以上必要です。
--------------------
ユーザー: [hogehoge]
3. エラーのラッピングとアンラッピング
ラッピングはエラーが連鎖的に発生する場合に、手前で発生したエラーを含めて呼び出し元に伝えるために利用します。
これには、%w
を利用します。
具体的なコードを見る方がわかりやすいのでコード例を示します。
package main
import (
"errors"
"fmt"
)
func div(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("divide by zero")
}
return a / b, nil
}
func average(datas []int) (int, error) {
sum := 0
for _, e := range datas {
sum += e
}
ave, err := div(sum, len(datas))
if err != nil {
return 0, fmt.Errorf("平均を計算中にエラーが発生しました: %w", err)
}
return ave, nil
}
func main() {
ave, err := average([]int{1, 2, 3, 4, 5})
if err != nil {
// ラップされたエラーは、%v で出力するとチェイン全体が表示される
fmt.Printf("最終的なエラー: %v\n", err)
return
}
fmt.Println("平均 : ", ave)
fmt.Println("------------------")
ave, err = average([]int{})
if err != nil {
// ラップされたエラーは、%v で出力するとチェイン全体が表示される
fmt.Printf("最終的なエラー: %v\n", err)
return
}
fmt.Println("平均 : ", ave)
}
fmt.Errorf("平均を計算中にエラーが発生しました: %w", err)
で、div
のエラーに新しいエラーを追加して、呼び出し元に返しています。
平均 : 3
------------------
最終的なエラー: 平均を計算中にエラーが発生しました: divide by zero
ラップしたエラーを、個別に表示したい場合には以下のようにアンラップします。
package main
import (
"errors"
"fmt"
)
func div(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("divide by zero")
}
return a / b, nil
}
func average(datas []int) (int, error) {
sum := 0
for _, e := range datas {
sum += e
}
ave, err := div(sum, len(datas))
if err != nil {
return 0, fmt.Errorf("平均を計算中にエラーが発生しました: %w", err)
}
return ave, nil
}
func main() {
ave, err := average([]int{1, 2, 3, 4, 5})
if err != nil {
for e := err; e != nil; e = errors.Unwrap(e) {
fmt.Printf(" - %v\n", e)
}
return
}
fmt.Println("平均 : ", ave)
fmt.Println("------------------")
ave, err = average([]int{})
if err != nil {
for e := err; e != nil; e = errors.Unwrap(e) {
fmt.Printf(" - %v\n", e)
}
return
}
fmt.Println("平均 : ", ave)
}
for e := err; e != nil; e = errors.Unwrap(e)
で、アンラップを行なっています。
これを実行すると以下のようになります。
平均 : 3
------------------
- 平均を計算中にエラーが発生しました: divide by zero
- divide by zero
特定のエラーと比較(errors.Is
)
errors.Is
関数を使うと、ラップされたエラーチェイン内に特定のエラーが含まれているかどうかをチェックできます。コードは以下のようになります。
package main
import (
"errors"
"fmt"
)
var ErrDivideByZero = errors.New("divide by zero")
func div(a, b int) (int, error) {
if b == 0 {
return 0, ErrDivideByZero
}
return a / b, nil
}
func average(datas []int) (int, error) {
sum := 0
for _, e := range datas {
sum += e
}
ave, err := div(sum, len(datas))
if err != nil {
return 0, fmt.Errorf("平均を計算中にエラーが発生しました: %w", err)
}
return ave, nil
}
func main() {
ave, err := average([]int{})
if err != nil {
if errors.Is(err, ErrDivideByZero) {
fmt.Println("要素数がゼロです")
} else {
fmt.Println("不明なエラー:", err)
}
return
}
fmt.Println("平均 : ", ave)
}
特定のエラー型へのキャスト(errors.As
)
errors.As
はカスタムエラーを定義した場合に利用できます。動的なエラーメッセージを受け取り側で判定したい場合は、errors.As
を使います。
以下のプログラムは、整数のスライスの平均を計算する際に、要素数が0だった場合(ゼロ除算)に対してカスタムエラーを定義し、errors.As
を使ってその型に応じたエラーハンドリングを行います。
package main
import (
"errors"
"fmt"
)
// カスタムエラー型
type DivideByZeroError struct {
a, b int
}
func (e *DivideByZeroError) Error() string {
return fmt.Sprintf("cannot divide a = %d by b = %d", e.a, e.b)
}
func div(a, b int) (int, error) {
if b == 0 {
return 0, &DivideByZeroError{a, b}
}
return a / b, nil
}
func average(datas []int) (int, error) {
sum := 0
for _, e := range datas {
sum += e
}
ave, err := div(sum, len(datas))
if err != nil {
return 0, fmt.Errorf("平均を計算中にエラーが発生しました: %w", err)
}
return ave, nil
}
func main() {
ave, err := average([]int{})
if err != nil {
var divideErr *DivideByZeroError
if errors.As(err, ÷Err) {
fmt.Println("要素数がゼロです", divideErr)
} else {
fmt.Println("不明なエラー:", err)
}
return
}
fmt.Println("平均 : ", ave)
}
このプログラムでは、標準の error
ではなく、自作の構造体 DivideByZeroError
を用意しています。a
と b
は除算の入力値で、ゼロ除算が発生した時にそれらの値を記録します。また、カスタムエラーにError()
メソッドを実装して error
インターフェースを満たすようにします。
これで、標準のerror
と同じように扱うことが可能になります。また、取り出すときはerros.As
関数を使います(main
参照)。
要素数がゼロです cannot divide a = 0 by b = 0
スタックトレースが欲しい!
エラーがどの行で発生したかのスタックトレースが欲しい場合は、標準ライブラリでは無理なようです。ここでは、cockroachdb/errors
を利用した例を紹介します。
package main
import (
"fmt"
cerrors "github.com/cockroachdb/errors"
)
func div(a, b int) (int, error) {
if b == 0 {
// スタックトレース付きエラーを生成
return 0, cerrors.New("divide by zero")
}
return a / b, nil
}
func average(datas []int) (int, error) {
sum := 0
for _, e := range datas {
sum += e
}
ave, err := div(sum, len(datas))
if err != nil {
// スタックトレースを保持したままラップ
return 0, cerrors.Wrap(err, "平均を計算中にエラーが発生しました")
}
return ave, nil
}
func main() {
ave, err := average([]int{}) // 要素数ゼロ → div(sum, 0) → divide by zero
if err != nil {
fmt.Printf("最終的なエラー:\n%+v\n", err)
return
}
fmt.Println("平均 : ", ave)
}
最終的なエラー:
平均を計算中にエラーが発生しました: divide by zero
(1) attached stack trace
-- stack trace:
| main.average
| /Users/tadanori/go/src/error/main.go:27
| [...repeated from below...]
Wraps: (2) 平均を計算中にエラーが発生しました
Wraps: (3) attached stack trace
-- stack trace:
| main.div
| /Users/tadanori/go/src/error/main.go:12
| main.average
| /Users/tadanori/go/src/error/main.go:24
| main.main
| /Users/tadanori/go/src/error/main.go:44
| runtime.main
| /usr/local/go/src/runtime/proc.go:272
| runtime.goexit
| /usr/local/go/src/runtime/asm_arm64.s:1223
Wraps: (4) divide by zero
Error types: (1) *withstack.withStack (2) *errutil.withPrefix (3) *withstack.withStack (4) *errutil.leafError
まとめ
Go言語のエラーハンドリングに調べて、まとめて見ました。try~catch
に慣れているとどうもしっくりこない部分もありますが、エラーの型を調べることも可能だし、カスタムのエラーを作ることも可能と、一通り必要な機能は揃っている気がします。
Go言語にはdefer
があるので、エラー発生次のファイルのCloseやメモリ解放などをcatch
節に書かなくていいので、案外このやり方が楽なのかもしれません。