[Golang] エラーハンドリング周りの知識
そもそものGoのerrorとは?
Goのエラーは単なる値である。と実用Go言語では記載されている。
これはどういったことか。詳しく見てみると、
- Goのエラーは
error型
のinterfaceを満たした単なる値である
と解説がある。
Goのランタイムに組み込まれており、interfaceとして定義されている。
type error interface {
Error() string
}
このinterfaceを満たしているものは error
intarfaceを満たしていることになるため、 error
として扱うことが可能になる。
Goでエラーを書く方法
主に3つ存在する。
- 標準ライブラリ:errors.New()
- 標準ライブラリ : fmt.Errorf()
- 独自でerror interfaceを満たす型を作成する
標準ライブラリ:errors.New() での実装
使い方は以下。
単純に呼び出し元にエラーであることを伝えるときに使用することが多い。
var ErrNotUser = errors.New("user not found")
標準ライブラリ : fmt.Errorf() での実装
フォーマットされた文字列を元にエラーを生成できる。また、エラーをラップすることで抽象度をあげたエラー内容にすることもできる。
エラーのラップについては後述。
func validate(length int) error {
if length <= 0 {
return fmt.Errorf("length must be greater than 0, length = %d", length)
}
}
独自でerror interfaceを満たす型を実装
エラーが発生した時の情報を構造体のフィールドとして保持しておきたい場合などに有効。
type HTTPError struct {
StatusCode int
URL string
}
func (he *HTTPError) Error() string {
return fmt.Sprintf("http status code = %d, url = %s", he.StatusCode, he.URL)
}
func ReadContents(url string) ([]byte, error) {
resp, err := http.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if err.StatusCode != http.StatusOK {
return nil, &HTTPError{
StatusCode: resp.StatusCode,
URL: url
}
}
}
エラーのラップ、アンラップ
初めラップとアンラップを聞いたときは全く理解ができなかった。ただ、ラップは既存のエラーの上から何かを被せるようなイメージ、アンラップはその反対であることは想像がついた。
以下の記事がラップについて噛み砕いて説明してくれており、わかりやすかった。
エラーをラップする
エラーをラップするには、さきほどエラーを書く方法で説明した fmt.Errorf
を使ってできる。
errors package のOverviewにも
ラップされたエラーを作る簡単な方法は、fmt.Errorfを呼び出し、エラーの引数に%w動詞を適用することである。
と記載があった。
通常は fmt.Errorf
は errors.errorString
型のエラーを返すが、以下の条件を満たす場合、 fmt.wrapError
型のエラーを返すようになる。
- フォーマット演算子に
%w
を使うこと -
%w
を置き換えるのがerror
型であること
fmt.Errorf
の実装を確認
func Errorf(format string, a ...interface{}) error {
p := newPrinter()
p.wrapErrs = true
p.doPrintf(format, a)
s := string(p.buf)
var err error
// 以下の処理でエラーの型を決定している。
if p.wrappedErr == nil {
err = errors.New(s)
} else {
err = &wrapError{s, p.wrappedErr}
}
p.free()
return err
}
p.wrappedErr
がnilか確認を行い、その結果によってエラーの型を変更していることがわかる。
まずは errors.errorString型
がどのような型かを確認する。
type errorString struct {
s string
}
errors.errorString
はエラー内容を単純な文字列として保持している。
そして、fmt.wrapError型
も確認する
type wrapError struct {
msg string // ラップ後のエラー文字列を格納する
err error // ラップ前のエラー文字列を格納する
}
fmt.wrapError
はerr
に元のエラーを格納することでエラーのラップを可能にしている。
これを踏まえて、fmt.Errorf
を使ってエラーのラップをしてみる。
user, err := getUser(email)
if err != nil {
return fmt.Errorf("fail to get user with email(%s): %w", email, err)
}
上記のように使える。
なぜラップするのか、何が嬉しいのか
エラーに情報が追加できるから
(他にもあると思うけど、今までの経験からこれしかわからない)
例えば、 I/Oエラーが起きた(原因)
というエラーメッセージよりも、コピーに失敗した(結果)
というようなエラーメッセージの方が質が良いと思う。どのレイヤーで失敗しているかなどの情報を付与してあげることでエラーの原因を探りやすくなる。
例えば以下のような2つのエラー内容があったとして、
googleapi: Error 409: Already Exists: Table test, duplicate
duplicate
この2つのエラー内容を見て、より詳細が分かりやすいのは上記であることは明白。
このようにエラー内容をラップ(情報を付与)することでどこでどんなエラーが発生しているのかを確認しやすくできるのがメリット。
エラーをアンラップする
上記のようにラップされたエラーを取り出すためには、errors.Unwrap
を使う。
fmt.Errorf
では、%w
を使ってラップすることによってfmt.wrapError型
になることが分かったが、fmt.wrapError型
には Unwrap
メソッドが定義されている。
使い方は以下
err := errors.New("deplicate")
newErr := fmt.Errorf("googleapi: Error 409: Already Exists: Table test, %w", err) // errをラップ
fmt.Println(newErr) // googleapi: Error 409: Already Exists: Table test, duplicate
fmt.Println(errors.Unwrap(newErr)) // deplicate
errors.Unwrap
は以下のように実装されている。
func Unwrap(err error) error {
u, ok := err.(interface { // 渡されたerrがUnwrapを実装しているか
Unwrap() error
})
if !ok { // Unwrapを実装していなければnilを返す
return nil
}
return u.Unwrap() // Unwrapを実装していればそのUnwrapを実行した結果を返す
}
fmt.wrapError型
には Unwrap
メソッドが定義されていると説明したが、そのUnwrap
メソッドは以下のように定義されている。
type wrapError struct {
msg string
err error
}
func (e *wrapError) Error() string {
return e.msg
}
// これ
func (e *wrapError) Unwrap() error {
return e.err
}
Unwrap
メソッドは単純にe.err
を返却するだけであり、これは先述したようにラップ前のエラー文字列を格納
しているため、
fmt.Println(errors.Unwrap(newErr)) // deplicate
という結果になる。
これを理解しておくと、この後エラーを比較するerrors.Is
やerrors.As
の理解が早くなると思った。
Goのエラーの比較方法
エラーの比較方法は2つ。
- エラーを値として比較する
errors.Is
- エラーを型として比較する
errors.As
errors.Is
errors.Is
は、以下のように実装されている。
func Is(err, target error) bool {
if target == nil {
return err == target
}
// targetが比較可能であるか
isComparable := reflectlite.TypeOf(target).Comparable()
for {
// targetが比較可能であれば、errとtargetを比較
if isComparable && err == target {
return true
}
// 比較できないエラーや == で合致しなかった場合は
// errにIs(error) bool が実装されているか確認し
// 実装されていればIsを使って評価する
if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) {
return true
}
// Isを実装していない場合や、Isの結果が合致しなかった場合は
// errをUnwrapし、次のerrを比較する
// もしUnwrapできない場合はnilが返却されるので、その時はfalseが返る
if err = Unwrap(err); err == nil {
return false
}
}
}
errors.Is
を使ってみる。
var ErrNotRead = errors.New("not readable")
func main() {
readableErrByIs(ErrNotRead) // not readable
readableErrByIs(fmt.Errorf("wrapping: %w", ErrNotRead)) // wrapping: not readable
}
func readableErrByIs(err error) {
if errors.Is(err, ErrNotRead) {
fmt.Println(err.Error())
}
}
初めは実用Go言語に「値の比較」とあったため、not readable
という文字列を比較していると思っていたが、実装を見ると「ラッピングされたエラーであっても、エラーが任意エラーと一致するかどうか」を検証していることが分かった。
errors.As
errors.As
は以下のように実装されている。
func As(err error, target any) bool {
if target == nil {
panic("errors: target cannot be nil")
}
val := reflectlite.ValueOf(target) // ①
typ := val.Type()
if typ.Kind() != reflectlite.Ptr || val.IsNil() { // ①'
panic("errors: target must be a non-nil pointer")
}
targetType := typ.Elem()
if targetType.Kind() != reflectlite.Interface && !targetType.Implements(errorType) { // ②
panic("errors: *target must be interface or implement error")
}
for err != nil {
if reflectlite.TypeOf(err).AssignableTo(targetType) { // ③
val.Elem().Set(reflectlite.ValueOf(err)) // ④
return true
}
if x, ok := err.(interface{ As(any) bool }); ok && x.As(target) {
return true
}
err = Unwrap(err) // ⑤
}
return false
}
①, ①':引数のtargetに有効なポインターが入っているか確認
②:引数targetがインターフェースではない場合、もしくはerrorインターフェースであるか
③:引数targetに引数errが代入可能であるか確認
④:代入できればtrueを返す
⑤:Unwrapして次のエラーを比較する
errors.As
は「型として比較」すると記載しているが、これは上記③・④がその特徴を表しているんだなと思った。
まとめ
いままでなんとなく使っていたが、内部の実装を確認しにいったり、アウトプット前提で調査すると頭に入りやすいことを再認識できた。
Discussion