The Go Blog
エラー処理と Go
はじめに
Go のコードを書いたことがあるなら、おそらく組み込みの error
型に出会ったことがあるでしょう。Go のコードは、異常な状態を示すために error
値を使用します。たとえば、os.Open
関数は、ファイルを開くことができない場合、nil 以外の error
値を返します。
func Open(name string) (file *File, err error)
次のコードは、os.Open
を使用してファイルを開きます。エラーが発生した場合、log.Fatal
を呼び出してエラーメッセージを出力し、停止します。
f, err := os.Open("filename.ext")
if err != nil {
log.Fatal(err)
}
// do something with the open *File f
error
型についてこれだけの知識があれば、Go で多くのことを達成できますが、この記事では error
を詳しく見て、Go でのエラー処理の良い習慣について説明します。
error 型
error
型はインターフェース型です。error
変数は、自身を文字列として記述できる任意の値を表します。インターフェースの宣言は次のとおりです。
type error interface {
Error() string
}
error
型は、すべての組み込み型と同様に、事前宣言されています(ユニバースブロック内)。
最も一般的に使用される error
実装は、errors パッケージのエクスポートされていない errorString
型です。
// errorString is a trivial implementation of error.
type errorString struct {
s string
}
func (e *errorString) Error() string {
return e.s
}
これらの値の1つは、errors.New
関数を使用して構築できます。これは、文字列を受け取り、それを errors.errorString
に変換し、error
値として返します。
// New returns an error that formats as the given text.
func New(text string) error {
return &errorString{text}
}
errors.New
の使用方法の例を示します。
func Sqrt(f float64) (float64, error) {
if f < 0 {
return 0, errors.New("math: square root of negative number")
}
// implementation
}
Sqrt
に負の引数を渡す呼び出し元は、nil 以外の error
値(具体的な表現は errors.errorString
値)を受け取ります。呼び出し元は、error
の Error
メソッドを呼び出すか、単に出力することによって、エラー文字列(「math: square root of…」)にアクセスできます。
f, err := Sqrt(-1)
if err != nil {
fmt.Println(err)
}
fmt パッケージは、Error() string
メソッドを呼び出すことによって、error
値をフォーマットします。
コンテキストを要約するのはエラー実装の責任です。os.Open
によって返されるエラーは、「permission denied」だけでなく、「open /etc/passwd: permission denied」としてフォーマットされます。私たちの Sqrt
によって返されるエラーには、無効な引数に関する情報がありません。
その情報を追加するために便利な関数は、fmt
パッケージの Errorf
です。これは、Printf
のルールに従って文字列をフォーマットし、errors.New
によって作成された error
として返します。
if f < 0 {
return 0, fmt.Errorf("math: square root of negative number %g", f)
}
多くの場合、fmt.Errorf
で十分ですが、error
はインターフェースであるため、任意のデータ構造をエラー値として使用して、呼び出し元がエラーの詳細を検査できるようにすることができます。
たとえば、私たちの仮定の呼び出し元は、Sqrt
に渡された無効な引数を回復したい場合があります。errors.errorString
を使用する代わりに、新しいエラー実装を定義することで、これを有効にすることができます。
type NegativeSqrtError float64
func (f NegativeSqrtError) Error() string {
return fmt.Sprintf("math: square root of negative number %g", float64(f))
}
洗練された呼び出し元は、型アサーションを使用して NegativeSqrtError
をチェックし、それを特別に処理できます。一方、fmt.Println
または log.Fatal
にエラーを渡すだけの呼び出し元は、動作に変化が見られません。
別の例として、json パッケージは、json.Decode
関数が JSON blob の構文解析で構文エラーを検出したときに返す SyntaxError
型を指定します。
type SyntaxError struct {
msg string // description of error
Offset int64 // error occurred after reading Offset bytes
}
func (e *SyntaxError) Error() string { return e.msg }
Offset
フィールドはエラーのデフォルトのフォーマットには表示されませんが、呼び出し元はそれを使用してファイルと行情報をエラーメッセージに追加できます。
if err := dec.Decode(&val); err != nil {
if serr, ok := err.(*json.SyntaxError); ok {
line, col := findLine(f, serr.Offset)
return fmt.Errorf("%s:%d:%d: %v", f.Name(), line, col, err)
}
return err
}
(これは、Camlistore プロジェクトの実際のコードを少し簡略化したバージョンです。)
error
インターフェースでは、Error
メソッドのみが必要です。特定のエラー実装には、追加のメソッドがある場合があります。たとえば、net パッケージは、通常の規則に従って error
型のエラーを返しますが、一部のエラー実装には、net.Error
インターフェースで定義された追加のメソッドがあります。
package net
type Error interface {
error
Timeout() bool // Is the error a timeout?
Temporary() bool // Is the error temporary?
}
クライアントコードは、型アサーションを使用して net.Error
をテストし、一時的なネットワークエラーと永続的なネットワークエラーを区別できます。たとえば、Webクローラーは、一時的なエラーが発生した場合にスリープして再試行し、そうでない場合はあきらめることがあります。
if nerr, ok := err.(net.Error); ok && nerr.Temporary() {
time.Sleep(1e9)
continue
}
if err != nil {
log.Fatal(err)
}
反復的なエラー処理の簡略化
Go では、エラー処理は重要です。言語の設計と規則により、エラーが発生した場所でエラーを明示的にチェックすることが推奨されます(他の言語の例外をスローし、場合によってはキャッチするという規則とは異なります)。場合によっては、これにより Go コードが冗長になりますが、幸いなことに、反復的なエラー処理を最小限に抑えるために使用できる手法がいくつかあります。
データストアからレコードを取得し、テンプレートでフォーマットする HTTP ハンドラーを持つ App Engine アプリケーションを考えてみましょう。
func init() {
http.HandleFunc("/view", viewRecord)
}
func viewRecord(w http.ResponseWriter, r *http.Request) {
c := appengine.NewContext(r)
key := datastore.NewKey(c, "Record", r.FormValue("id"), 0, nil)
record := new(Record)
if err := datastore.Get(c, key, record); err != nil {
http.Error(w, err.Error(), 500)
return
}
if err := viewTemplate.Execute(w, record); err != nil {
http.Error(w, err.Error(), 500)
}
}
この関数は、datastore.Get
関数と viewTemplate
の Execute
メソッドによって返されるエラーを処理します。どちらの場合も、HTTP ステータスコード 500(「内部サーバーエラー」)を使用して、ユーザーに簡単なエラーメッセージを表示します。これは管理しやすい量のコードのように見えますが、HTTP ハンドラーをさらに追加すると、すぐに同じエラー処理コードのコピーが多数作成されます。
繰り返しを減らすために、error
戻り値を含む独自の HTTP appHandler
型を定義できます。
type appHandler func(http.ResponseWriter, *http.Request) error
次に、viewRecord
関数を変更してエラーを返すことができます。
func viewRecord(w http.ResponseWriter, r *http.Request) error {
c := appengine.NewContext(r)
key := datastore.NewKey(c, "Record", r.FormValue("id"), 0, nil)
record := new(Record)
if err := datastore.Get(c, key, record); err != nil {
return err
}
return viewTemplate.Execute(w, record)
}
これは元のバージョンよりも単純ですが、http パッケージは error
を返す関数を理解していません。これを修正するには、appHandler
で http.Handler
インターフェースの ServeHTTP
メソッドを実装できます。
func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if err := fn(w, r); err != nil {
http.Error(w, err.Error(), 500)
}
}
ServeHTTP
メソッドは appHandler
関数を呼び出し、返されたエラー(ある場合)をユーザーに表示します。メソッドのレシーバーである fn
は関数であることに注意してください。(Go はそれを実行できます!)メソッドは、式 fn(w, r)
でレシーバーを呼び出すことによって関数を呼び出します。
これで、viewRecord
を http パッケージに登録するときに、appHandler
は http.Handler
(http.HandlerFunc
ではない)であるため、Handle
関数(HandleFunc
ではなく)を使用します。
func init() {
http.Handle("/view", appHandler(viewRecord))
}
この基本的なエラー処理インフラストラクチャが整ったので、よりユーザーフレンドリーにすることができます。エラー文字列を表示するだけでなく、適切な HTTP ステータスコードでユーザーに簡単なエラーメッセージを表示し、デバッグのために App Engine デベロッパーコンソールに完全なエラーを記録する方が適切です。
これを行うために、error
と他のいくつかのフィールドを含む appError
構造体を作成します。
type appError struct {
Error error
Message string
Code int
}
次に、appHandler
型を変更して *appError
値を返すようにします。
type appHandler func(http.ResponseWriter, *http.Request) *appError
(Go FAQ で説明されている理由により、error
ではなくエラーの具体的な型を渡すのは通常は間違いですが、ServeHTTP
が値を見てその内容を使用する唯一の場所であるため、ここでは正しいことです。)
そして、appHandler
の ServeHTTP
メソッドが、正しい HTTP ステータス Code
でユーザーに appError
の Message
を表示し、デベロッパーコンソールに完全な Error
を記録するようにします。
func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if e := fn(w, r); e != nil { // e is *appError, not os.Error.
c := appengine.NewContext(r)
c.Errorf("%v", e.Error)
http.Error(w, e.Message, e.Code)
}
}
最後に、viewRecord
を新しい関数シグネチャに更新し、エラーが発生したときにさらにコンテキストを返すようにします。
func viewRecord(w http.ResponseWriter, r *http.Request) *appError {
c := appengine.NewContext(r)
key := datastore.NewKey(c, "Record", r.FormValue("id"), 0, nil)
record := new(Record)
if err := datastore.Get(c, key, record); err != nil {
return &appError{err, "Record not found", 404}
}
if err := viewTemplate.Execute(w, record); err != nil {
return &appError{err, "Can't display record", 500}
}
return nil
}
このバージョンの viewRecord
は元のバージョンと同じ長さですが、これらの行はそれぞれ特定の意味を持ち、よりフレンドリーなユーザーエクスペリエンスを提供しています。
それで終わりではありません。アプリケーションのエラー処理をさらに改善できます。いくつかのアイデアがあります。
-
エラーハンドラーにきれいな HTML テンプレートを提供する、
-
ユーザーが管理者の場合にスタックトレースを HTTP レスポンスに書き込むことによって、デバッグを容易にする、
-
デバッグを容易にするためにスタックトレースを格納する
appError
のコンストラクター関数を記述する、 -
appHandler
内のパニックから回復し、エラーをコンソールに「クリティカル」として記録し、ユーザーに「重大なエラーが発生しました」と伝える。これは、プログラミングエラーによって引き起こされる不可解なエラーメッセージがユーザーに公開されるのを防ぐための優れた方法です。詳細については、Defer、Panic、および Recover の記事を参照してください。
結論
適切なエラー処理は、優れたソフトウェアの不可欠な要件です。この記事で説明されている手法を採用することで、より信頼性が高く簡潔な Go コードを記述できるはずです。