CLOVER🍀

That was when it all began.

Goのxerrorsパッケージを試す

これは、なにをしたくて書いたもの?

Goのエラー処理に関する情報を見ているとxerrorsというものがよく出てくるので、1度見ておこうかなと思いまして。

xerrorsパッケージ

xerrorsパッケージは、Goのエラーハンドリングのためのパッケージです。

xerrors · pkg.go.dev

GitHub - golang/xerrors

このパッケージの提案内容は、こちら。

Proposal: Go 2 Error Inspection

これが標準パッケージ、errorsに取り込まれたのがGo 1.13のようです。

errors - The Go Programming Language

errorsパッケージについては、以前このエントリで試してみました。

Goのエラーに関するAPIを学ぶ - CLOVER🍀

xerrorsパッケージが持っている機能のうち、ほとんどのものはerrorsパッケージに導入されたようです。
でも、導入されなかったものもあるようです。

それが呼び出し元のフレームを保存する機能のようですね。

今回は、xerrorsパッケージを試してみたいと思います。

環境

今回の環境は、こちらです。

$ go version
go version go1.16 linux/amd64
$ go mod init xerrors-example
go: creating new go.mod: module xerrors-example

go.mod

module xerrors-example

go 1.16

require golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect

go.sum

golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

errorsとxerrorsを比べながら使ってみる

では、使っていってみましょう。標準パッケージerrorsの元になっただけあって、とてもとても良く似ています。

errors - The Go Programming Language

xerrors · pkg.go.dev

以下の対比になるようです。

  • errors.New - xerros.New
  • fmt.Errorf - xerrors.Errorf
  • errors.Is - xerrors.Is
  • errors.As - xerrors.As
  • errors.Unwrap - xerrors.Unwrap

エラーの原因を含むものについては、errorsパッケージではなくfmtパッケージを使いますね。

また、errorsと対応先のないものとして、xerrors.Opaqueというのもあります。

今回は、NewおよびErrorfを中心に使っていきましょう。他はだいたい同じようなので。

作成したソースコードは、こちらです。呼び出し位置の表示があることもあって、最初は行番号付きで表示しておきます。

