> Source: https://www.ymotongpoo.com/works/goblog-ja/error-handling-and-go/


# エラー処理とGo

[Error handling and Go](https://go.dev/blog/error-handling-and-go) by Andrew Gerrand

## はじめに

Goのコードを書いたことがあれば、組み込みの `error` 型に触れたことがあるはずです。
Goのコードでは、異常な状態を示すために `error` 値を使います。たとえば、`os.Open` 関数は
ファイルのオープンに失敗すると、非nilの `error` 値を返します。

```go
func Open(name string) (file *File, err error)
```

次のコードは `os.Open` を使ってファイルを開いています。エラーが発生した場合は
`log.Fatal` を呼び出してエラーメッセージを表示し、処理を止めます。

```go
f, err := os.Open("filename.ext")
if err != nil {
    log.Fatal(err)
}
// 開いた *File である f に対して何らかの処理を行う
```

`error` 型について知っているだけでもGoで多くのことができますが、この記事では `error` について
もう少し詳しく見て、Goにおけるエラー処理の良い習慣についていくつか議論します。

## `error` 型

`error` 型はインターフェース型です。`error` 型の変数は、自分自身を文字列として表現できる
任意の値を表します。インターフェースの宣言は次のとおりです。

```go
type error interface {
    Error() string
}
```

`error` 型は、他の組み込み型と同様に、[ユニバースブロック](/doc/go_spec.html#Blocks)内で
[事前宣言](/doc/go_spec.html#Predeclared_identifiers)されています。

最もよく使われる `error` の実装は、[errors](/pkg/errors/) パッケージの非公開の `errorString` 型です。

```go
// errorStringはerrorの単純な実装です。
type errorString struct {
    s string
}

func (e *errorString) Error() string {
    return e.s
}
```

これらの値は `errors.New` 関数を使って生成できます。この関数は文字列を受け取り、それを
`errors.errorString` に変換して `error` 値として返します。

```go
// Newは指定されたテキストとしてフォーマットされるerrorを返します。
func New(text string) error {
    return &errorString{text}
}
```

`errors.New` は次のように使えます。

```go
func Sqrt(f float64) (float64, error) {
    if f < 0 {
        return 0, errors.New("math: square root of negative number")
    }
    // 実装
}
```

`Sqrt` に負の引数を渡した呼び出し元は、非nilの `error` 値を受け取ります
（その具体的な実体は `errors.errorString` の値です）。呼び出し元は `error` の `Error` メソッドを
呼び出す、あるいは単に出力することで、エラー文字列（"math: square root of..."）にアクセスできます。

```go
f, err := Sqrt(-1)
if err != nil {
    fmt.Println(err)
}
```

[fmt](/pkg/fmt/) パッケージは、`error` 値の `Error() string` メソッドを呼び出すことでフォーマットします。

文脈を要約するのは、エラーの実装側の責務です。`os.Open` が返すエラーは、単に
"permission denied" ではなく "open /etc/passwd: permission denied" というフォーマットになります。
一方、先ほどの `Sqrt` が返すエラーには、不正な引数についての情報が欠けています。

その情報を追加するために便利な関数が `fmt` パッケージの `Errorf` です。この関数は `Printf` の
規則に従って文字列をフォーマットし、`errors.New` で生成した `error` として返します。

```go
if f < 0 {
    return 0, fmt.Errorf("math: square root of negative number %g", f)
}
```

多くの場合 `fmt.Errorf` で十分ですが、`error` はインターフェースなので、任意のデータ構造を
`error` 値として使い、呼び出し元がエラーの詳細を調べられるようにすることもできます。

たとえば、仮に呼び出し元が `Sqrt` に渡された不正な引数を取り出したいとしましょう。
`errors.errorString` を使う代わりに新しい `error` の実装を定義することで、これを実現できます。

```go
type NegativeSqrtError float64

func (f NegativeSqrtError) Error() string {
    return fmt.Sprintf("math: square root of negative number %g", float64(f))
}
```

高度な呼び出し元は、[型アサーション](/doc/go_spec.html#Type_assertions)を使って
`NegativeSqrtError` かどうかを確認し、特別な処理を行えます。一方で、エラーを単に
`fmt.Println` や `log.Fatal` に渡すだけの呼び出し元にとっては、振る舞いに変化はありません。

もう一つの例として、[json](/pkg/encoding/json/) パッケージは、JSONのバイト列を解析中に
構文エラーに遭遇したときに `json.Decode` 関数が返す `SyntaxError` 型を定義しています。

```go
type SyntaxError struct {
    msg    string // エラーの説明
    Offset int64  // Offsetバイト読み込んだ後にエラーが発生
}

func (e *SyntaxError) Error() string { return e.msg }
```

`Offset` フィールドはエラーのデフォルトのフォーマットには表示すらされませんが、
呼び出し元はこれを使ってエラーメッセージにファイル名や行番号の情報を追加できます。

```go
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](http://camlistore.org) プロジェクトの
[実際のコード](https://github.com/camlistore/go4/blob/03efcb870d84809319ea509714dd6d19a1498483/jsonconfig/eval.go#L123-L135)
を少し簡略化したものです。）

`error` インターフェースが要求するのは `Error` メソッドだけですが、個々の `error` の実装は
追加のメソッドを持つことがあります。たとえば、[net](/pkg/net/) パッケージは通常の慣習に従って
`error` 型のエラーを返しますが、いくつかの `error` の実装は `net.Error` インターフェースで
定義された追加のメソッドを持っています。

```go
package net

type Error interface {
    error
    Timeout() bool   // タイムアウトによるエラーかどうか
    Temporary() bool // 一時的なエラーかどうか
}
```

クライアントのコードは、型アサーションで `net.Error` かどうかを確認し、一時的な
ネットワークエラーと恒久的なエラーを区別できます。たとえば、Webクローラーは一時的な
エラーに遭遇した場合はスリープしてリトライし、そうでなければ諦める、という実装ができます。

```go
if nerr, ok := err.(net.Error); ok && nerr.Temporary() {
    time.Sleep(1e9)
    continue
}
if err != nil {
    log.Fatal(err)
}
```

## 繰り返し現れるエラー処理を簡潔にする

Goにおいて、エラー処理は重要です。この言語の設計と慣習は、エラーが発生した箇所で
明示的にエラーを確認することを推奨しています（例外を投げて、ときにそれを捕捉するという
他の言語の慣習とは対照的です）。これによってGoのコードが冗長になる場合もありますが、
幸いにも繰り返し現れるエラー処理を最小限に抑えるためのテクニックがいくつかあります。

[App Engine](https://cloud.google.com/appengine/docs/go/) のアプリケーションで、
データストアからレコードを取得し、テンプレートでフォーマットするHTTPハンドラを考えてみましょう。

```go
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（「Internal Server Error」）とともに
単純なエラーメッセージをユーザーに提示します。これくらいなら扱いやすいコード量に見えますが、
HTTPハンドラをいくつか増やせば、たちまち同じようなエラー処理のコードのコピーだらけになってしまいます。

この繰り返しを減らすために、`error` の戻り値を含む独自のHTTP `appHandler` 型を定義できます。

```go
type appHandler func(http.ResponseWriter, *http.Request) error
```

次に `viewRecord` 関数を `error` を返すように変更できます。

```go
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](/pkg/net/http/) パッケージは `error` を
返す関数を理解できません。これを解決するには、`appHandler` に `http.Handler` インターフェースの
`ServeHTTP` メソッドを実装します。

```go
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)` という式でレシーバを呼び出すことで、
その関数を実行しています。

`appHandler` は（`http.HandlerFunc` ではなく）`http.Handler` なので、`viewRecord` を
httpパッケージに登録するときは（`HandleFunc` の代わりに）`Handle` 関数を使います。

```go
func init() {
    http.Handle("/view", appHandler(viewRecord))
}
```

この基本的なエラー処理の仕組みができたので、これをさらにユーザーフレンドリーにできます。
単にエラー文字列を表示するのではなく、適切なHTTPステータスコードとともに簡単なエラーメッセージを
ユーザーに提示しつつ、デバッグのために完全なエラーをApp Engineの開発者コンソールにログ出力する
ほうが良いでしょう。

そのために、`error` といくつかの他のフィールドを含む `appError` 構造体を作ります。

```go
type appError struct {
    Error   error
    Message string
    Code    int
}
```

次に `appHandler` 型を `*appError` 値を返すように変更します。

```go
type appHandler func(http.ResponseWriter, *http.Request) *appError
```

（[Go FAQ](/doc/go_faq.html#nil_error) で議論されている理由から、通常は `error` ではなく
具体的な型のエラーを返すのは間違いです。しかしここでは、その値を見て中身を使うのは
`ServeHTTP` だけなので、これが正しいやり方です。）

そして `appHandler` の `ServeHTTP` メソッドが、正しいHTTPステータス `Code` とともに
`appError` の `Message` をユーザーに表示し、完全な `Error` を開発者コンソールにログ出力する
ようにします。

```go
func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    if e := fn(w, r); e != nil { // eは*appErrorであり、os.Errorではない。
        c := appengine.NewContext(r)
        c.Errorf("%v", e.Error)
        http.Error(w, e.Message, e.Code)
    }
}
```

最後に、`viewRecord` を新しい関数シグネチャに更新し、エラーに遭遇したときに
より多くの文脈情報を返すようにします。

```go
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` の内部でpanicから回復（recover）し、コンソールには「Critical」としてエラーを
  ログ出力しつつ、ユーザーには「重大なエラーが発生しました」と伝える。これはプログラミングの
  ミスによる理解不能なエラーメッセージをユーザーにさらさないための、ちょっとした工夫です。
  詳しくは [Defer, Panic, and Recover](/doc/articles/defer_panic_recover.html) の記事を
  参照してください。

## まとめ

適切なエラー処理は、優れたソフトウェアに欠かせない要件です。この記事で説明したテクニックを
活用すれば、より信頼性が高く簡潔なGoのコードを書けるようになるはずです。

By Andrew Gerrand

