第13章 エラー処理

エラー処理の基本

Go言語における「エラー処理」は、プログラムの実行中に発生する問題に適切に対応するための重要な概念です。Goでは、エラーを値として扱い、エラーハンドリングはプログラムの正常なフローの一部とされています。以下に、エラー処理の基本に関する具体的な事例を示します。


エラー型

エラーはerrorインターフェイスを実装する任意の型として表現されます。errorインターフェイスには、Error()メソッドが定義されており、これはエラーメッセージを表す文字列を返します。

package main

import (
    "errors"
    "fmt"
    "log"
)

func canDivide(a, b int) (int, error) {
    if b == 0 {
        return 0, errors.New("cannot divide by zero")
    }
    return a / b, nil
}

func main() {
    result, err := canDivide(10, 0)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("Result:", result)
}

この例では、canDivide関数は、ゼロによる除算を試みた場合にエラーを返します。このエラーはerrors.New関数を使用して作成され、その後main関数内でチェックされています。


カスタムエラー

独自のエラー型を作成することもでき、これによりより多くの情報をエラーに含めることが可能です。

type MyError struct {
    Msg    string
    Status int
}

func (e *MyError) Error() string {
    return fmt.Sprintf("%d - %s", e.Status, e.Msg)
}

func doSomething() error {
    // エラーが発生したとき
    return &MyError{"Something went wrong", 500}
}

func main() {
    err := doSomething()
    if err != nil {
        log.Fatal(err)
    }
}

この例では、MyError型には追加の情報(MsgStatus)が含まれており、Error()メソッドでこれらの情報を含む文字列を生成しています。


エラーハンドリング

Go言語では、エラーが返されたときにそれをチェックし、適切に処理することが重要です。

func openFile(name string) error {
    _, err := os.Open(name)
    if err != nil {
        return fmt.Errorf("opening %s: %w", name, err)
    }
    return nil
}

func main() {
    err := openFile("nonexistent.txt")
    if err != nil {
        log.Printf("An error occurred: %v\n", err)
    }
}

この例では、ファイルを開く際にエラーが発生した場合、そのエラーをmain関数に伝播し、そこでエラーメッセージをログに出力しています。


エラー処理の応用

「エラー処理の応用」は、プログラムの堅牢性を高めるために重要な側面です。ここでは、エラー処理の応用に関する具体的な事例を通じて、エラーの伝播、パニックとリカバー、エラーラッピングなどについて説明します。


エラーの伝播

関数がエラーを返した場合、そのエラーを呼び出し元に「伝播」させることが一般的です。これにより、エラーの原因を適切にハンドリングできるようになります。

func divide(a, b int) (int, error) {
    if b == 0 {
        return 0, fmt.Errorf("cannot divide by zero")
    }
    return a / b, nil
}

func calculate() error {
    _, err := divide(10, 0)
    if err != nil {
        // エラーを伝播させる
        return fmt.Errorf("calculate error: %w", err)
    }
    return nil
}

func main() {
    err := calculate()
    if err != nil {
        log.Println("main error:", err)
    }
}


パニックとリカバー

致命的なエラーの場合にpanicを使用します。panicは通常のエラーハンドリングでは捕捉できない重大なエラーを示します。recover関数を使ってパニックから回復し、プログラムのクラッシュを防ぐことができます。

func riskyOperation() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered from panic:", r)
        }
    }()

    // 致命的なエラーを引き起こす
    panic("something bad happened")
}

func main() {
    riskyOperation()
    fmt.Println("Program continues after recovery")
}


エラーラッピング

Go 1.13以降では、エラーのラッピングとアンラッピングがサポートされています。これにより、エラーのコンテキストを保持しながら新しいエラーを生成できます。

func processFile(name string) error {
    _, err := os.Open(name)
    if err != nil {
        // エラーのラッピング
        return fmt.Errorf("error processing file %s: %w", name, err)
    }
    return nil
}

func main() {
    err := processFile("nonexistent.txt")
    if err != nil {
        if wrappedErr := errors.Unwrap(err); wrappedErr != nil {
            fmt.Println("Unwrapped error:", wrappedErr)
        }
        fmt.Println("Error:", err)
    }
}


デバッグとエラーロギング

