コンテンツにスキップ

Go/Standard library/errors

出典: フリー教科書『ウィキブックス(Wikibooks)』

はじめに

[編集]

Goにおけるエラーハンドリングは、例外処理ではなく明示的なエラー値を返すことで行われます。errorsパッケージは、このエラーハンドリングの中核となる機能を提供し、エラーの作成、ラップ、アンラップなどの操作を可能にします。

基本的なエラーの作成

[編集]

errors.New()

[編集]

最も基本的なエラーの作成方法はerrors.New()関数を使用することです。

package main

import (
	"errors"
	"fmt"
)

func divide(x, y float64) (quotient float64, err error) {
	switch {
	case x == 0 && y == 0:
		err = errors.New("division zero by zero")
	case y == 0:
		err = errors.New("division by zero")
	default:
		quotient = x / y
	}
	return
}

func main() {
	if result, err := divide(10, 0); err != nil {
		fmt.Printf("エラーが発生しました: %v\n", err)
	} else {
		fmt.Printf("結果: %f\n", result)
	}
}

fmt.Errorf()

[編集]

より柔軟なエラーメッセージを作成したい場合は、fmt.Errorf()を使用します。

func validateAge(age int) (err error) {
	switch {
	case age < 0:
		err = fmt.Errorf("年齢は負の値にできません: %d", age)
	case age > 150:
		err = fmt.Errorf("年齢が異常に大きいです: %d", age)
	}
	return
}

エラーのラップ(Wrapping)

[編集]

Go 1.13以降、エラーをラップする機能が追加されました。これにより、エラーの文脈を保持しながら新しいエラー情報を追加できます。

fmt.Errorf()による基本的なラップ

[編集]
package main

import (
	"fmt"
	"os"
)

func readConfig(filename string) error {
	if file, err := os.Open(filename); err != nil {
		return fmt.Errorf("設定ファイルを読み込めません: %w", err)
	} else {
		file.Close()
	}
	return nil
}

func main() {
	if err := readConfig("config.txt"); err != nil {
		fmt.Printf("エラー: %v\n", err)
	}
}

%w動詞を使用することで、元のエラーをラップできます。

複数レベルのラップ

[編集]
func loadDatabase() error {
	if err := connectToDatabase(); err != nil {
		return fmt.Errorf("データベース接続に失敗: %w", err)
	}
	return nil
}

func connectToDatabase() error {
	// 実際のデータベース接続処理
	return fmt.Errorf("接続タイムアウト")
}

func initializeApp() error {
	if err := loadDatabase(); err != nil {
		return fmt.Errorf("アプリケーション初期化に失敗: %w", err)
	}
	return nil
}

エラーのアンラップ(Unwrapping)

[編集]

errors.Unwrap()

[編集]

ラップされたエラーから元のエラーを取得するにはerrors.Unwrap()を使用します。

func main() {
	if err := initializeApp(); err != nil {
		fmt.Printf("最上位エラー: %v\n", err)

		// 一段階アンラップ
		if unwrapped := errors.Unwrap(err); unwrapped != nil {
			fmt.Printf("アンラップされたエラー: %v\n", unwrapped)
		}
	}
}

errors.Is()

[編集]

特定のエラーが含まれているかチェックするにはerrors.Is()を使用します。

var ErrNotFound = errors.New("データが見つかりません")

func findUser(id int) error {
    if id == 0 {
        return fmt.Errorf("無効なユーザーID: %w", ErrNotFound)
    }
    return nil
}

func main() {
    if err := findUser(0); errors.Is(err, ErrNotFound) {
        fmt.Println("ユーザーが見つかりませんでした")
    }
}

errors.As()

[編集]

エラーを特定の型にキャストするにはerrors.As()を使用します。

type ValidationError struct {
    Field string
    Value interface{}
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("バリデーションエラー: フィールド '%s' の値 '%v' が無効です", e.Field, e.Value)
}

func validateUser(name string, age int) error {
    if age < 0 {
        return &ValidationError{Field: "age", Value: age}
    }
    return nil
}

func main() {
    if err := validateUser("太郎", -1); err != nil {
        var validationErr *ValidationError
        if errors.As(err, &validationErr) {
            fmt.Printf("バリデーションエラーが発生: %s\n", validationErr.Field)
        }
    }
}

カスタムエラー型

[編集]

基本的なカスタムエラー

[編集]
type FileError struct {
    Filename string
    Operation string
    Err error
}

func (e *FileError) Error() string {
    return fmt.Sprintf("ファイル '%s' での %s 操作に失敗: %v", e.Filename, e.Operation, e.Err)
}

