M.Hiroi's Home Page

お気楽 Go 言語プログラミング入門

応用編 : エラー処理


Copyright (C) 2014-2021 Makoto Hiroi
All rights reserved.

はじめに

今回は Go 言語のエラー処理について説明します。他のプログラミング言語、たとえばC++ や Java などには、try, throw, catch といったエラーを処理するための構文が用意されています。これを「例外処理」といいます。「例外=エラー処理」と考えてもらってもかまいません。最近は例外処理を持っているプログラミング言語が多くなりました。

Go 言語の場合、関数は多値を返すことができるので、エラーは簡単に報告することができます。このため、Go 言語には例外処理専用の構文はないのですが、組み込み関数 panic, recover と defer 文を使って例外処理と似たような動作を行わせることができます。

●エラーの返し方

Go 言語にはエラーを表すインターフェース error が定義されています。Go 言語でエラーを報告する場合、error を多値で返すことがよく行われます。

リスト : error の定義

type error interface {
    Error() string
}

メソッド Error はエラーメッセージを文字列として返します。Print や Println は error を受け取るとメソッド Error を呼び出して、返り値の文字列を表示します。

パッケージ errors の関数 New もしくはパッケージ fmt の関数 Errorf を使うと、簡単にエラーを返すことができます。

func New(text string) error

関数 errors.New は引数 text を格納した構造体 (非公開) へのポインタを返します。この構造体にはメソッド Error が定義されているので、error 型として扱うことができます。

func Errorf(format string, args... interface{}) error

関数 fmt.Errorf は fmt.Sprintf と errors.New を組み合わせたもので、書式文字列 format に従って変数 args を整形し、その結果を文字列に変換して errors.New に渡します。

簡単な例として、階乗を求める関数 fact で引数の範囲チェックを行うことにします。次のリストを見てください。

リスト : エラーの返し方 (sample1501.go)

package main

import (
    "fmt"
    "errors"
)

// 階乗
func fact(n int) (int, error) {
    if n < 0 {
        return 0, errors.New("fact : domain error")
    }
    a := 1;
    for ; n > 1; n-- {
        a *= n
    }
    return a, nil
}

func main() {
    for x := 10; x >= -1; x-- {
        v, err := fact(x)
        if err != nil {
            fmt.Println(err)
        } else {
            fmt.Println(v)
        }
    }
}

関数 fact は int と error を多値で返します。引数 n が負の場合、int のゼロ値 0 と error を返します。error は errors.New で生成します。n が正常の範囲であれば、階乗を計算して、その結果 a と nil を返します。main では fact の返り値 err をチェックして、err が nil でなければ fmt.Println(err) でエラーメッセージを表示します。

それでは実行してみましょう。

$ go run sample1501.go
3628800
362880
40320
5040
720
120
24
6
2
1
1
fact : domain error

このように、Go 言語は多値を使ってエラーを返すことができます。

なお、自分でエラーを定義することも簡単にできます。次のリストを見てください。

リスト : エラーの返し方 (2)

package main

import "fmt"

// エラーの定義
type MyError struct {
    msg string
}

// エラーの生成
func newMyError(s string) *MyError {
    err := new(MyError) //
    err.msg = s         //
    return err          // return &MyError{s} でもよい
}

// メソッドの定義
func (err *MyError) Error() string {
    return err.msg
}

// 階乗
func fact(n int) (int, error) {
    if n < 0 {
        return 0, newMyError("fact : domain error")
    }
    a := 1;
    for ; n > 1; n-- {
        a *= n
    }
    return a, nil
}

func main() {
    for x := 10; x >= -1; x-- {
        v, err := fact(x)
        if err != nil {
            fmt.Println(err)
        } else {
            fmt.Println(v)
        }
    }
}

エラーメッセージを格納する構造体 MyError を定義します。関数 newMyError は new で MyError のメモリを確保し、引数の文字列 s をフィールド変数 msg にセットします。なお、new を使わずに &MyError{s} を return で返しても、メモリを動的に確保することができます。

メソッド Error は MyError のフィールド変数 msg の値を返すだけです。これで MyError は error と同じ型として扱うことができます。関数 fact では、errors.New のかわりに newMyError を呼び出してエラーを生成します。

●panic

組み込み関数 panic はプログラムの実行を中断するランタイムエラーを生成します。

panic(err interface{})

