Go言語入門:効果的なGo -Errors-

スポンサーリンク
Go言語入門:効果的なGo -Errors- ノウハウ
Go言語入門:効果的なGo -Errors-
この記事は約28分で読めます。
よっしー
よっしー

こんにちは。よっしーです(^^)

本日は、Go言語を効果的に使うためのガイドラインについて解説しています。

スポンサーリンク

背景

Go言語を学び始めて、より良いコードを書きたいと思い、Go言語の公式ドキュメント「Effective Go」を知りました。これは、いわば「Goらしいコードの書き方指南書」になります。単に動くコードではなく、効率的で保守性の高いコードを書くためのベストプラクティスが詰まっているので、これを読んだ時の内容を備忘として残しました。

エラー

ライブラリルーチンは、呼び出し元に何らかのエラー表示を返すことがしばしば必要です。先に述べたように、Goの複数値返却により、通常の戻り値と一緒に詳細なエラー説明を返すことが簡単になります。この機能を使用して詳細なエラー情報を提供することは良いスタイルです。例えば、見ていくように、os.Openは失敗時に単にnilポインターを返すだけでなく、何が間違ったかを説明するエラー値も返します。

慣例により、エラーはerror型を持ち、これは単純な組み込みインターフェースです。

type error interface {
    Error() string
}

ライブラリ作成者は、このインターフェースをより豊富なモデルで実装する自由があり、エラーを見るだけでなく、いくつかのコンテキストも提供することが可能になります。先に述べたように、通常の*os.File戻り値と一緒に、os.Openはエラー値も返します。ファイルが正常に開かれた場合、エラーはnilになりますが、問題がある場合は、os.PathErrorを保持します:

// PathErrorはエラーと、それを引き起こした操作およびファイルパスを記録します。
type PathError struct {
    Op string    // "open", "unlink"など。
    Path string  // 関連するファイル。
    Err error    // システムコールによって返される。
}

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

PathErrorErrorは次のような文字列を生成します:

open /etc/passwx: no such file or directory

このようなエラーは、問題のあるファイル名、操作、それが引き起こしたオペレーティングシステムエラーを含み、それを引き起こした呼び出しから遠く離れて印刷されても有用です。単純な「no such file or directory」よりもはるかに有益です。

可能な場合、エラー文字列は、エラーを生成した操作やパッケージを命名するプレフィックスを持つなどして、その起源を特定すべきです。例えば、パッケージimageでは、未知の形式による復号化エラーの文字列表現は「image: unknown format」です。

正確なエラーの詳細に関心のある呼び出し元は、型スイッチまたは型アサーションを使用して特定のエラーを探し、詳細を抽出できます。PathErrorsの場合、これには回復可能な失敗のために内部のErrフィールドを調べることが含まれるかもしれません。

for try := 0; try < 2; try++ {
    file, err = os.Create(filename)
    if err == nil {
        return
    }
    if e, ok := err.(*os.PathError); ok && e.Err == syscall.ENOSPC {
        deleteTempFiles()  // スペースを回復。
        continue
    }
    return
}

ここでの2番目のif文は別の型アサーションです。失敗した場合、okfalseになり、enilになります。成功した場合、oktrueになり、これはエラーが*os.PathError型であったことを意味し、それからeもそうなので、エラーについてより多くの情報を調べることができます。

Goのエラーハンドリングの基本

Goではエラーは値として扱われます。例外(exception)ではなく、通常の戻り値として返されます。

基本的なエラーの使用例

package main

import (
    "fmt"
    "os"
)

func basicErrorExample() {
    fmt.Println("=== 基本的なエラーの例 ===")
    
    // ファイルを開く(存在しないファイル)
    file, err := os.Open("存在しないファイル.txt")
    if err != nil {
        fmt.Printf("エラー発生: %v\n", err)
        fmt.Printf("エラー型: %T\n", err)
        return
    }
    defer file.Close()
    
    fmt.Println("ファイルが正常に開かれました")
}

func main() {
    basicErrorExample()
}

errorインターフェースの実装

package main

import (
    "fmt"
    "time"
)