$ cat -n main.go

     1 package main
     2  
     3 import (
     4     "errors"
     5     "fmt"
     6     "runtime"
     7  
     8     "golang.org/x/xerrors"
     9 )
    10  
    11 func main() {
    12     fmt.Println("Case1:\n")
    13  
    14     fmt.Printf("New error as errors: %v\n", errors.New("Oops!!"))
    15     fmt.Println()
    16     fmt.Printf("New error as errors: %+v\n", errors.New("Oops!!"))
    17  
    18     printSeparator()
    19  
    20     fmt.Printf("New error as xerrors: %v\n", xerrors.New("Oops!!"))
    21     fmt.Println()
    22     fmt.Printf("New error as xerrors: %+v\n", xerrors.New("Oops!!"))
    23  
    24     fmt.Println("\nCase2:\n")
    25  
    26     fmt.Printf("call error as errors: %+v\n", callErrors())
    27  
    28     printSeparator()
    29  
    30     fmt.Printf("call error as xerrors: %+v\n", callXerrors())
    31  
    32     fmt.Println("\nCase3:\n")
    33  
    34     fmt.Printf("wrap error as errors: %+v\n", wrapErrors())
    35  
    36     printSeparator()
    37  
    38     fmt.Printf("wrap error as xerrors: %+v\n", wrapXerrors())
    39  
    40     fmt.Println("\nCase4:\n")
    41  
    42     fmt.Printf("unwrap as errors: %+v\n", errors.Unwrap(wrapErrors()))
    43  
    44     printSeparator()
    45  
    46     fmt.Printf("unwrap as xerrors: %+v\n", xerrors.Unwrap(wrapXerrors()))
    47  
    48     fmt.Println()
    49  
    50     fmt.Printf("unwrap as xerrors / errors.Unwrap: %+v\n", errors.Unwrap(wrapXerrors()))
    51  
    52     fmt.Println("\nCase5:\n")
    53  
    54     fmt.Printf("wrap error as errors: %+v\n", wrapErrors())
    55  
    56     printSeparator()
    57  
    58     fmt.Printf("wrap error as xerrors with opaque: %+v\n", xerrors.Opaque(wrapXerrors()))
    59  
    60     fmt.Println("\nCase6:\n")
    61  
    62     pc, file, line, ok := callRuntimeCaller0()
    63     fmt.Printf("%v, %s, %d, %t\n", pc, file, line, ok)
    64  
    65     printSeparator()
    66  
    67     pc, file, line, ok = callRuntimeCaller1()
    68     fmt.Printf("%v, %s, %d, %t\n", pc, file, line, ok)
    69  
    70     printSeparator()
    71  
    72     pcs := callRuntimeCallers0()
    73     frames := runtime.CallersFrames(pcs)
    74  
    75     for {
    76         frame, more := frames.Next()
    77         fmt.Printf("  %s(%s:%d)\n", frame.Function, frame.File, frame.Line)
    78  
    79         if !more {
    80             break
    81         }
    82     }
    83  
    84     printSeparator()
    85  
    86     pcs = callRuntimeCallers1()
    87     frames = runtime.CallersFrames(pcs)
    88  
    89     for {
    90         frame, more := frames.Next()
    91         fmt.Printf("  %s(%s:%d)\n", frame.Function, frame.File, frame.Line)
    92  
    93         if !more {
    94             break
    95         }
    96     }
    97  
    98     printSeparator()
    99  
   100     pcs = wrapCallRuntimeCaller1()
   101     frames = runtime.CallersFrames(pcs)
   102  
   103     for {
   104         frame, more := frames.Next()
   105         fmt.Printf("  %s(%s:%d)\n", frame.Function, frame.File, frame.Line)
   106  
   107         if !more {
   108             break
   109         }
   110     }
   111 }
   112  
   113 func printSeparator() {
   114     fmt.Println()
   115     fmt.Println("======")
   116     fmt.Println()
   117 }
   118  
   119 func causeErrorUseErrors() error {
   120     return errors.New("Oops!!")
   121 }
   122  
   123 func callErrors() error {
   124     return causeErrorUseErrors()
   125 }
   126  
   127 func wrapErrors() error {
   128     return fmt.Errorf("Wrap Error!! cause: %w", causeErrorUseErrors())
   129 }
   130  
   131 func causeErrorUseXerrors() error {
   132     return xerrors.New("Oops!!")
   133 }
   134  
   135 func callXerrors() error {
   136     return causeErrorUseXerrors()
   137 }
   138  
   139 func wrapXerrors() error {
   140     return xerrors.Errorf("Wrap Error!! cause: %w", causeErrorUseXerrors())
   141 }
   142  
   143 func callRuntimeCaller0() (uintptr, string, int, bool) {
   144     return runtime.Caller(0)
   145 }
   146  
   147 func callRuntimeCaller1() (uintptr, string, int, bool) {
   148     return runtime.Caller(1)
   149 }
   150  
   151 func callRuntimeCallers0() []uintptr {
   152     var pcs [3]uintptr
   153     runtime.Callers(0, pcs[:])
   154     return pcs[:]
   155 }
   156  
   157 func callRuntimeCallers1() []uintptr {
   158     var pcs [3]uintptr
   159     runtime.Callers(1, pcs[:])
   160     return pcs[:]
   161 }
   162  
   163 func wrapCallRuntimeCaller1() []uintptr {
   164     return callRuntimeCallers1()
   165 }

実行は、こちらで。

$ go run main.go

関連するソースコードの抜粋と、実行結果を使って書いてきます。

%+vと呼び出し元の表示

最初はこちら。New関数を使って、エラーを作成します。

Package errors / func New

xerrors / func New

    12     fmt.Println("Case1:\n")
    13  
    14     fmt.Printf("New error as errors: %v\n", errors.New("Oops!!"))
    15     fmt.Println()
    16     fmt.Printf("New error as errors: %+v\n", errors.New("Oops!!"))
    17  
    18     printSeparator()
    19  
    20     fmt.Printf("New error as xerrors: %v\n", xerrors.New("Oops!!"))
    21     fmt.Println()
    22     fmt.Printf("New error as xerrors: %+v\n", xerrors.New("Oops!!"))

