エラー

ライブラリルーチンは、呼び出し元に何らかのエラー表示を返すことがしばしば必要です。前述のように、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もそうで、エラーに関するより多くの情報を調べることができます。

パニック

呼び出し元にエラーを報告する通常の方法は、追加の戻り値としてerrorを返すことです。標準的なReadメソッドはよく知られたインスタンスです。バイトカウントとerrorを返します。しかし、エラーが回復不可能な場合はどうでしょうか?時々、プログラムは単純に続行できません。

この目的のために、プログラムを停止する実行時エラーを効果的に作成する組み込み関数panicがあります(ただし、次のセクションを参照してください)。この関数は、任意の型の単一の引数を取ります(多くの場合は文字列)。これは、プログラムが死ぬときに印刷されます。また、無限ループから抜けるなど、不可能なことが起こったことを示す方法でもあります。

// ニュートン法を使用した立方根の玩具実装
func CubeRoot(x float64) float64 {
    z := x/3   // 任意の初期値
    for i := 0; i < 1e6; i++ {
        prevz := z
        z -= (z*z*z-x) / (3*z*z)
        if veryClose(z, prevz) {
            return z
        }
    }
    // 100万回の反復が収束しませんでした。何かが間違っています。
    panic(fmt.Sprintf("CubeRoot(%g) did not converge", x))
}

これは単なる例ですが、実際のライブラリ関数はpanicを避けるべきです。問題がマスクされたり回避されたりする場合は、プログラム全体を停止するよりも、物事を実行し続ける方が常に良いです。1つの可能な反例は初期化中です:ライブラリが本当に自分自身を設定できない場合、いわば、パニックになることは合理的かもしれません。

var user = os.Getenv("USER")

func init() {
    if user == "" {
        panic("no value for $USER")
    }
}

回復

panicが呼び出されると、スライスの範囲外のインデックス作成や失敗した型アサーションなどの実行時エラーに対して暗黙的に呼び出されることも含めて、現在の関数の実行を即座に停止し、ゴルーチンのスタックの巻き戻しを開始し、その過程で遅延関数を実行します。その巻き戻しがゴルーチンのスタックの上に達すると、プログラムは死にます。ただし、組み込み関数recoverを使用してゴルーチンの制御を取り戻し、通常の実行を再開することが可能です。

recoverへの呼び出しは巻き戻しを停止し、panicに渡された引数を返します。巻き戻し中に実行される唯一のコードは遅延関数内にあるため、recoverは遅延関数内でのみ有用です。

recoverの1つの応用は、他の実行中のゴルーチンを殺すことなく、サーバー内の失敗したゴルーチンをシャットダウンすることです。

func server(workChan <-chan *Work) {
    for work := range workChan {
        go safelyDo(work)
    }
}

func safelyDo(work *Work) {
    defer func() {
        if err := recover(); err != nil {
            log.Println("work failed:", err)
        }
    }()
    do(work)
}

この例では、do(work)がパニックした場合、結果はログに記録され、ゴルーチンは他のゴルーチンを妨げることなくきれいに終了します。遅延クロージャで他に何もする必要はありません。recoverを呼び出すことで条件を完全に処理します。

recoverは遅延関数から直接呼び出されない限り、常にnilを返すため、遅延コードは自分自身でpanicrecoverを使用するライブラリルーチンを失敗することなく呼び出すことができます。例として、safelyDoの遅延関数はrecoverを呼び出す前にログ関数を呼び出すかもしれません。そのログコードはパニック状態の影響を受けずに実行されます。

回復パターンが適切に配置されていれば、do関数(およびそれが呼び出すもの)はpanicを呼び出すことで、どんな悪い状況からもきれいに抜け出すことができます。このアイデアを使用して複雑なソフトウェアでのエラー処理を簡素化できます。regexpパッケージの理想化されたバージョンを見てみましょう。これは、ローカルエラー型でpanicを呼び出すことで解析エラーを報告します。Errorの定義、errorメソッド、およびCompile関数は次のとおりです。

// Errorは解析エラーの型です。errorインターフェースを満たします。
type Error string
func (e Error) Error() string {
    return string(e)
}

// errorは*Regexpのメソッドで、Errorでパニックすることで
// 解析エラーを報告します。
func (regexp *Regexp) error(err string) {
    panic(Error(err))
}

// Compileは正規表現の解析された表現を返します。
func Compile(str string) (regexp *Regexp, err error) {
    regexp = new(Regexp)
    // doParseは解析エラーがある場合にパニックします。
    defer func() {
        if e := recover(); e != nil {
            regexp = nil    // 戻り値をクリアします。
            err = e.(Error) // 解析エラーでない場合は再パニックします。
        }
    }()
    return regexp.doParse(str), nil
}

doParseがパニックした場合、回復ブロックは戻り値をnilに設定します—遅延関数は名前付き戻り値を変更できます。それから、errへの代入で、ローカル型Errorを持つことをアサートして、問題が解析エラーであったかどうかをチェックします。そうでない場合、型アサーションは失敗し、何も中断されなかったかのようにスタック巻き戻しを続ける実行時エラーを引き起こします。このチェックは、インデックスが範囲外であるなど、予期しないことが起こった場合、解析エラーを処理するためにpanicrecoverを使用していても、コードが失敗することを意味します。

エラー処理が適切に配置されていれば、errorメソッド(型にバインドされたメソッドなので、組み込みerror型と同じ名前を持つことは、自然でも問題でもありません)により、手動で解析スタックを巻き戻すことを心配することなく、解析エラーを報告することが簡単になります:

if pos == 0 {
    re.error("'*' illegal at start of expression")
}

このパターンは有用ですが、パッケージ内でのみ使用すべきです。Parseは内部panic呼び出しをerror値に変換します。クライアントにpanicsを公開しません。これは従うべき良いルールです。

ちなみに、この再パニックイディオムは、実際のエラーが発生した場合にパニック値を変更します。ただし、元の失敗と新しい失敗の両方がクラッシュレポートに表示されるため、問題の根本原因は依然として見えます。したがって、この単純な再パニックアプローチは通常十分です—結局のところクラッシュですが—元の値のみを表示したい場合は、予期しない問題をフィルタリングして元のエラーで再パニックするために、もう少しコードを書くことができます。これは読者への練習問題として残されています。