Go言語でのデバッグとエラーロギングは、問題の診断と解決において重要な役割を果たします。デバッグでは、コードの実行を追跡し、問題の原因を特定します。一方、エラーロギングでは、実行時のエラーを記録し、後で分析するための情報を提供します。以下に、これらの概念に関する具体的な事例を示します。


デバッグ

Go言語では、デバッグには主にプリントステートメント(fmt.Printlnなど)や専用のデバッガ(例えばDelve)を使用します。

package main

import (
    "fmt"
    "log"
    "net/http"
)

func fetchURL(url string) {
    resp, err := http.Get(url)
    if err != nil {
        log.Println("Error fetching URL:", err)
        return
    }
    defer resp.Body.Close()

    // デバッグ情報の印刷
    fmt.Println("Fetched URL:", url, "Status Code:", resp.StatusCode)
}

func main() {
    fetchURL("https://www.example.com")
}

この例では、URLを取得する際に発生するエラーをロギングし、URLの取得が成功した場合はそのステータスコードをプリントしています。


エラーロギング

Go言語のlogパッケージは、エラーメッセージの記録に便利な機能を提供します。エラーログにはタイムスタンプやカスタムメッセージを含めることができます。

func processFile(filename string) {
    _, err := os.Open(filename)
    if err != nil {
        // エラーとタイムスタンプをログに記録
        log.Printf("Error opening file %s: %v", filename, err)
    }
}

func main() {
    log.SetPrefix("LOG: ")
    log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)

    processFile("nonexistent.txt")
}

この例では、ファイルを開く際にエラーが発生した場合、そのエラーをロギングしています。log.SetPrefixlog.SetFlagsを使用して、ログメッセージのフォーマットをカスタマイズしています。


Delveの使い方

Delveのインストール

DelveはGo言語のパッケージとしてインストールできます。以下のコマンドでインストールできます。

go get -u github.com/go-delve/delve/cmd/dlv


プログラムのデバッグ開始

Delveを使用してプログラムのデバッグを開始するには、dlv debugコマンドを使用します。これは、現在のディレクトリにあるプログラムをデバッグモードで起動します。

dlv debug


ブレークポイントの設定

デバッグセッション中に、特定の行や関数で実行を停止するには、ブレークポイントを設定します。

(dlv) break main.main


プログラムの実行とステップ実行

ブレークポイントが設定されたら、プログラムを実行します。

(dlv) continue

また、ステップ実行(行ごとに実行)をするにはnextコマンドを使用します。

(dlv) next


変数の検査

プログラムが停止した状態で変数の値を確認するには、printコマンドを使用します。

(dlv) print myVariable


デバッグセッションの終了

デバッグセッションを終了するには、exitコマンドを使用します。

(dlv) exit


その他のコマンド

Delveには他にも多くのコマンドがあります。例えば、現在の実行スタックを表示するbt、特定のゴルーチンのスタックトレースを表示するgoroutinesなどがあります。


練習問題1.

os.Open関数を使用してファイルを開く関数openFileを作成してください。ファイルが存在しない場合、エラーを返し、存在する場合はファイルを閉じてnilを返すようにしてください。main関数でこの関数を呼び出し、エラーが返された場合はその内容を出力してください。

package main

import (
    "fmt"
    "os"
)

// ここに openFile 関数を定義

func main() {
    // ここに openFile 関数を呼び出すコードを記述
}


練習問題2.

Divide関数を作成し、2つの整数を引数として受け取り、第二引数が0の場合はカスタムエラーを返し、それ以外の場合は2つの数の商を返すようにしてください。カスタムエラーはerrors.Newを使用して作成し、エラーメッセージには「division by zero」を含めること。main関数でこの関数を呼び出し、結果またはエラーを出力してください。

package main

import (
    "errors"
    "fmt"
)

// ここに Divide 関数を定義

func main() {
    // ここに Divide 関数を呼び出すコードを記述
}


練習問題3.

os.Open関数を使用してファイルを開く関数readFileを作成してください。この関数は、ファイルを開けない場合にエラーをラッピングして返すようにし、main関数でこのエラーをアンラップして元のエラーメッセージを出力してください。

package main

import (
    "fmt"
    "os"
)

// ここに readFile 関数を定義

func main() {
    // ここに readFile 関数を呼び出し、エラーをアンラップするコードを記述
}