実行結果。

Case1:

New error as errors: Oops!!

New error as errors: Oops!!

======

New error as xerrors: Oops!!

New error as xerrors: Oops!!:
    main.main
        /path/to/main.go:22

xerrors.Newを使い、かつエラー表示の書式に%+vを指定した場合にファイル名と行番号が表示されました。

  fmt.Printf("New error as xerrors: %+v\n", xerrors.New("Oops!!"))

errors.Newの方は、エラーの文字列表示のみです。

%+vの意味は、フィールドを加えたものです。

%v the value in a default format when printing structs, the plus flag (%+v) adds field names

fmt - The Go Programming Language

ソースコードを見ると、Frameを出力していることになりますね。

https://github.com/golang/xerrors/blob/5ec99f83aff198f5fbd629d6c8d8eb38a04218ca/errors.go#L12

これがxerrorsを使った時に、フレームと呼ばれているもののようです。

関数呼び出しを挟んでみる

次は、New関数と標準出力の間に、関数呼び出しを挟んでみましょう。

    24     fmt.Println("\nCase2:\n")
    25  
    26     fmt.Printf("call error as errors: %+v\n", callErrors())
    27  
    28     printSeparator()
    29  
    30     fmt.Printf("call error as xerrors: %+v\n", callXerrors())

呼び出し先の関数。

   119 func causeErrorUseErrors() error {
   120     return errors.New("Oops!!")
   121 }
   122  
   123 func callErrors() error {
   124     return causeErrorUseErrors()
   125 }

〜省略〜

   131 func causeErrorUseXerrors() error {
   132     return xerrors.New("Oops!!")
   133 }
   134  
   135 func callXerrors() error {
   136     return causeErrorUseXerrors()
   137 }

実行結果。

Case2:

call error as errors: Oops!!

======

call error as xerrors: Oops!!:
    main.causeErrorUseXerrors
        /path/to/main.go:132

どうも、エラーが生成されたところまでのコールスタックが表示されるようなものではなさそうです。
ここに表示されているのは、xerrors.Newを呼び出した場所そのものですね。

エラーをラップしてみる

次は、エラーをラップしてみましょう。fmt.Errorfおよびxerrors.Errorfと、書式文字列の%wを使います。

Package fmt / func Errorf

xerrors / func Errorf

    32     fmt.Println("\nCase3:\n")
    33  
    34     fmt.Printf("wrap error as errors: %+v\n", wrapErrors())
    35  
    36     printSeparator()
    37  
    38     fmt.Printf("wrap error as xerrors: %+v\n", wrapXerrors())

呼び出し先。

   119 func causeErrorUseErrors() error {
   120     return errors.New("Oops!!")
   121 }
   122  

〜省略〜

   126  
   127 func wrapErrors() error {
   128     return fmt.Errorf("Wrap Error!! cause: %w", causeErrorUseErrors())
   129 }


〜省略〜

   131 func causeErrorUseXerrors() error {
   132     return xerrors.New("Oops!!")
   133 }
   134  

〜省略〜

   138  
   139 func wrapXerrors() error {
   140     return xerrors.Errorf("Wrap Error!! cause: %w", causeErrorUseXerrors())
   141 }

結果。

Case3:

wrap error as errors: Wrap Error!! cause: Oops!!

======

wrap error as xerrors: Wrap Error!! cause:
    main.wrapXerrors
        /path/to/main.go:140
  - Oops!!:
    main.causeErrorUseXerrors
        /path/to/main.go:132

xerrors.Errorfの方は、%wを使ってネストさせた分のエラーも含めて表示されています。

このひとつ前の結果(単純にエラーを関数呼び出しで包んだ場合)ではコールスタックが増えなかったことから考えると、
他の言語でよく見るような関数呼び出しのコールスタックを再現したければ、各呼び出しごとにエラーをラップしていく
必要がありそうですね。