// カスタムエラー型
type MyError struct {
    Time    time.Time
    Message string
    Code    int
}

// errorインターフェースを実装
func (e *MyError) Error() string {
    return fmt.Sprintf("[%s] エラーコード %d: %s", 
        e.Time.Format("2006-01-02 15:04:05"), e.Code, e.Message)
}

// エラーを返す関数
func doSomething(value int) error {
    if value < 0 {
        return &MyError{
            Time:    time.Now(),
            Message: "負の値は許可されていません",
            Code:    400,
        }
    }
    
    if value > 100 {
        return &MyError{
            Time:    time.Now(),
            Message: "値が大きすぎます",
            Code:    413,
        }
    }
    
    return nil // エラーなし
}

func customErrorExample() {
    fmt.Println("=== カスタムエラーの例 ===")
    
    testValues := []int{-5, 50, 150}
    
    for _, value := range testValues {
        if err := doSomething(value); err != nil {
            fmt.Printf("値 %d: %v\n", value, err)
            
            // 型アサーションでカスタムエラーの詳細にアクセス
            if myErr, ok := err.(*MyError); ok {
                fmt.Printf("  詳細情報 - コード: %d, 時刻: %s\n", 
                    myErr.Code, myErr.Time.Format("15:04:05"))
            }
        } else {
            fmt.Printf("値 %d: 正常に処理されました\n", value)
        }
    }
}

func main() {
    customErrorExample()
}

より複雑なエラー型の例

package main

import (
    "fmt"
    "net"
    "os"
    "syscall"
)

// ネットワークエラーを模擬
type NetworkError struct {
    Operation string
    Network   string
    Address   string
    Err       error
}

func (e *NetworkError) Error() string {
    return fmt.Sprintf("%s %s %s: %v", e.Operation, e.Network, e.Address, e.Err)
}

// ファイル処理エラーを模擬
type FileProcessError struct {
    Operation string
    Filename  string
    LineNumber int
    Err       error
}

func (e *FileProcessError) Error() string {
    if e.LineNumber > 0 {
        return fmt.Sprintf("%s %s (行 %d): %v", e.Operation, e.Filename, e.LineNumber, e.Err)
    }
    return fmt.Sprintf("%s %s: %v", e.Operation, e.Filename, e.Err)
}

// ネットワーク接続をシミュレート
func connectToServer(network, address string) error {
    // 失敗をシミュレート
    if address == "invalid-server:8080" {
        return &NetworkError{
            Operation: "connect",
            Network:   network,
            Address:   address,
            Err:       syscall.ECONNREFUSED,
        }
    }
    
    return nil
}

// ファイル処理をシミュレート
func processFile(filename string) error {
    // ファイルが存在しない場合
    if filename == "missing.txt" {
        return &FileProcessError{
            Operation: "read",
            Filename:  filename,
            Err:       os.ErrNotExist,
        }
    }
    
    // 処理中にエラーが発生した場合
    if filename == "corrupt.txt" {
        return &FileProcessError{
            Operation:  "parse",
            Filename:   filename,
            LineNumber: 42,
            Err:        fmt.Errorf("不正なJSON形式"),
        }
    }
    
    return nil
}

func complexErrorExample() {
    fmt.Println("=== 複雑なエラー型の例 ===")
    
    // ネットワークエラーのテスト
    servers := []string{"valid-server:8080", "invalid-server:8080"}
    
    for _, server := range servers {
        if err := connectToServer("tcp", server); err != nil {
            fmt.Printf("接続エラー: %v\n", err)
            
            // ネットワークエラーの詳細情報を取得
            if netErr, ok := err.(*NetworkError); ok {
                fmt.Printf("  ネットワーク: %s\n", netErr.Network)
                fmt.Printf("  アドレス: %s\n", netErr.Address)
                fmt.Printf("  システムエラー: %v\n", netErr.Err)
            }
        } else {
            fmt.Printf("サーバー %s に正常に接続\n", server)
        }
    }
    
    fmt.Println()
    
    // ファイルエラーのテスト
    files := []string{"valid.txt", "missing.txt", "corrupt.txt"}
    
    for _, file := range files {
        if err := processFile(file); err != nil {
            fmt.Printf("ファイルエラー: %v\n", err)
            
            // ファイルエラーの詳細情報を取得
            if fileErr, ok := err.(*FileProcessError); ok {
                fmt.Printf("  操作: %s\n", fileErr.Operation)
                fmt.Printf("  ファイル名: %s\n", fileErr.Filename)
                if fileErr.LineNumber > 0 {
                    fmt.Printf("  行番号: %d\n", fileErr.LineNumber)
                }
            }
        } else {
            fmt.Printf("ファイル %s を正常に処理\n", file)
        }
    }
}

