これは、なにをしたくて書いたもの?
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.Newfmt.Errorf-xerrors.Errorferrors.Is-xerrors.Iserrors.As-xerrors.Aserrors.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() }