panic に渡された引数 err はプログラムをエラー終了するとき画面に出力されます。

簡単な例を示しましょう。次のリストを見てください。

リスト : panic の使用例 (sample1502.go)

package main

import "fmt"

// 階乗
func fact(n int) (int, error) {
    if n < 0 {
        panic("fact : domain error")
    }
    a := 1;
    for ; n > 1; n-- {
        a *= n
    }
    return a, nil
}

func main() {
    for x := 10; x >= -1; x-- {
        v, err := fact(x)
        if err != nil {
            fmt.Println(err)
        } else {
            fmt.Println(v)
        }
    }
}

関数 fact は引数 n が負の場合、panic でランタイムエラーを生成します。panic を実行すると、画面にエラー情報を表示して、プログラムの実行を終了します。

それでは実行してみましょう。

$ go run sample1502.go
3628800
362880
40320
5040
720
120
24
6
2
1
1
panic: fact : domain error

・・・エラー情報 (省略) ・・・

exit status 2

●defer 文

defer 文は関数の終了時に実行する処理を指定します。

defer 処理

defer 文で指定する処理は関数またはメソッド呼び出しでなければなりません。

簡単な例を示しましょう。

リスト : defer 文の使用例 (sample1503.go)

package main

import "fmt"

func bar() {
    defer fmt.Println("bar end")
    fmt.Println("bar start!")
}

func foo() {
    defer fmt.Println("foo end")
    fmt.Println("foo start!")
    bar()
}

func main(){
    foo()
}
$ go run sample1503.go
foo start!
bar start!
bar end
foo end

関数 foo と bar は defer でメッセージを表示する処理を指定します。foo から bar を呼び出して、bar の実行が終了すると defer で指定した処理が実行されて bar end が表示されます。そのあと foo に戻ってきて foo の実行が終了すると、defer で指定した処理が実行されて foo end が表示されます。

defer 文の指定は、ランタイムエラーで関数の実行が中断された場合にも有効です。次のリストを見てください。

リスト : defer 文の使用例 (sample1504.go)

package main

import "fmt"

func baz(){
    panic("oops!")
}

func bar() {
    defer fmt.Println("bar end")
    fmt.Println("bar start!")
    baz()
}

func foo() {
    defer fmt.Println("foo end")
    fmt.Println("foo start!")
    bar()
}

func main(){
    foo()
}
$ go run sample1504.go
foo start!
bar start!
bar end
foo end
panic: oops!

・・・省略・・・

exit status 2

関数 bar から関数 baz を呼び出し、baz の中で panic を実行します。ランタイムエラーが発生すると、Go 言語は関数の呼び出し履歴 (スタック) をたどり、呼び出した関数に defer 文の記述があれば、その処理を実行します。

baz で panic を実行したとき、関数は main -> foo -> bar -> baz の順番で呼び出されているので、baz, bar, foo, main の順番で defer 文の記述があるかチェックします。bar と foo には defer 文があるので、bar の defer 文の処理を実行して bar end が表示され、そのあと foo end が表示されます。最後に、ランタイムエラーのメッセージが表示されてプログラムの実行が終了します。

●ランタイムエラーの捕捉

ところで、ランタイムエラーが発生するたびにプログラムをエラー終了するのではなく、致命的なエラーでなければプログラムの実行を継続したい場合もあるでしょう。Go 言語では組み込み関数 recover と defer 文を使ってランタイムエラーを捕まえることができます。

func recover() interface{}

recover は defer 文で指定した処理の中で使います。recover を実行すると、関数の呼び出し履歴をたどるのをやめて、panic に渡された引数を返します。エラーが発生していない場合、recover は nil を返します。defer 文の処理が終了すると、その関数の呼び出し元に戻ってプログラムの実行を継続します。

簡単な例を示しましょう。次のリストを見てください。

リスト : recover の使用例 (sample1505.go)

package main

import "fmt"

func baz(){
    panic("oops!")
}

func bar() {
    defer fmt.Println("bar end")
    fmt.Println("bar start!")
    baz()
}

func foo() {
    defer func(){
        fmt.Println("foo end")
        err := recover()
        if err != nil {
            fmt.Println(err)
        }
    }()
    fmt.Println("foo start!")
    bar()
}