func (e *FileError) Unwrap() error {
    return e.Err
}

func readFile(filename string) error {
    if _, err := os.Open(filename); err != nil {
        return &FileError{
            Filename: filename,
            Operation: "読み込み",
            Err: err,
        }
    }
    return nil
}

エラーチェーンの実装

[編集]
type ChainedError struct {
    Message string
    Cause   error
}

func (e *ChainedError) Error() string {
    if e.Cause != nil {
        return fmt.Sprintf("%s: %v", e.Message, e.Cause)
    }
    return e.Message
}

func (e *ChainedError) Unwrap() error {
    return e.Cause
}

func processData(data []byte) error {
    if len(data) == 0 {
        return &ChainedError{
            Message: "データ処理に失敗",
            Cause:   errors.New("空のデータ"),
        }
    }
    return nil
}

エラーハンドリングのベストプラクティス

[編集]

エラーを無視しない

[編集]
// 悪い例
file, _ := os.Open("config.txt")

// 良い例
file, err := os.Open("config.txt")
if err != nil {
    return fmt.Errorf("設定ファイルを開けません: %w", err)
}
defer file.Close()

適切なエラーメッセージ

[編集]
// 悪い例
func validateInput(input string) error {
    if input == "" {
        return errors.New("エラー")
    }
    return nil
}

// 良い例
func validateInput(input string) error {
    if input == "" {
        return errors.New("入力値が空です")
    }
    return nil
}

エラーの早期返却

[編集]
func processUser(userID int) error {
    user, err := getUser(userID)
    if err != nil {
        return fmt.Errorf("ユーザー取得に失敗: %w", err)
    }
    
    if err := validateUser(user); err != nil {
        return fmt.Errorf("ユーザー検証に失敗: %w", err)
    }
    
    if err := saveUser(user); err != nil {
        return fmt.Errorf("ユーザー保存に失敗: %w", err)
    }
    
    return nil
}

エラーのロギング

[編集]

構造化ログ

[編集]
import (
    "log"
    "fmt"
)

func handleError(err error) {
    if err != nil {
        log.Printf("エラーが発生しました: %v", err)
        
        // エラーチェーンを辿る
        current := err
        for current != nil {
            log.Printf("  - %v", current)
            current = errors.Unwrap(current)
        }
    }
}

エラーレベルの分類

[編集]
type ErrorLevel int

const (
    ErrorLevelInfo ErrorLevel = iota
    ErrorLevelWarning
    ErrorLevelError
    ErrorLevelFatal
)

type LeveledError struct {
    Level   ErrorLevel
    Message string
    Err     error
}

func (e *LeveledError) Error() string {
    return fmt.Sprintf("[%s] %s: %v", e.levelString(), e.Message, e.Err)
}

func (e *LeveledError) levelString() string {
    switch e.Level {
    case ErrorLevelInfo:
        return "INFO"
    case ErrorLevelWarning:
        return "WARNING"
    case ErrorLevelError:
        return "ERROR"
    case ErrorLevelFatal:
        return "FATAL"
    default:
        return "UNKNOWN"
    }
}

テストでのエラーハンドリング

[編集]

エラーの検証

[編集]
func TestDivideByZero(t *testing.T) {
    _, err := divide(10, 0)
    if err == nil {
        t.Error("ゼロ除算でエラーが発生しませんでした")
    }
    
    expected := "division by zero"
    if err.Error() != expected {
        t.Errorf("期待されたエラーメッセージ: %s, 実際: %s", expected, err.Error())
    }
}

エラーの型チェック

[編集]
func TestValidationError(t *testing.T) {
    err := validateUser("", -1)
    
    var validationErr *ValidationError
    if !errors.As(err, &validationErr) {
        t.Error("ValidationErrorが期待されましたが、異なる型でした")
    }
    
    if validationErr.Field != "age" {
        t.Errorf("期待されたフィールド: age, 実際: %s", validationErr.Field)
    }
}

まとめ

[編集]

errorsパッケージは、Goにおけるエラーハンドリングの基盤を提供します。適切なエラーの作成、ラップ、アンラップの技術を習得することで、保守性が高く、デバッグしやすいコードを書くことができます。

重要なポイント:
  • エラーは値として扱い、明示的にチェックする
  • fmt.Errorf()%wを使用してエラーをラップする
  • errors.Is()errors.As()を使用してエラーを適切に判定する
  • カスタムエラー型を定義して、より詳細な情報を提供する
  • エラーメッセージは明確で実用的にする
  • テストでエラーハンドリングを検証する

これらの原則に従うことで、堅牢で保守性の高いGoアプリケーションを開発できます。