これは、なにをしたくて書いたもの?
Goのエラー処理に関する情報を見ているとxerrorsというものがよく出てくるので、1度見ておこうかなと思いまして。
xerrorsパッケージ
xerrorsパッケージは、Goのエラーハンドリングのためのパッケージです。
このパッケージの提案内容は、こちら。
Proposal: Go 2 Error Inspection
これが標準パッケージ、errorsに取り込まれたのがGo 1.13のようです。
errors - The Go Programming Language
errorsパッケージについては、以前このエントリで試してみました。
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
以下の対比になるようです。
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
関数を使って、エラーを作成します。
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
を使います。
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してみましょう。
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
を使ってみましょう。
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
を使ってみましょう。
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() }