CLOVER🍀

That was when it all began.

Goの゚ラヌに関するAPIを孊ぶ

これは、なにをしたくお曞いたもの

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()  // Recover some space.
        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:
            // no-op
        default:
            assert.FailNow(t, "not ENOENT")
        }
    default:
        assert.FailNow(t, "not PathError")
    }
}

この郚分に関しおは、倀でも比范できたす。

       switch err.Err {
        case syscall.ENOENT:
            // no-op
        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がよくわからなかったので、暙準ラむブラリの䜿い方、自分で゚ラヌを定矩する堎合を含めお、
いろいろ詊しおみたした。

ある皋床䜿い方がわかったかなずいう気がしたす。