func main() {
    complexErrorExample()
}

実用的なエラーハンドリング:リトライ機能

package main

import (
    "fmt"
    "math/rand"
    "syscall"
    "time"
)

// リトライ可能なエラー
type RetryableError struct {
    Operation string
    Attempt   int
    Cause     error
}

func (e *RetryableError) Error() string {
    return fmt.Sprintf("%s (試行 %d): %v", e.Operation, e.Attempt, e.Cause)
}

// 一時的なエラー
type TemporaryError struct {
    Message string
}

func (e *TemporaryError) Error() string {
    return e.Message
}

func (e *TemporaryError) Temporary() bool {
    return true
}

// ネットワーク操作をシミュレート
func networkOperation() error {
    // ランダムにエラーを発生
    switch rand.Intn(4) {
    case 0:
        return nil // 成功
    case 1:
        return &TemporaryError{"一時的なネットワークエラー"}
    case 2:
        return syscall.ECONNREFUSED
    default:
        return fmt.Errorf("予期しないエラー")
    }
}

// リトライ機能付きの関数
func reliableNetworkOperation(maxRetries int) error {
    for attempt := 1; attempt <= maxRetries; attempt++ {
        err := networkOperation()
        if err == nil {
            fmt.Printf("試行 %d: 成功\n", attempt)
            return nil
        }
        
        fmt.Printf("試行 %d: エラー - %v\n", attempt, err)
        
        // エラーの種類に応じてリトライを判断
        shouldRetry := false
        
        // 型アサーションでエラーの種類を確認
        if tempErr, ok := err.(interface{ Temporary() bool }); ok && tempErr.Temporary() {
            shouldRetry = true
            fmt.Printf("  -> 一時的なエラーのためリトライします\n")
        } else if err == syscall.ECONNREFUSED {
            shouldRetry = true
            fmt.Printf("  -> 接続拒否のためリトライします\n")
        } else {
            fmt.Printf("  -> 回復不可能なエラーです\n")
            return &RetryableError{
                Operation: "network operation",
                Attempt:   attempt,
                Cause:     err,
            }
        }
        
        if !shouldRetry || attempt == maxRetries {
            return &RetryableError{
                Operation: "network operation",
                Attempt:   attempt,
                Cause:     err,
            }
        }
        
        // 指数バックオフでリトライ間隔を調整
        delay := time.Duration(attempt) * 500 * time.Millisecond
        fmt.Printf("  -> %v 後にリトライします\n", delay)
        time.Sleep(delay)
    }
    
    return fmt.Errorf("最大リトライ回数に達しました")
}

func retryExample() {
    fmt.Println("=== リトライ機能の例 ===")
    
    for i := 1; i <= 3; i++ {
        fmt.Printf("\n--- テスト %d ---\n", i)
        
        err := reliableNetworkOperation(3)
        if err != nil {
            fmt.Printf("最終結果: 失敗 - %v\n", err)
            
            // RetryableErrorの詳細情報を表示
            if retryErr, ok := err.(*RetryableError); ok {
                fmt.Printf("詳細: 操作=%s, 最終試行=%d, 原因=%v\n", 
                    retryErr.Operation, retryErr.Attempt, retryErr.Cause)
            }
        } else {
            fmt.Printf("最終結果: 成功\n")
        }
        
        time.Sleep(1 * time.Second)
    }
}

func main() {
    retryExample()
}

エラーのラッピングとアンラッピング(Go 1.13+)

package main