Unwrapしてみる

ネストされたエラーをUnwrapしてみましょう。

Package errors / func Unwrap

xerrors / func Unwrap

    40     fmt.Println("\nCase4:\n")
    41  
    42     fmt.Printf("unwrap as errors: %+v\n", errors.Unwrap(wrapErrors()))
    43  
    44     printSeparator()
    45  
    46     fmt.Printf("unwrap as xerrors: %+v\n", xerrors.Unwrap(wrapXerrors()))
    47  
    48     fmt.Println()
    49  
    50     fmt.Printf("unwrap as xerrors / errors.Unwrap: %+v\n", errors.Unwrap(wrapXerrors()))

エラーの作成に使っている関数は、先ほどと同じですね。

結果。

Case4:

unwrap as errors: Oops!!

======

unwrap as xerrors: Oops!!:
    main.causeErrorUseXerrors
        /path/to/main.go:132

unwrap as xerrors / errors.Unwrap: Oops!!:
    main.causeErrorUseXerrors
        /path/to/main.go:132

xerrorsの方は、Unwrapしても呼び出し元のフレームが表示されます。また、xerrorsで作成したエラーをerrorsの方でUnwrapすることも
できますね。

xerrors.Opaqueを使ってみる

xerrors.Opaqueを使ってみましょう。

xerrors / func Opaque

    52     fmt.Println("\nCase5:\n")
    53  
    54     fmt.Printf("wrap error as errors: %+v\n", wrapErrors())
    55  
    56     printSeparator()
    57  
    58     fmt.Printf("wrap error as xerrors with opaque: %+v\n", xerrors.Opaque(wrapXerrors()))

こちらを使うと、xerrorsを使った場合でも呼び出し元のフレームが出なくなります。

Case5:

wrap error as errors: Wrap Error!! cause: Oops!!

======

wrap error as xerrors with opaque: Wrap Error!! cause: Oops!!

つまり、errorsを使った時と同じになりましたね。ネストされたエラーは保持しているようです。

およそこんなところでしょうか。だいたい雰囲気はわかった気がします。

呼び出し元フレームの取得方法

ところで、xerrorsはどうやって呼び出し元のフレームを取得しているんでしょうか。

こちらのようです。

https://github.com/golang/xerrors/blob/5ec99f83aff198f5fbd629d6c8d8eb38a04218ca/frame.go#L24

runtimeパッケージを使っているようですね。

runtime - The Go Programming Language

また、他の言語でのコールスタックのようにならないのは、文字列に変換している時に1行分しか残していないからみたいですね。

https://github.com/golang/xerrors/blob/5ec99f83aff198f5fbd629d6c8d8eb38a04218ca/frame.go#L48

https://github.com/golang/xerrors/blob/5ec99f83aff198f5fbd629d6c8d8eb38a04218ca/frame.go#L31-L41

xerrorsが使っているのはruntime.Callersですが、

https://github.com/golang/xerrors/blob/5ec99f83aff198f5fbd629d6c8d8eb38a04218ca/frame.go#L24

まずはruntime.Callerを使ってみましょう。

runtime / func Caller

    60     fmt.Println("\nCase6:\n")
    61  
    62     pc, file, line, ok := callRuntimeCaller0()
    63     fmt.Printf("%v, %s, %d, %t\n", pc, file, line, ok)
    64  
    65     printSeparator()
    66  
    67     pc, file, line, ok = callRuntimeCaller1()
    68     fmt.Printf("%v, %s, %d, %t\n", pc, file, line, ok)

それぞれ、runtime.Callerに0と1を渡しています。

   143 func callRuntimeCaller0() (uintptr, string, int, bool) {
   144     return runtime.Caller(0)
   145 }
   146  
   147 func callRuntimeCaller1() (uintptr, string, int, bool) {
   148     return runtime.Caller(1)
   149 }

実行結果。

Case6:

4856742, /path/to/main.go, 144, true

======

4857125, /path/to/main.go, 67, true

0の方は、まさにこの位置を表示しています。

   143 func callRuntimeCaller0() (uintptr, string, int, bool) {
   144     return runtime.Caller(0)
   145 }

