エラー
ライブラリルーチンは、呼び出し元に何らかのエラー表示を返すことがしばしば必要です。前述のように、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()
}
PathError
のError
は次のような文字列を生成します:
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
ステートメントは別の型アサーションです。失敗した場合、ok
はfalse
になり、e
はnil
になります。成功した場合、ok
はtrue
になり、エラーは型*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
を返すため、遅延コードは自分自身でpanic
とrecover
を使用するライブラリルーチンを失敗することなく呼び出すことができます。例として、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
を持つことをアサートして、問題が解析エラーであったかどうかをチェックします。そうでない場合、型アサーションは失敗し、何も中断されなかったかのようにスタック巻き戻しを続ける実行時エラーを引き起こします。このチェックは、インデックスが範囲外であるなど、予期しないことが起こった場合、解析エラーを処理するためにpanic
とrecover
を使用していても、コードが失敗することを意味します。
エラー処理が適切に配置されていれば、error
メソッド(型にバインドされたメソッドなので、組み込みerror
型と同じ名前を持つことは、自然でも問題でもありません)により、手動で解析スタックを巻き戻すことを心配することなく、解析エラーを報告することが簡単になります:
if pos == 0 {
re.error("'*' illegal at start of expression")
}
このパターンは有用ですが、パッケージ内でのみ使用すべきです。Parse
は内部panic
呼び出しをerror
値に変換します。クライアントにpanics
を公開しません。これは従うべき良いルールです。
ちなみに、この再パニックイディオムは、実際のエラーが発生した場合にパニック値を変更します。ただし、元の失敗と新しい失敗の両方がクラッシュレポートに表示されるため、問題の根本原因は依然として見えます。したがって、この単純な再パニックアプローチは通常十分です—結局のところクラッシュですが—元の値のみを表示したい場合は、予期しない問題をフィルタリングして元のエラーで再パニックするために、もう少しコードを書くことができます。これは読者への練習問題として残されています。