import (
    "errors"
    "fmt"
    "os"
)

// カスタムエラー
type ValidationError struct {
    Field string
    Value interface{}
    Err   error
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("validation failed for field '%s' with value '%v': %v", 
        e.Field, e.Value, e.Err)
}

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

// データ検証関数
func validateAge(age int) error {
    if age < 0 {
        return &ValidationError{
            Field: "age",
            Value: age,
            Err:   errors.New("年齢は負の値にできません"),
        }
    }
    
    if age > 150 {
        return &ValidationError{
            Field: "age",
            Value: age,
            Err:   errors.New("年齢が現実的ではありません"),
        }
    }
    
    return nil
}

func validateName(name string) error {
    if name == "" {
        return &ValidationError{
            Field: "name",
            Value: name,
            Err:   errors.New("名前は空にできません"),
        }
    }
    
    return nil
}

// 複合的な検証
func validateUser(name string, age int) error {
    if err := validateName(name); err != nil {
        return fmt.Errorf("ユーザー検証失敗: %w", err)
    }
    
    if err := validateAge(age); err != nil {
        return fmt.Errorf("ユーザー検証失敗: %w", err)
    }
    
    return nil
}

// ファイル処理の例
func processUserFile(filename string) error {
    file, err := os.Open(filename)
    if err != nil {
        return fmt.Errorf("ユーザーファイルの処理に失敗: %w", err)
    }
    defer file.Close()
    
    // ファイル処理をシミュレート
    return nil
}

func errorWrappingExample() {
    fmt.Println("=== エラーのラッピングとアンラッピングの例 ===")
    
    // 検証エラーのテスト
    testCases := []struct {
        name string
        age  int
    }{
        {"太郎", 25},    // 正常
        {"", 30},       // 名前が空
        {"花子", -5},    // 年齢が負
        {"次郎", 200},   // 年齢が異常
    }
    
    for _, tc := range testCases {
        err := validateUser(tc.name, tc.age)
        if err != nil {
            fmt.Printf("エラー: %v\n", err)
            
            // エラーチェーンを辿る
            var validationErr *ValidationError
            if errors.As(err, &validationErr) {
                fmt.Printf("  検証エラーの詳細: フィールド=%s, 値=%v\n", 
                    validationErr.Field, validationErr.Value)
            }
            
            // 特定のエラーをチェック
            if errors.Is(err, os.ErrNotExist) {
                fmt.Printf("  ファイルが存在しません\n")
            }
        } else {
            fmt.Printf("ユーザー %s (年齢 %d): 検証成功\n", tc.name, tc.age)
        }
    }
    
    fmt.Println()
    
    // ファイルエラーのテスト
    if err := processUserFile("存在しないファイル.txt"); err != nil {
        fmt.Printf("ファイル処理エラー: %v\n", err)
        
        // ラップされたエラーを確認
        if errors.Is(err, os.ErrNotExist) {
            fmt.Printf("  根本原因: ファイルが存在しません\n")
        }
        
        // エラーチェーンを表示
        current := err
        level := 0
        for current != nil {
            fmt.Printf("  レベル %d: %v\n", level, current)
            current = errors.Unwrap(current)
            level++
        }
    }
}

func main() {
    errorWrappingExample()
}

実用的なエラーハンドリングパターン

package main

import (
    "context"
    "fmt"
    "math/rand"
    "sync"
    "time"
)

// 結果とエラーを組み合わせた構造体
type Result struct {
    Data  string
    Error error
}

// 複数の処理を並行実行し、エラーを集約
func processMultipleTasks(tasks []string) ([]string, []error) {
    var results []string
    var errors []error
    var mu sync.Mutex
    var wg sync.WaitGroup
    
    for i, task := range tasks {
        wg.Add(1)
        go func(id int, taskData string) {
            defer wg.Done()
            
            // タスク処理をシミュレート
            time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)
            
            var result string
            var err error
            
            if rand.Float32() < 0.3 { // 30%の確率でエラー
                err = fmt.Errorf("タスク %d '%s' でエラーが発生", id, taskData)
            } else {
                result = fmt.Sprintf("処理済み: %s", taskData)
            }
            
            mu.Lock()
            if err != nil {
                errors = append(errors, err)
            } else {
                results = append(results, result)
            }
            mu.Unlock()
            
            fmt.Printf("タスク %d 完了: %s (エラー: %v)\n", id, taskData, err)
        }(i, task)
    }
    
    wg.Wait()
    return results, errors
}

