Skip to content
This repository was archived by the owner on Dec 1, 2021. It is now read-only.

Commit 560e9b0

Browse files
committed
Introduce Stacktrace and Frame
This PR is a continuation of a series aimed at exposing the stack trace information embedded in each error value. The secondary effect is to deprecated the `Fprintf` helper. Taking cues from from @ChrisHines' `stack` package this PR introduces a new interface `Stacktrace() []Frame` and a `Frame` type, similar in function to the `runtime.Frame` type (although lacking its iterator type). Each `Frame` implemnts `fmt.Formatter` allowing it to print itself. The older `Location` interface is still supported but also deprecated.
1 parent f45f2b7 commit 560e9b0

File tree

6 files changed

+324
-81
lines changed

6 files changed

+324
-81
lines changed

README.md

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,19 @@ if err != nil {
2424
`New`, `Errorf`, `Wrap`, and `Wrapf` record a stack trace at the point they are invoked.
2525
This information can be retrieved with the following interface.
2626
```go
27-
type Stack interface {
28-
Stack() []uintptr
27+
type Stacktrace interface {
28+
Stacktrace() []Frame
2929
}
3030
```
31+
The `Frame` type represents a call site in the stacktrace.
32+
`Frame` supports the `fmt.Formatter` interface that can be used for printing information about the stacktrace of this error. For example
33+
```
34+
if err, ok := err.(Stacktrace); ok {
35+
fmt.Printf("%+s:%d", err.Stacktrace())
36+
}
37+
```
38+
See [the documentation for `Frame.Format`](https://godoc.org/github.com/pkg/errors#Frame_Format) for more details.
39+
3140
## Retrieving the cause of an error
3241

3342
Using `errors.Wrap` constructs a stack of errors, adding context to the preceding error. Depending on the nature of the error it may be necessary to recurse the operation of errors.Wrap to retrieve the original error for inspection. Any error value which implements this interface can be inspected by `errors.Cause`.

errors.go

Lines changed: 24 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,6 @@
2121
// return errors.Wrap(err, "read failed")
2222
// }
2323
//
24-
// Retrieving the stack trace of an error or wrapper
25-
//
26-
// New, Errorf, Wrap, and Wrapf record a stack trace at the point they are
27-
// invoked. This information can be retrieved with the following interface.
28-
//
29-
// type Stack interface {
30-
// Stack() []uintptr
31-
// }
32-
//
3324
// Retrieving the cause of an error
3425
//
3526
// Using errors.Wrap constructs a stack of errors, adding context to the
@@ -51,25 +42,23 @@
5142
// default:
5243
// // unknown error
5344
// }
45+
//
46+
// Retrieving the stack trace of an error or wrapper
47+
//
48+
// New, Errorf, Wrap, and Wrapf record a stack trace at the point they are
49+
// invoked. This information can be retrieved with the following interface.
50+
//
51+
// type Stacktrace interface {
52+
// Stacktrace() []Frame
53+
// }
5454
package errors
5555

5656
import (
5757
"errors"
5858
"fmt"
5959
"io"
60-
"runtime"
61-
"strings"
6260
)
6361

64-
// stack represents a stack of program counters.
65-
type stack []uintptr
66-
67-
func (s *stack) Stack() []uintptr { return *s }
68-
69-
func (s *stack) Location() (string, int) {
70-
return location((*s)[0] - 1)
71-
}
72-
7362
// New returns an error that formats as the given text.
7463
func New(text string) error {
7564
return struct {
@@ -167,7 +156,11 @@ func Cause(err error) error {
167156
// Fprint prints the error to the supplied writer.
168157
// If the error implements the Causer interface described in Cause
169158
// Print will recurse into the error's cause.
170-
// If the error implements the inteface:
159+
// If the error implements one of the following interfaces:
160+
//
161+
// type Stacktrace interface {
162+
// Stacktrace() []Frame
163+
// }
171164
//
172165
// type Location interface {
173166
// Location() (file string, line int)
@@ -179,14 +172,23 @@ func Fprint(w io.Writer, err error) {
179172
type location interface {
180173
Location() (string, int)
181174
}
175+
type stacktrace interface {
176+
Stacktrace() []Frame
177+
}
182178
type message interface {
183179
Message() string
184180
}
185181

186182
for err != nil {
187-
if err, ok := err.(location); ok {
183+
switch err := err.(type) {
184+
case stacktrace:
185+
frame := err.Stacktrace()[0]
186+
fmt.Fprintf(w, "%+s:%d: ", frame, frame)
187+
case location:
188188
file, line := err.Location()
189189
fmt.Fprintf(w, "%s:%d: ", file, line)
190+
default:
191+
// de nada
190192
}
191193
switch err := err.(type) {
192194
case message:
@@ -202,58 +204,3 @@ func Fprint(w io.Writer, err error) {
202204
err = cause.Cause()
203205
}
204206
}
205-
206-
func callers() *stack {
207-
const depth = 32
208-
var pcs [depth]uintptr
209-
n := runtime.Callers(3, pcs[:])
210-
var st stack = pcs[0:n]
211-
return &st
212-
}
213-
214-
// location returns the source file and line matching pc.
215-
func location(pc uintptr) (string, int) {
216-
fn := runtime.FuncForPC(pc)
217-
if fn == nil {
218-
return "unknown", 0
219-
}
220-
221-
// Here we want to get the source file path relative to the compile time
222-
// GOPATH. As of Go 1.6.x there is no direct way to know the compiled
223-
// GOPATH at runtime, but we can infer the number of path segments in the
224-
// GOPATH. We note that fn.Name() returns the function name qualified by
225-
// the import path, which does not include the GOPATH. Thus we can trim
226-
// segments from the beginning of the file path until the number of path
227-
// separators remaining is one more than the number of path separators in
228-
// the function name. For example, given:
229-
//
230-
// GOPATH /home/user
231-
// file /home/user/src/pkg/sub/file.go
232-
// fn.Name() pkg/sub.Type.Method
233-
//
234-
// We want to produce:
235-
//
236-
// pkg/sub/file.go
237-
//
238-
// From this we can easily see that fn.Name() has one less path separator
239-
// than our desired output. We count separators from the end of the file
240-
// path until it finds two more than in the function name and then move
241-
// one character forward to preserve the initial path segment without a
242-
// leading separator.
243-
const sep = "/"
244-
goal := strings.Count(fn.Name(), sep) + 2
245-
file, line := fn.FileLine(pc)
246-
i := len(file)
247-
for n := 0; n < goal; n++ {
248-
i = strings.LastIndex(file[:i], sep)
249-
if i == -1 {
250-
// not enough separators found, set i so that the slice expression
251-
// below leaves file unmodified
252-
i = -len(sep)
253-
break
254-
}
255-
}
256-
// get back to 0 or trim the leading separator
257-
file = file[i+len(sep):]
258-
return file, line
259-
}

errors_test.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ func TestCause(t *testing.T) {
102102
}
103103
}
104104

105-
func TestFprint(t *testing.T) {
105+
func TestFprintError(t *testing.T) {
106106
x := New("error")
107107
tests := []struct {
108108
err error
@@ -234,7 +234,8 @@ func TestStack(t *testing.T) {
234234
}
235235
st := x.Stack()
236236
for i, want := range tt.want {
237-
file, line := location(st[i] - 1)
237+
frame := Frame(st[i])
238+
file, line := fmt.Sprintf("%+s", frame), frame.line()
238239
if file != want.file || line != want.line {
239240
t.Errorf("frame %d: expected %s:%d, got %s:%d", i, want.file, want.line, file, line)
240241
}

example_test.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,3 +69,19 @@ func ExampleErrorf() {
6969

7070
// Output: github.com/pkg/errors/example_test.go:67: whoops: foo
7171
}
72+
73+
func ExampleError_Stacktrace() {
74+
type Stacktrace interface {
75+
Stacktrace() []errors.Frame
76+
}
77+
78+
err, ok := errors.Cause(fn()).(Stacktrace)
79+
if !ok {
80+
panic("oops, err does not implement Stacktrace")
81+
}
82+
83+
st := err.Stacktrace()
84+
fmt.Printf("%+v", st[0:2]) // top two framces
85+
86+
// Output: [github.com/pkg/errors/example_test.go:33 github.com/pkg/errors/example_test.go:78]
87+
}

stack.go

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
package errors
2+
3+
import (
4+
"fmt"
5+
"io"
6+
"path"
7+
"runtime"
8+
"strings"
9+
)
10+
11+
// Frame repesents an activation record.
12+
type Frame uintptr
13+
14+
// pc returns the program counter for this frame;
15+
// multiple frames may have the same PC value.
16+
func (f Frame) pc() uintptr { return uintptr(f) - 1 }
17+
18+
// file returns the full path to the file that contains the
19+
// function for this Frame's pc.
20+
func (f Frame) file() string {
21+
fn := runtime.FuncForPC(f.pc())
22+
if fn == nil {
23+
return "unknown"
24+
}
25+
file, _ := fn.FileLine(f.pc())
26+
return file
27+
}
28+
29+
// line returns the line number of source code of the
30+
// function for this Frame's pc.
31+
func (f Frame) line() int {
32+
fn := runtime.FuncForPC(f.pc())
33+
if fn == nil {
34+
return 0
35+
}
36+
_, line := fn.FileLine(f.pc())
37+
return line
38+
}
39+
40+
// Format formats the frame according to the fmt.Formatter interface.
41+
//
42+
// %s source file
43+
// %d source line
44+
// %n function name
45+
// %v equivalent to %s:%d
46+
//
47+
// Format accepts flags that alter the printing of some verbs, as follows:
48+
//
49+
// %+s path of source file relative to the compile time GOPATH
50+
func (f Frame) Format(s fmt.State, verb rune) {
51+
switch verb {
52+
case 's':
53+
switch {
54+
case s.Flag('+'):
55+
pc := f.pc()
56+
fn := runtime.FuncForPC(pc)
57+
io.WriteString(s, trimGOPATH(fn, pc))
58+
default:
59+
io.WriteString(s, path.Base(f.file()))
60+
}
61+
case 'd':
62+
fmt.Fprintf(s, "%d", f.line())
63+
case 'n':
64+
name := runtime.FuncForPC(f.pc()).Name()
65+
i := strings.LastIndex(name, ".")
66+
io.WriteString(s, name[i+1:])
67+
case 'v':
68+
f.Format(s, 's')
69+
io.WriteString(s, ":")
70+
f.Format(s, 'd')
71+
}
72+
}
73+
74+
// stack represents a stack of program counters.
75+
type stack []uintptr
76+
77+
// Deprecated: use Stacktrace()
78+
func (s *stack) Stack() []uintptr { return *s }
79+
80+
// Deprecated: use Stacktrace()[0]
81+
func (s *stack) Location() (string, int) {
82+
frame := s.Stacktrace()[0]
83+
return fmt.Sprintf("%+s", frame), frame.line()
84+
}
85+
86+
func (s *stack) Stacktrace() []Frame {
87+
f := make([]Frame, len(*s))
88+
for i := 0; i < len(f); i++ {
89+
f[i] = Frame((*s)[i])
90+
}
91+
return f
92+
}
93+
94+
func callers() *stack {
95+
const depth = 32
96+
var pcs [depth]uintptr
97+
n := runtime.Callers(3, pcs[:])
98+
var st stack = pcs[0:n]
99+
return &st
100+
}
101+
102+
func trimGOPATH(fn *runtime.Func, pc uintptr) string {
103+
// Here we want to get the source file path relative to the compile time
104+
// GOPATH. As of Go 1.6.x there is no direct way to know the compiled
105+
// GOPATH at runtime, but we can infer the number of path segments in the
106+
// GOPATH. We note that fn.Name() returns the function name qualified by
107+
// the import path, which does not include the GOPATH. Thus we can trim
108+
// segments from the beginning of the file path until the number of path
109+
// separators remaining is one more than the number of path separators in
110+
// the function name. For example, given:
111+
//
112+
// GOPATH /home/user
113+
// file /home/user/src/pkg/sub/file.go
114+
// fn.Name() pkg/sub.Type.Method
115+
//
116+
// We want to produce:
117+
//
118+
// pkg/sub/file.go
119+
//
120+
// From this we can easily see that fn.Name() has one less path separator
121+
// than our desired output. We count separators from the end of the file
122+
// path until it finds two more than in the function name and then move
123+
// one character forward to preserve the initial path segment without a
124+
// leading separator.
125+
const sep = "/"
126+
goal := strings.Count(fn.Name(), sep) + 2
127+
file, _ := fn.FileLine(pc)
128+
i := len(file)
129+
for n := 0; n < goal; n++ {
130+
i = strings.LastIndex(file[:i], sep)
131+
if i == -1 {
132+
// not enough separators found, set i so that the slice expression
133+
// below leaves file unmodified
134+
i = -len(sep)
135+
break
136+
}
137+
}
138+
// get back to 0 or trim the leading separator
139+
file = file[i+len(sep):]
140+
return file
141+
}

0 commit comments

Comments
 (0)