対して1の方は、呼び出し元のコードの位置を表示しています。

    67      pc, file, line, ok = callRuntimeCaller1()

つまり、runtime.Callerに与える値は、どれだけ呼び出し元までのフレームをスキップするかを意味します。

次にruntime.Callersを使ってみます。

Packages runtime / func Callers

こちらの第1引数は、どれだけフレームをスキップするかです。そして、第2引数にフレームを保存します。

この結果とruntime.CallersFramesを組み合わせることで、スタックトレースを再現できます。

Package runtime / func CallersFrames

    72     pcs := callRuntimeCallers0()
    73     frames := runtime.CallersFrames(pcs)
    74  
    75     for {
    76         frame, more := frames.Next()
    77         fmt.Printf("  %s(%s:%d)\n", frame.Function, frame.File, frame.Line)
    78  
    79         if !more {
    80             break
    81         }
    82     }
    83  
    84     printSeparator()
    85  
    86     pcs = callRuntimeCallers1()
    87     frames = runtime.CallersFrames(pcs)
    88  
    89     for {
    90         frame, more := frames.Next()
    91         fmt.Printf("  %s(%s:%d)\n", frame.Function, frame.File, frame.Line)
    92  
    93         if !more {
    94             break
    95         }
    96     }

ただ、どのくらいの数のフレームを取得するかは、固定で決めないといけなさそうですけどね。

   151 func callRuntimeCallers0() []uintptr {
   152     var pcs [3]uintptr
   153     runtime.Callers(0, pcs[:])
   154     return pcs[:]
   155 }
   156  
   157 func callRuntimeCallers1() []uintptr {
   158     var pcs [3]uintptr
   159     runtime.Callers(1, pcs[:])
   160     return pcs[:]
   161 }

結果。

  runtime.Callers(/usr/lib/go-1.16/src/runtime/extern.go:229)
  main.callRuntimeCallers0(/path/to/main.go:153)
  main.main(/path/to/main.go:72)

======

  main.callRuntimeCallers1(/path/to/main.go:159)
  main.main(/path/to/main.go:86)
  runtime.main(/usr/lib/go-1.16/src/runtime/proc.go:225)

runtime.Callersの第1引数を0にすると、runtime.Callers自身が含まれますね…。どこまでスキップするかを指定するのは、
重要な様子です。

フレームを取得するまでに、さらに関数呼び出しを挟んでみましょう。

   100     pcs = wrapCallRuntimeCaller1()
   101     frames = runtime.CallersFrames(pcs)
   102  
   103     for {
   104         frame, more := frames.Next()
   105         fmt.Printf("  %s(%s:%d)\n", frame.Function, frame.File, frame.Line)
   106  
   107         if !more {
   108             break
   109         }
   110     }

こんな感じですね。

   157 func callRuntimeCallers1() []uintptr {
   158     var pcs [3]uintptr
   159     runtime.Callers(1, pcs[:])
   160     return pcs[:]
   161 }
   162  
   163 func wrapCallRuntimeCaller1() []uintptr {
   164     return callRuntimeCallers1()
   165 }

結果。それっぽくなっていますね。

  main.callRuntimeCallers1(/path/to/main.go:159)
  main.wrapCallRuntimeCaller1(/path/to/main.go:164)
  main.main(/path/to/main.go:100)

このあたりの雰囲気はわかった気がします。

とはいえ、xerrorsの方も保持しているフレームも3つ分だけのようですし、

https://github.com/golang/xerrors/blob/5ec99f83aff198f5fbd629d6c8d8eb38a04218ca/frame.go#L16

そもそもこういうスタックトレースのようなものに頼るような言語ではないのでしょうね。

このあたりの情報は、参考程度に覚えておこうかなと思います。

オマケ

最後に、行番号なしのソースコードを載せておきましょう。

main.go

package main

import (
    "errors"
    "fmt"
    "runtime"

    "golang.org/x/xerrors"
)

