想定している読者
Goのエラーハンドリングについて体系だった記事が見つからなかったので、色々調べたことを整理して備忘録も兼ねて記事にしました。
以下のような方を読者対象に記事を書いています。
- Goのエラーハンドリングの基本的なやり方を知りたい
- スタックトレースを標準エラー出力したい
- エラーの種類に応じてステータスコードを変えたい
まずデファクトになっているエラーパッケージpkg/errorsの使い方を確認して、実際のWebアプリケーションで追加で実装しないといけないことを説明していきます。
そもそもerrorとは?
goのエラーは以下のようなエラーメッセージを返す関数を実装していれば満たせるインターフェースです。
// cf. https://golang.org/pkg/builtin/#error
type error interface {
Error() string
}
実際には返されたエラーがnilかどうかで条件分岐して、nilでない場合はerror.Error()
でエラー内容を出力するような使われ方をします。
例として、引数で与えられたファイルを開いて、それをJSONとして扱って、map[string]interface{}
型の値(jsonMap
)に変換してみます。
import (
"encoding/json"
"fmt"
"io/ioutil"
)
func main() {
// 中身が空のファイルをJSONとして扱うとjson.Unmarshalでエラーになる
if _, err := unmarshalToMap("src.json"); err != nil {
// err.Error()の結果が出力される
fmt.Println(err)
// unexpected end of JSON input
}
}
func unmarshalToMap(src string) (map[string]interface{}, error) {
jsonMap := map[string]interface{}{}
// ファイルパスからファイルを読み込む
data, err := ioutil.ReadFile(src)
if err != nil {
return jsonMap, err
}
// ファイルの内容をJSONとみなして、key, valueの対応をmap[string]interface{}型の値にする
if err := json.Unmarshal(data, &jsonMap); err != nil {
return nil, err
}
return jsonMap, nil
}
しかし、引数で渡されたファイルsrc.json
は何も書かれていないので(JSONの書式に沿っていないので)エラーが返ってきます。
$ go run main.go
unexpected end of JSON input
このように、Error() string
さえ実装してエラーメッセージを返せればerror
型を満たすことができます。
一見、上記のような方法でも問題がなさそうですが、実際のアプリケーションは実装が多くなり、複雑になりがちにも関わらず、unexpected end of JSON input
というエラー内容だけだと、どのファイルの内容をjson.Umarshal
した時に起こったのか分からないとデバッグが困難になります。そこで重要なのが、返ってきたエラーに「何を」、「どこで」、「どんな処理で」起こったのかコンテキスト情報を付与することです。
コンテキスト情報をエラー内容に含める
返ってきたエラーにコンテキスト情報を付与する一番簡単な方法はfmt.Error
を利用することです。
これは、指定したフォーマットにしたがって、第二引数以降をフォーマットし、新しいエラーメッセージを持つエラーを作って返すことができます。
先ほどの実装でfmt.Errorf
を使ってみます。
func unmarshalToMap(src string) (map[string]interface{}, error) {
jsonMap := map[string]interface{}{}
// ファイルパスからファイルを読み込む
data, err := ioutil.ReadFile(src)
if err != nil {
return jsonMap, err
}
if err := json.Unmarshal(data, &jsonMap); err != nil {
return nil, fmt.Errorf("read %s, %s", src, err) // ここをfmt.Errorfに置き換えた
}
return jsonMap, nil
}
これによって以下のようにどのファイルを読み込んでエラーがでたのかメッセージに含めることができます。
$ go run main.go
read src.json, unexpected end of JSON input
このようにfmt.Errorf
を使うことで「どこで」、「どんな処理で」エラーが起こったのか知ることができます。
fmt.Errorfの問題点
しかし、これで問題なしかというとそうではありません。理由はfmt.Errorf
は元のerrorインターフェースを実装するある型と値を消失させるからです。
fmt.Errorf
の実装を見ると、内部でerrors.New
を呼び出していることがわかります。
// Errorf formats according to a format specifier and returns the string
// as a value that satisfies error.
func Errorf(format string, a ...interface{}) error {
return errors.New(Sprintf(format, a...))
}
errors.New
とは、error
インターフェースを実装したエラーメッセージだけを持つ構造体を返します。
// cf. https://golang.org/pkg/errors/#New
// New returns an error that formats as the given text.
func New(text string) error {
return &errorString{text}
}
// errorString is a trivial implementation of error.
type errorString struct {
s string
}
func (e *errorString) Error() string {
return e.s
}
しかし、ライブラリによって、独自のerror
インターフェースを実装した構造体を定義して、エラーメッセージ以外の情報を付与していることがよくあります。これによって、受け取った(errorインターフェース型に抽象化された)エラーを型アサーションして元の型に戻して、付加された値を取り出すことができます。fmt.Errorf
は上記のerrorString
構造体に作り直してしまうのでこれをできなくしてしまいます。
例えば、JSONのKey, Valueの対応はSliceで表現することはできないのでUnmarshalするとエラーになりますが、そのエラーはJSONのバイト列におけるエラーが起こった位置やUnmarshalしようとした型の名前をエラーメッセージ以外を持っています。
具体的には、以下のUnmarshalTypeErrorがerrorインターフェースを実装しており、それが返ってきます。
// cf. https://golang.org/pkg/encoding/json/#UnmarshalTypeError
// An UnmarshalTypeError describes a JSON value that was
// not appropriate for a value of a specific Go type.
type UnmarshalTypeError struct {
Value string // description of JSON value - "bool", "array", "number -5"
Type reflect.Type // type of Go value it could not be assigned to
Offset int64 // error occurred after reading Offset bytes
Struct string // name of the struct type containing the field
Field string // name of the field holding the Go value
}
func (e *UnmarshalTypeError) Error() string {
if e.Struct != "" || e.Field != "" {
return "json: cannot unmarshal " + e.Value + " into Go struct field " + e.Struct + "." + e.Field + " of type " + e.Type.String()
}
return "json: cannot unmarshal " + e.Value + " into Go value of type " + e.Type.String()
}
実際に、対応していない型にUnmarshalするとUnmarshalTypeError型に型アサーションして、エラーメッセージ以外を取り出してみます。
import (
"encoding/json"
"fmt"
)
var jsonData = []byte(`
{
"name": "user",
"password": "pass"
}
`)
func main() {
// []string型はJSONのKey, Valueの内容を持つことができないのでUnmarshalするとエラーになる
valueOfInvalidType := make([]string, 0)
err := json.Unmarshal(jsonData, &valueOfInvalidType)
switch err := err.(type) {
case *json.UnmarshalTypeError:
fmt.Printf("type: %s\n", err.Type)
fmt.Printf("offet: %d\n", err.Offset)
fmt.Printf("Error(): %s\n", err)
default:
fmt.Println(err)
}
}
$ go run main.go
type: []string
offet: 2
Error(): json: cannot unmarshal object into Go value of type []string
このようにしたくとも、fmt.Errorf
が既存のError()
の結果以外の情報を切り捨ててしまうので、以下のような問題に直面します。
- errorインターフェースの元の型に応じて条件分岐ができなくなる
- errorインターフェースの元の型の持つコンテキスト情報が消失する
これらの問題を解決する手段でかつ、現在(2019/01/22)でデファクトとなっているエラーパッケージとして、pkg/errors
があります。
pkg/errorsを使う
では、具体的にpkg/errors
で上記問題を解決できるのかというと、以下のWrap
とCause
を用います。
func Wrap(err error, message string) error
func Cause(err error) error
まず、Wrap
を使うことで元のerrorインターフェースを実装した型と値を保持して、エラーメッセージだけコンテキスト情報を追加した新しいものにできます。
if err := json.Unmarshal(data, &jsonMap); err != nil {
// failed to unmarshal src.json: unexpected end of JSON input
return nil, errors.Wrap(err, "failed to unmarshal src.json")
}
この結果だけ見れば、fmt.Errorf("failed to unmarshal scr.json: %s", err)
した結果と同じですが、%+v
でフォーマットするとStackTraceを出力することもできます。
// cf. https://godoc.org/github.com/pkg/errors#hdr-Formatted_printing_of_errors
if err := json.Unmarshal(data, &jsonMap); err != nil {
fmt.Printf("%+v", err)
}
main.unmarshalToMap
/go/src/github.com/shoichiimamura/error-handling-example/main.go:30
main.main
/go/src/github.com/shoichiimamura/error-handling-example/main.go:18
runtime.main
/usr/local/go/src/runtime/proc.go:198
runtime.goexit
さらにCause
を使うことでWrap
されたエラーから元のerrorインターフェースを実装した型と値を取り出すことができます。
より厳密に言うと、causer
インターフェースを実装していない一番最後のerror
インターフェース型を取り出します。
// cf. https://github.com/pkg/errors/blob/master/errors.go#L269
func Cause(err error) error {
type causer interface {
Cause() error
}
for err != nil {
cause, ok := err.(causer)
if !ok {
break
}
err = cause.Cause()
}
return err
}
これを使うと、以下のように元のエラーの型に応じた条件分岐が可能になります。
switch err := errors.Cause(err).(type) {
case *json.UnmarshalTypeError:
fmt.Println(err.Offset)
case *json.InvalidUnmarshalError:
fmt.Println(err.Type)
default:
fmt.Println(err)
}
このCause
でエラーの型に応じた条件分岐ができることがわかりました。
しかし、実際のAPIを持つアプリケーションサーバーの開発では、エラーの種類に応じてHTTPステータスコードを変えたいことがよくあります。これを実現するためには、pkg/errors
を拡張したエラーパッケージを作る必要があります。次はpkg/errors
を利用して、エラーの種類の応じたHTTPステータスコードを返す実装をしてみます。
エラーに応じてステータスコードを選ぶ
pkg/errors
を使った以下のようなerrorインターフェースを実装した構造体を作ることで、Wrap
やCause
を使えつつ、エラータイプを取得できるようになります。errors.Wrapf
などは説明を簡単にするためにラップしていませんが、同じような要領で実装することもできます。
import (
"github.com/pkg/errors"
)
// ErrorType エラーの種類
type ErrorType uint
const (
Unknown ErrorType = iota
InvalidArgument
Unauthorized
ConnectionFailed
)
// ErrorTypeを返すインターフェース
type typeGetter interface {
Type() ErrorType
}
// ErrorTypeを持つ構造体
type customError struct {
errorType ErrorType
originalError error
}
// New 指定したErrorTypeを持つcustomErrorを返す
func (et ErrorType) New(message string) error {
return customError{errorType: et, originalError: errors.New(message)}
}
// Wrap 指定したErrorTypeと与えられたメッセージを持つcustomErrorにWrapする
func (et ErrorType) Wrap(err error, message string) error {
return customError{errorType: et, originalError: errors.Wrap(err, message)}
}
// Error errorインターフェースを実装する
func (e customError) Error() string {
return e.originalError.Error()
}
// Type typeGetterインターフェースを実装する
func (e customError) Type() ErrorType {
return e.errorType
}
// Wrap 受け取ったerrorがErrorTypeを持つ場合はそれを引き継いで与えられたエラーメッセージを持つcustomErrorにWrapする
func Wrap(err error, message string) error {
we := errors.Wrap(err, message)
if ce, ok := err.(typeGetter); ok {
return customError{errorType: ce.Type(), originalError: we}
}
return customError{errorType: Unknown, originalError: we}
}
// Cause errors.CauseのWrapper
func Cause(err error) error {
return errors.Cause(err)
}
// GetType ErrorTypeを持つ場合はそれを返し、無ければUnknownを返す
func GetType(err error) ErrorType {
for {
if e, ok := err.(typeGetter); ok {
return e.Type()
}
break
}
return Unknown
}
これによって、任意のErrorTypeを持つエラーを作って、Controller層でそれを取り出し、対応するステータスコードを選ぶことができます。
func main() {
err := Unauthorized.New("ある認証の処理内で返されたエラー")
fmt.Println(statusCode(err)) // 401
}
func statusCode(err error) int {
switch GetType(err) {
case ConnectionFailed:
return http.StatusInternalServerError // 500
case Unauthorized:
return http.StatusUnauthorized // 401
default:
return http.StatusBadRequest // 400
}
}
Panicでアプリケーションをクラッシュさせない
panicとは関数呼び出し元の処理を連続的に中断するgoの組み込み関数のことです。
明示的に呼び出すこともできますし、他にはnilポインタにメソッド呼び出しをした時などでも起きます。
type User struct {
Name string
}
func (u *User) Name() string {
return u.Name
}
var user *models.User
user.Name() // panic
panicが起こるとdefer内でrecover(後述)しないと、プログラムはCrashしてしまうので、そうさせないようにPanicが起きた旨をエラーにして返してあげるようにします。そのためには、まず、deferとrecoverについて概要を押さえる必要があります。
deferは、関数を登録することができ、その定義元の関数がreturnされた後に呼び出され、panicが起こった場合も呼びされます。
そのため以下の実装は、returnされた値(i
)をdeferでインクリメントして出力しているので、結果は2
になります。
func a() (i int) {
defer func() {
i++
fmt.Printf("%d\n", i)
}()
return 1
}
また、deferに登録した関数は後入れ先出し(Last in First out)で呼び出されるので、以下の実装は3210
を出力します。
func b() {
for i := 0; i < 4; i++ {
defer fmt.Print(i)
}
}
そして、recoverはpanicが起こったgoroutineを再び制御する組み込み関数で、panicが起こった後にdeferが呼ばれ、その中でpanicの伝播を止める役割を担います。defer外でrecoverしてもpanic時には呼び出されず、nilを返すだけなので、この使われ方以外はなさそうです。
以下は、panicが起こった時にdefer内でrecoverを呼び出し、panicの伝播を止めてエラーを返しています。
import (
"fmt"
"github.com/pkg/errors"
)
func panicAndRecover() (err error) {
defer func() {
if r := recover(); r != nil {
err = errors.New(fmt.Sprintf("recovered: %v\n", r))
}
}()
panic("panic at panicAndRecover")
return
}
func main() {
err := panicAndRecover()
fmt.Println(err)
// recovered: panic at panicAndRecover
}
こうすることで、panicが起こってもエラーとして扱うことができます。