// コンテキストを使用したキャンセル可能な処理
func processWithTimeout(ctx context.Context, data string) error {
    // 長時間の処理をシミュレート
    for i := 0; i < 10; i++ {
        select {
        case <-ctx.Done():
            return fmt.Errorf("処理がキャンセルされました: %w", ctx.Err())
        default:
            time.Sleep(200 * time.Millisecond)
            fmt.Printf("  %s の処理中... (%d/10)\n", data, i+1)
        }
    }
    
    return nil
}

func practicalErrorHandlingExample() {
    fmt.Println("=== 実用的なエラーハンドリングの例 ===")
    
    // 1. 並行処理でのエラー集約
    fmt.Println("--- 並行処理でのエラー集約 ---")
    tasks := []string{"タスクA", "タスクB", "タスクC", "タスクD", "タスクE"}
    
    results, errors := processMultipleTasks(tasks)
    
    fmt.Printf("\n成功した処理: %d個\n", len(results))
    for i, result := range results {
        fmt.Printf("  %d: %s\n", i+1, result)
    }
    
    fmt.Printf("\nエラーが発生した処理: %d個\n", len(errors))
    for i, err := range errors {
        fmt.Printf("  %d: %v\n", i+1, err)
    }
    
    // 2. タイムアウト付きの処理
    fmt.Println("\n--- タイムアウト付きの処理 ---")
    
    // 3秒でタイムアウト
    ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
    defer cancel()
    
    err := processWithTimeout(ctx, "重要なデータ")
    if err != nil {
        fmt.Printf("処理エラー: %v\n", err)
        
        // タイムアウトエラーかどうかをチェック
        if ctx.Err() == context.DeadlineExceeded {
            fmt.Println("  原因: タイムアウト")
        } else if ctx.Err() == context.Canceled {
            fmt.Println("  原因: キャンセル")
        }
    } else {
        fmt.Println("処理が正常に完了しました")
    }
}

func main() {
    practicalErrorHandlingExample()
}

重要なポイント

1. Goのエラーハンドリングの哲学

  • エラーは値:例外ではなく通常の戻り値
  • 明示的な処理:エラーは無視できない
  • 詳細な情報:コンテキストを含む有用なエラーメッセージ

2. errorインターフェース

type error interface {
    Error() string
}

シンプルな設計で拡張性が高い

3. エラーの作成パターン

  • fmt.Errorf: 簡単なエラーメッセージ
  • errors.New: 固定メッセージのエラー
  • カスタム型: 構造化された詳細情報

4. エラーの検査方法

  • 基本チェック: if err != nil
  • 型アサーション: err.(*SpecificError)
  • errors.Is: エラーの同一性チェック(Go 1.13+)
  • errors.As: 特定の型へのキャスト(Go 1.13+)

5. 良いエラーメッセージの特徴

  • 起源の特定: どのパッケージ・操作から発生したか
  • コンテキスト: 何を処理していたときのエラーか
  • 詳細情報: 問題の特定に必要な情報

6. 実用的なパターン

  • エラーラッピング: fmt.Errorf("context: %w", err)
  • リトライ機能: 一時的なエラーの再試行
  • エラー集約: 複数の処理のエラーをまとめる
  • タイムアウト処理: context.Contextとの組み合わせ

Goのエラーハンドリングは、シンプルでありながら強力で、明示的なエラー処理により堅牢なプログラムを書くことができます。

おわりに 

本日は、Go言語を効果的に使うためのガイドラインについて解説しました。

よっしー
よっしー

何か質問や相談があれば、コメントをお願いします。また、エンジニア案件の相談にも随時対応していますので、お気軽にお問い合わせください。

それでは、また明日お会いしましょう(^^)

コメント

タイトルとURLをコピーしました