func main() {
    fmt.Println("Case1:\n")

    fmt.Printf("New error as errors: %v\n", errors.New("Oops!!"))
    fmt.Println()
    fmt.Printf("New error as errors: %+v\n", errors.New("Oops!!"))

    printSeparator()

    fmt.Printf("New error as xerrors: %v\n", xerrors.New("Oops!!"))
    fmt.Println()
    fmt.Printf("New error as xerrors: %+v\n", xerrors.New("Oops!!"))

    fmt.Println("\nCase2:\n")

    fmt.Printf("call error as errors: %+v\n", callErrors())

    printSeparator()

    fmt.Printf("call error as xerrors: %+v\n", callXerrors())

    fmt.Println("\nCase3:\n")

    fmt.Printf("wrap error as errors: %+v\n", wrapErrors())

    printSeparator()

    fmt.Printf("wrap error as xerrors: %+v\n", wrapXerrors())

    fmt.Println("\nCase4:\n")

    fmt.Printf("unwrap as errors: %+v\n", errors.Unwrap(wrapErrors()))

    printSeparator()

    fmt.Printf("unwrap as xerrors: %+v\n", xerrors.Unwrap(wrapXerrors()))

    fmt.Println()

    fmt.Printf("unwrap as xerrors / errors.Unwrap: %+v\n", errors.Unwrap(wrapXerrors()))

    fmt.Println("\nCase5:\n")

    fmt.Printf("wrap error as errors: %+v\n", wrapErrors())

    printSeparator()

    fmt.Printf("wrap error as xerrors with opaque: %+v\n", xerrors.Opaque(wrapXerrors()))

    fmt.Println("\nCase6:\n")

    pc, file, line, ok := callRuntimeCaller0()
    fmt.Printf("%v, %s, %d, %t\n", pc, file, line, ok)

    printSeparator()

    pc, file, line, ok = callRuntimeCaller1()
    fmt.Printf("%v, %s, %d, %t\n", pc, file, line, ok)

    printSeparator()

    pcs := callRuntimeCallers0()
    frames := runtime.CallersFrames(pcs)

    for {
        frame, more := frames.Next()
        fmt.Printf("  %s(%s:%d)\n", frame.Function, frame.File, frame.Line)

        if !more {
            break
        }
    }

    printSeparator()

    pcs = callRuntimeCallers1()
    frames = runtime.CallersFrames(pcs)

    for {
        frame, more := frames.Next()
        fmt.Printf("  %s(%s:%d)\n", frame.Function, frame.File, frame.Line)

        if !more {
            break
        }
    }

    printSeparator()

    pcs = wrapCallRuntimeCaller1()
    frames = runtime.CallersFrames(pcs)

    for {
        frame, more := frames.Next()
        fmt.Printf("  %s(%s:%d)\n", frame.Function, frame.File, frame.Line)

        if !more {
            break
        }
    }
}

func printSeparator() {
    fmt.Println()
    fmt.Println("======")
    fmt.Println()
}

func causeErrorUseErrors() error {
    return errors.New("Oops!!")
}

func callErrors() error {
    return causeErrorUseErrors()
}

func wrapErrors() error {
    return fmt.Errorf("Wrap Error!! cause: %w", causeErrorUseErrors())
}

func causeErrorUseXerrors() error {
    return xerrors.New("Oops!!")
}

func callXerrors() error {
    return causeErrorUseXerrors()
}

func wrapXerrors() error {
    return xerrors.Errorf("Wrap Error!! cause: %w", causeErrorUseXerrors())
}

func callRuntimeCaller0() (uintptr, string, int, bool) {
    return runtime.Caller(0)
}

func callRuntimeCaller1() (uintptr, string, int, bool) {
    return runtime.Caller(1)
}

func callRuntimeCallers0() []uintptr {
    var pcs [3]uintptr
    runtime.Callers(0, pcs[:])
    return pcs[:]
}

func callRuntimeCallers1() []uintptr {
    var pcs [3]uintptr
    runtime.Callers(1, pcs[:])
    return pcs[:]
}

func wrapCallRuntimeCaller1() []uintptr {
    return callRuntimeCallers1()
}