func main(){
    fmt.Println("main start!")
    foo()
    fmt.Println("main end")
}
$ go run sample1505.go
main start!
foo start!
bar start!
bar end
foo end
oops!
main end

関数 foo の defer 文の処理は匿名関数で、その中で recover を呼び出しています。関数 baz で panic を実行すると、foo の recover で捕捉されて、変数 err には "oops!" がセットされます。foo end と oops! を表示したあと、foo を呼び出した main に戻ってプログラムの実行を継続するので、main end が表示されます。recover でエラーを捕捉しないとプログラムはエラー終了するので、main end は表示されません。

●返り値がある関数でエラーを捕捉する

返り値がある関数でエラーを捕捉する場合は注意が必要です。関数 foo を次のように修正します。

リスト : 返り値がある関数でエラーを捕捉 (sample1506.go)

func foo(n int) int {
    defer func(){
        fmt.Println("foo end")
        err := recover()
        if err != nil {
            fmt.Println(err)
        }
    }()
    fmt.Println("foo start!")
    bar()
    return n * n
}

func main(){
    fmt.Println("main start!")
    fmt.Println(foo(10))
    fmt.Println("main end")
}
$ go run sample1506.go
main start!
foo start!
bar start!
bar end
foo end
oops!
0
main end

関数 foo は整数値 n * n を返します。ところが、bar の実行中にランタイムエラーが発生すると、return n * n は実行されません。この場合、foo の返り値はゼロ値 (0) になります。エラーを捕捉したあと、ゼロ値以外の値を返したい場合は、返り値の型で変数名を指定して、その変数に値をセットします。次の例を見てください。

リスト : 返り値がある関数でエラーを捕捉 (sample1507.go)

func foo(n int) (m int) {
    defer func(){
        fmt.Println("foo end")
        err := recover()
        if err != nil {
            fmt.Println(err)
            m = -1
        }
    }()
    fmt.Println("foo start!")
    bar()
    m = n * n
    return
}
$ go run sample1507.go
main start!
foo start!
bar start!
bar end
foo end
oops!
-1
main end

foo は変数 m の値を返します。bar の実行でエラーがない場合は n * n を返します。エラーが発生した場合は defer 文の匿名関数の中で m に -1 をセットします。これで -1 を返すことができます。

●特定のエラーを捕捉する

特定のエラーだけを捕捉してプログラムの実行を継続することもできます。次のリストを見てください。

リスト : 特定のエラーを捕捉する (sample1508.go)

package main

import "fmt"

// エラーの定義
type MyError struct {
    msg string
}

// エラーの生成
func newMyError(s string) *MyError {
    err := new(MyError)
    err.msg = s
    return err
}

// メソッドの定義
func (err *MyError) Error() string {
    return err.msg
}

func baz1(){
    panic(newMyError("oops!"))
}

func baz2(){
    panic("oops!")
}

func bar(f func()) {
    defer fmt.Println("bar end")
    fmt.Println("bar start!")
    f()
}

func foo(f func()) {
    defer func(){
        fmt.Println("foo end")
        err := recover()
        if err != nil {
            v := err.(*MyError)
            fmt.Println(v)
        }
    }()
    fmt.Println("foo start!")
    bar(f)
}

func main(){
    fmt.Println("main start!")
    foo(baz1)
    foo(baz2)
    fmt.Println("main end")
}

関数 foo の defer 文の匿名関数でエラーを捕捉します。このとき、型アサーションを使って err が *MyError かチェックします。そうであれば、エラーメッセージを表示してプログラムの実行を継続します。err が *MyError でなければ、型アサーションに失敗してエラーが発生するので、プログラムはエラー終了します。

それでは実行してみましょう。

C>go run sample1508.go
main start!
foo start!
bar start!
bar end
foo end
oops!
foo start!
bar start!
bar end
foo end
panic: oops! [recovered]
        panic: interface conversion: interface{} is string, not *main.MyError

・・・省略・・・

exit status 2

foo(baz1) の呼び出しは MyError を返すので、oops! を表示したあとプログラムの実行は継続されます。このあと、foo(baz2) が呼び出されますが、baz2 では panic に文字列 "oops!" を渡して呼び出しているので、foo の defer 文で型アサーションに失敗します。エラー情報を表示したあと、プログラムはエラー終了するので main end は表示されません。


初版 2014 年 4 月 20 日
改訂 2021 年 12 月 18 日