これは、なにをしたくて書いたもの?
Goの本を読んだり、サンプルコードを見ていたりすると、こういうのを目にするのですが。
file, err = os.Create(filename)
if err == nil {
return
}
nilかどうかの判定はさておき…。
エラーの中身や種類に踏み込みたい場合は?とか、自分でエラーを定義する場合は?あたりが気になるので、
ちょっと調べてみることにしました。
インプット、それから
今回のエントリの主な情報源は、以下です。
The Go Programming Language Specification / Errors
Effective Go / Errors
errors - The Go Programming Language
Working with Errors in Go 1.13 - The Go Blog
言語仕様、Effective Goそれぞれのエラーのセクション、errorsパッケージ、それからGo 1.13以降のエラーを扱うことに
ついてのブログです。
このあたりを見たり、実際に動かしてみていろいろ考えたのですが、こんな感じでしょうか?
- 自分でエラーを作る、作らないに関係なく
- エラーに関する条件判定には
erros.Isを使う
- 具体的なエラーの型の変数に束縛する際には、
erros.Asを使う
- 自分でエラーを作る場合
erros.Newから始める
- 他のエラーを原因とする、簡単なエラーを作成する場合は
fmt.Errorfと%w書式を使う
- エラーの内容が固定できる場合は、
Errで始まる名前でパッケージからエクスポートしておく(※1)
- 関数呼び出し時にエラーのインスタンスを作成する場合は、
error.Isで比較可能な手段を提供しておく(※2)
- Variableと直接比較できないから
- エラーの発生原因等、比較可能な情報を使って
error.Isさせることになる?
- 自分でエラーに関する構造体を定義する場合は、
Error、少なくともUnwrapメソッドを作成する
- 型の名前は
Errorで終わるようにする
- 比較をカスタマイズする場合は
Is、Asの作成も考える
Goを見ている感じ、※1の場合は通常の型として、※2の場合はエラーの型をポインタとすることになるのかな?という
印象を持ちました。
エラーとは?
Goの言語仕様に関する内容を見てみます。
The Go Programming Language Specification / Errors
Goでいうエラーとは、以下のように定義されたerrorインターフェースです。要するに、stringを返すErrorメソッドを
実装しなさい、と。
type error interface {
Error() string
}
Goで使う多数の関数のうち、エラーが発生する可能性があるものは、成功した時の値とerrorのインスタンスを返すような
複数の戻り値を返せる定義になっています。
errorを返せる定義になっている関数の戻り値は、先に返されたerrorがnilかどうかを確認するところからスタート、となります。
ここで、Effective Goを見てみます。
Effective Go / Errors
errorがnilでない場合で、さらにエラーの中身に踏み込む場合(起こった事象や原因を知りたい場合など)は、
型アサーションを使って具体的な型の変数に代入してフィールドを見たりします。
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
}
これが、まずは基本(?)みたいですね。
errorsパッケージとfmtパッケージ
次に、errorsパッケージを見てみます。
errors - The Go Programming Language
errosパッケージには、errorを扱う関数が含まれています。
- 文字列(メッセージ)から
errorを作成するNew
errorに別のerrorが含まれる場合、それを取り出すUnwrap
errorを(Unwrapすることも含めて)特定のerrorと比較するIs
errorを(Unwrapしていき)特定の型の引数に割り当てるAs
https://github.com/golang/go/blob/go1.15.6/src/errors/errors.go
https://github.com/golang/go/blob/go1.15.6/src/errors/wrap.go
また、fmtパッケージのErrorf関数を使うことでもerrorを作成できます。
Package fmt / func Errorf
Errorf関数は指定した文字列とその中に含まれる書式からerrorを作成しますが、この時に%w書式とその引数としてerrorを
含めることにより、errors.Unwrapすることができるerror(原因となるerrorを含んだerrorを作成します。
https://github.com/golang/go/blob/go1.15.6/src/fmt/errors.go
エラーの原因を含めない場合は、fmt.Errorfはerrors.Newと完全に同等です。
では、このあたりのAPIを使ってちょっと確認していってみましょう。
環境
今回の環境は、こちら。
$ go version
go version go1.15.6 linux/amd64
モジュール作成。
$ go mod init error-handling
go: creating new go.mod: module error-handling
確認は、テストコードで行います。
go.mod
module error-handling
go 1.15
require github.com/stretchr/testify v1.7.0
標準パッケージに定義されているエラーで確認してみる
まずは、標準パッケージに定義されているエラーで確認してみましょう。
簡単に起こせるエラーでパッと思いつくものとして、開こうとしたファイルがなかった場合、をお題にしましょう。
Package os / func Open
エラーが起こった場合、*PathErrorが返ってくるようです。
Package os / type PathError
テストコードの雛形を用意。
file_read_error_test.go
package main
import (
"errors"
"github.com/stretchr/testify/assert"
"os"
"syscall"
"testing"
)
最初はシンプルに。返ってきたerrorがnilかどうかだけ確認。
func TestOpenFileSuccess(t *testing.T) {
file, err := os.Open("file_read_error_test.go")
if err == nil {
assert.Equal(t, "file_read_error_test.go", file.Name())
} else {
assert.Failf(t, "can't open file", "target file = %s", "file_read_error_test.go")
}
}
よくありそうなコードです。
次に、型アサーションを使って*PathErrorとして扱ってみます。
The Go Programming Language Specification / Type assertions
func TestOpenFileFailureTypeAssertion1(t *testing.T) {
_, err := os.Open("not_found_file")
if err == nil {
assert.FailNow(t, "file exists")
}
if pathError, isPathError := err.(*os.PathError); isPathError {
assert.Equal(t, "open", pathError.Op)
assert.Equal(t, "not_found_file", pathError.Path)
assert.Equal(t, "no such file or directory", pathError.Err.Error())
assert.Equal(t, syscall.ENOENT, pathError.Err)
errno, isErrno := pathError.Err.(syscall.Errno)
assert.True(t, isErrno)
assert.True(t, errno.Is(os.ErrNotExist))
} else {
assert.FailNow(t, "not PathError")
}
}
*PathError型にできてしまえば、いくつかフィールドがあるのでもう少し詳細な内容を確認できます。
assert.Equal(t, "open", pathError.Op)
assert.Equal(t, "not_found_file", pathError.Path)
assert.Equal(t, "no such file or directory", pathError.Err.Error())
また*PathErrorはErrというフィールドにエラーの発生原因を持っていて、こちらでosパッケージを使ってエラーの種類を
確認できます。
assert.Equal(t, syscall.ENOENT, pathError.Err)
errno, isErrno := pathError.Err.(syscall.Errno)
assert.True(t, isErrno)
assert.True(t, errno.Is(os.ErrNotExist))
osパッケージに定義されているエラーは、Variablesを見るとわかります。
Package os / Variables
今回はファイルが存在しないので、ErrNotExistとなります。
なお、これらのエラーはsyscallパッケージのErrnoという型(実体はuintptr)です。
Package syscall / type Errno
型の判定は、Type switchesを使う方法もあるでしょう。
The Go Programming Language Specification / Type switches
func TestOpenFileFailureTypeAssertion2(t *testing.T) {
_, err := os.Open("not_found_file")
if err == nil {
assert.FailNow(t, "file exists")
}
switch err := err.(type) {
case *os.PathError:
assert.Equal(t, "open", err.Op)
assert.Equal(t, "not_found_file", err.Path)
assert.Equal(t, "no such file or directory", err.Err.Error())
assert.Equal(t, syscall.ENOENT, err.Err)
switch errno := err.Err.(type) {
case syscall.Errno:
assert.True(t, errno.Is(os.ErrNotExist))
default:
assert.FailNow(t, "not Errno")
}
switch err.Err {
case syscall.ENOENT:
default:
assert.FailNow(t, "not ENOENT")
}
default:
assert.FailNow(t, "not PathError")
}
}
この部分に関しては、値でも比較できます。
switch err.Err {
case syscall.ENOENT:
default:
assert.FailNow(t, "not ENOENT")
}
次に、これらをerrorsパッケージを使って書き換えていきましょう。
errors - The Go Programming Language
Asから。Asを使用すると、エラー(とその内部的に持っている原因となったエラー)に対して指定した変数の型に合致するものが
あれば、その変数に値を設定しtrueを返します。そうでなければfalseを返します。
Package errors / func As
こんな感じですね。先ほどの*PathErrorに対する型アサーションを書き換えたものです。
func TestOpenFileFailureUsingErrorsAs1(t *testing.T) {
_, err := os.Open("not_found_file")
if err == nil {
assert.FailNow(t, "file exists")
}
var pathError *os.PathError
if errors.As(err, &pathError) {
assert.Equal(t, "open", pathError.Op)
assert.Equal(t, "not_found_file", pathError.Path)
assert.Equal(t, "no such file or directory", pathError.Err.Error())
assert.Equal(t, syscall.ENOENT, pathError.Err)
var errno syscall.Errno
if errors.As(err, &errno) {
assert.True(t, errno.Is(os.ErrNotExist))
} else {
assert.FailNow(t, "not Errno")
}
} else {
assert.FailNow(t, "not PathError")
}
}
内部的に持っているエラー…チェインされたエラーに対しても見れるという話なので、*PathErrorからいきなりsyscall.Errnoまで
引き抜くことも可能です。
func TestOpenFileFailureUsingErrorsAs2(t *testing.T) {
_, err := os.Open("not_found_file")
if err == nil {
assert.FailNow(t, "file exists")
}
var errno syscall.Errno
if errors.As(err, &errno) {
assert.True(t, errno.Is(os.ErrNotExist))
} else {
assert.FailNow(t, "not Errno")
}
}
次は、Isです。Isを使うと、エラー(とその内部的に持っている原因となったエラー)に対して、引数で指定したerrorに
合致するものがあれば、trueを返します。そうでなければfalseを返します。
Package errors / func Is
要するに、Asで変数に値を束縛しない版みたいなものです。
シンプルに、条件分岐で使うのでしょう。
func TestOpenFileFailureUsingErrorsIs(t *testing.T) {
_, err := os.Open("not_found_file")
if err == nil {
assert.FailNow(t, "file exists")
}
if errors.Is(err, os.ErrNotExist) {
pathError, _ := err.(*os.PathError)
assert.Equal(t, "no such file or directory", pathError.Err.Error())
} else {
assert.FailNow(t, "not PathError")
}
}
*PathErrorの場合、値として比較するerrorがないのでos.ErrNotExistで比較しました。これでtrueになります。
if errors.Is(err, os.ErrNotExist) {
最後はUnwrapです。Unwrapを使うと、指定したerrorがUnwrapメソッドを実装していた場合、Unwrapメソッドの
呼び出し結果を返します。要するに、errorの原因が返ります。Unwrapメソッドを実装していなかった場合は、nilが
返ります。
Package errors / func Unwrap
こんな感じです。
func TestOpenFileFailureUsingErrorsUnwrap(t *testing.T) {
_, err := os.Open("not_found_file")
if err == nil {
assert.FailNow(t, "file exists")
}
unwrapped := errors.Unwrap(err)
if errno, isErrno := unwrapped.(syscall.Errno); isErrno {
assert.True(t, errno.Is(os.ErrNotExist))
} else {
assert.FailNow(t, "not PathError")
}
}
ファイル未存在の場合の*PathError型の変数をerrors.Unwrapに渡すと、その原因(syscall.Errno)が取得できます。
errorsパッケージの使い方はこんな感じです。
ところで、今回のお題では*PathError、syscall.Errno、os.ErrNotExistと3つ扱うことになっていろいろややこしかったのですが、
改めてと見ると
という定義になっています。いずれも、errorインターフェースの定義を満たしています。
*PathErrorはErrフィールドをUnwrapで返すようになっていたことから、
https://github.com/golang/go/blob/go1.15.6/src/os/error.go#L58
*PathError → 原因 → syscall.Errno → 実体の型 or 原因 → os.ErrNotExistみたいな関係になっているのかなと思ったのですが。
そうではなく、syscall.ErrnoはIsで受け取ったerrorの値を使った比較処理を行っていたので
https://github.com/golang/go/blob/go1.15.6/src/syscall/syscall_unix.go#L126-L136
*PathErrorをUnwrapした結果を==で直接os.ErrNotExistと比較してもtrueにならずに、だいぶ混乱しました。
errors.Isで比較すると、trueになるのに…と。
Isの使い方をちゃんと見た感じですね…。
また、Errnoの実体が持つエラーに割り当てられた数字は、以下で確認でき、
Package syscall / Constants
https://github.com/golang/go/blob/go1.15.6/src/syscall/tables_js.go#L102-L228
os.Err〜に割り当てられる実体は、こちらを見ると確認できます。
https://github.com/golang/go/blob/go1.15.6/src/os/error.go#L29-L34
だいぶ、型に振り回されましたね…。
自分でエラーを定義する
ここまでは、標準パッケージに定義されているエラーを使いましたが、ここからは自分でエラーを定義していきましょう。
コードの雛形を書いていきます。
エラーを定義する方。
myerrors.go
package main
import (
"errors"
)
テストコードを書く方。
myerrors_test.go
package main
import (
"errors"
"fmt"
"github.com/stretchr/testify/assert"
"os"
"syscall"
"testing"
)
自分でエラーを定義する1番シンプルなパターンとしては、errors.New、fmt.Errorfを使う方法かな、と。
Package errors / func New
Package fmt / func Errorf
errors.Newは文字列からerrorを作成し、fmt.Errorfは書式も使えつつ文字列からerrorを作成します。
テストコードで定義すると、こんな感じに。
func TestCreateError(t *testing.T) {
error1 := errors.New("Oops!!")
assert.Error(t, error1)
assert.Equal(t, "Oops!!", error1.Error())
assert.Nil(t, errors.Unwrap(error1))
error2 := fmt.Errorf("Oops!!")
assert.Error(t, error2)
assert.Equal(t, "Oops!!", error2.Error())
assert.Nil(t, errors.Unwrap(error2))
}
どちらもErrorメソッドを実装しています。この場合は、errors.Unwrapを呼び出しても双方nilです。
ただ、fmt.Errorfの場合、書式に%wを含め、そこにerrorをあてはめるとUnwrapで指定したerrorが返るように構築されます。
こんな感じですね。
func TestWrapedError1(t *testing.T) {
_, err := os.Open("not_found_file")
wrappedError := fmt.Errorf("Open file error, reason [%w]", err)
assert.Error(t, wrappedError)
assert.Equal(t, "Open file error, reason [open not_found_file: no such file or directory]", wrappedError.Error())
assert.Equal(t, "*fmt.wrapError", fmt.Sprintf("%T", wrappedError))
causeError := errors.Unwrap(wrappedError)
assert.True(t, err == causeError)
assert.True(t, errors.Is(wrappedError, os.ErrNotExist))
assert.ErrorIs(t, wrappedError, os.ErrNotExist)
}
ファイル未存在のエラーを元にして%wに与え
_, err := os.Open("not_found_file")
wrappedError := fmt.Errorf("Open file error, reason [%w]", err)
これをUnwrapすると%wに指定したものと同じであることが確認できます。
causeError := errors.Unwrap(wrappedError)
assert.True(t, err == causeError)
あまり意味はないですが、%wだけを与えてUnwrapで返させることもできます。
func TestWrapedError2(t *testing.T) {
_, err := os.Open("not_found_file")
wrappedError := fmt.Errorf("%w", err)
assert.Error(t, wrappedError)
assert.Equal(t, "open not_found_file: no such file or directory", wrappedError.Error())
assert.Equal(t, "*fmt.wrapError", fmt.Sprintf("%T", wrappedError))
causeError := errors.Unwrap(wrappedError)
assert.True(t, err == causeError)
assert.True(t, errors.Is(wrappedError, os.ErrNotExist))
assert.ErrorIs(t, wrappedError, os.ErrNotExist)
}
myerrors.go側に、Variableとしてerrorを定義(エクスポート)し、関数呼び出しの結果としてerrorを返すようにしてみましょう。
var (
ErrMySimpleError = errors.New("My Error")
)
func GetMySimpleErr() error {
return ErrMySimpleError
}
Variableとしてエクスポートするerrorの名前は、Errで始めるのが通例のようです。
テストコードはこんな感じで。
func TestGetMySimpleErr(t *testing.T) {
err := GetMySimpleErr()
assert.Equal(t, ErrMySimpleError, err)
assert.Equal(t, "My Error", err.Error())
assert.Equal(t, "*errors.errorString", fmt.Sprintf("%T", err))
}
最後に、エラーを構造体で定義してみます。
2つ用意して、片方はエラーの原因を持てるようにしました。ところで、エラーとなる型の名前はErrorで終わらせるのが通例の
ようです。1とか2とか付けてしまいました…。
type MyError1 struct {
Message string
}
func (e MyError1) Error() string {
return e.Message
}
func (e MyError1) Is(target error) bool {
return errors.Is(e, target)
}
type MyError2 struct {
Message string
Cause error
}
func (e *MyError2) Error() string {
return e.Message + ": " + e.Cause.Error()
}
func (e *MyError2) Unwrap() error {
return e.Cause
}
Errorメソッドを実装するのはerrorとしての必須条件ですが、MyError1の方はIsを、MyError2の方はUnwrapを実装して
おきました。
MyError1の方は、Variableとして固定で定義します。
var (
ErrMyError = MyError1{Message: "Oops!!"}
)
MyError2の方は、実行時にエラーの原因を含んで動的に作成することにします。
これら2つのエラーを、引数の指定でどちらかのエラー(もしくは正常に値を返す)を返すような関数を作成します。
func Execute(word string, cause error) (string, error) {
switch word {
case "Error1":
return "NG", ErrMyError
case "Error2":
e := &MyError2{Message: "Oops!!", Cause: cause}
return "NG", e
default:
return "OK", nil
}
}
MyError2の方は、この関数呼び出し時に構造体のインスタンスを作成していますが、ポインタを返すようにしました。
*PathErrorと同じですね。
https://github.com/golang/go/blob/go1.15.6/src/os/file_unix.go#L210
他のパッケージもいくつか見たのですが、関数の呼び出し時にエラー発生時の情報を含めるなどの理由でインスタンスを作ったものは、
ポインタを返しているように思います。それに習いました。
テストコードで確認してみます。
MyError1の確認。
func TestMyCustomError1(t *testing.T) {
_, err := os.Open("not_found_file")
result, myerr := Execute("Error1", err)
assert.Equal(t, "NG", result)
assert.True(t, myerr == ErrMyError)
assert.Equal(t, "Oops!!", myerr.Error())
assert.True(t, errors.Is(myerr, ErrMyError))
assert.Nil(t, errors.Unwrap(myerr))
var e MyError1
assert.True(t, errors.As(myerr, &e))
}
関数の戻り値となったerrorを、==やerrors.Isで比較できます。
assert.True(t, myerr == ErrMyError)
assert.True(t, errors.Is(myerr, ErrMyError))
Unwrapは実装していないのでnilですね。
assert.Nil(t, errors.Unwrap(myerr))
errors.Asで変数に束縛もできます。
var e MyError1
assert.True(t, errors.As(myerr, &e))
続いて、MyError2を使う方。
func TestMyCustomError2(t *testing.T) {
_, err := os.Open("not_found_file")
_, myerr := Execute("Error2", err)
assert.Equal(t, "Oops!!: open not_found_file: no such file or directory", myerr.Error())
assert.True(t, errors.Is(myerr, os.ErrNotExist))
assert.True(t, errors.Unwrap(myerr) == err)
var e *MyError2
assert.True(t, errors.As(myerr, &e))
var errno syscall.Errno
assert.True(t, errors.As(myerr, &errno))
}
Isメソッドは実装していないのですが、普通に使えます。
assert.True(t, errors.Is(myerr, os.ErrNotExist))
Unwrapメソッドを実装していれば、そちらも見てくれるので。
https://github.com/golang/go/blob/go1.15.6/src/errors/wrap.go#L39-L59
単純比較、Is、Unwrapと推移していくので、比較に特殊な条件がなければUnwrapを実装しておくのがよいのでしょう。
errors.Unwrapでは、エラーの原因が取得できます。これは、Unwrapメソッドを実装したからですね。
assert.True(t, errors.Unwrap(myerr) == err)
errors.Asでの変数への束縛。
var e *MyError2
assert.True(t, errors.As(myerr, &e))
こちらも、Asメソッドを実装していなくてもUnwrapメソッドの呼び出し結果を使ってくれます。
https://github.com/golang/go/blob/go1.15.6/src/errors/wrap.go#L77-L101
*PathErrorの時と同じように、errorsAsで一気にsyscall.Errno`を取得することもできます。
var errno syscall.Errno
assert.True(t, errors.As(myerr, &errno))
こんな感じでしょうか。
まとめ
Goのエラーに関するAPIがよくわからなかったので、標準ライブラリの使い方、自分でエラーを定義する場合を含めて、
いろいろ試してみました。
ある程度使い方がわかったかな?という気がします。