Skip to content

Commit 792d14a

Browse files
committed
avoid creating duplicate stack traces
This is done by using the new AddStack function instead of WithStack. There is also a StackError utility function exposed.
1 parent 816c908 commit 792d14a

File tree

3 files changed

+97
-11
lines changed

3 files changed

+97
-11
lines changed

errors.go

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -65,12 +65,7 @@
6565
// Retrieving the stack trace of an error or wrapper
6666
//
6767
// New, Errorf, Wrap, and Wrapf record a stack trace at the point they are
68-
// invoked. This information can be retrieved with the following interface.
69-
//
70-
// type stackTracer interface {
71-
// StackTrace() errors.StackTrace
72-
// }
73-
//
68+
// invoked. This information can be retrieved with the StackTracer interface that returns a StackTrace.
7469
// Where errors.StackTrace is defined as
7570
//
7671
// type StackTrace []Frame
@@ -79,15 +74,12 @@
7974
// the fmt.Formatter interface that can be used for printing information about
8075
// the stack trace of this error. For example:
8176
//
82-
// if err, ok := err.(stackTracer); ok {
83-
// for _, f := range err.StackTrace() {
77+
// if stacked := errors.GetStackTracer(err); stacked != nil {
78+
// for _, f := range stacked.StackTrace() {
8479
// fmt.Printf("%+s:%d", f)
8580
// }
8681
// }
8782
//
88-
// stackTracer interface is not exported by this package, but is considered a part
89-
// of stable public API.
90-
//
9183
// See the documentation for Frame.Format for more details.
9284
package errors
9385

@@ -145,12 +137,44 @@ func WithStack(err error) error {
145137
if err == nil {
146138
return nil
147139
}
140+
148141
return &withStack{
149142
err,
150143
callers(),
151144
}
152145
}
153146

147+
// AddStack is similar to WithStack.
148+
// However, it will first check to see if a stack trace already exists in the causer chain before creating another one.
149+
// It does this by using the StackError function.
150+
func AddStack(err error) error {
151+
if GetStackTracer(err) != nil {
152+
return err
153+
}
154+
return WithStack(err)
155+
}
156+
157+
// GetStackTracer will return the first error in the causer chain that has a StackTrace.
158+
// This function is used by AddStack to avoid creating redundant stack traces.
159+
//
160+
// You can also use the StackTracer interface on the returned error to get the stack trace.
161+
func GetStackTracer(err error) StackTracer {
162+
type causer interface {
163+
Cause() error
164+
}
165+
for err != nil {
166+
if stacked, ok := err.(StackTracer); ok {
167+
return stacked
168+
}
169+
cause, ok := err.(causer)
170+
if !ok {
171+
return nil
172+
}
173+
err = cause.Cause()
174+
}
175+
return nil
176+
}
177+
154178
type withStack struct {
155179
error
156180
*stack

errors_test.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,12 @@ func TestCause(t *testing.T) {
9696
}, {
9797
WithStack(io.EOF),
9898
io.EOF,
99+
}, {
100+
AddStack(nil),
101+
nil,
102+
}, {
103+
AddStack(io.EOF),
104+
io.EOF,
99105
}}
100106

101107
for i, tt := range tests {
@@ -154,6 +160,10 @@ func TestWithStackNil(t *testing.T) {
154160
if got != nil {
155161
t.Errorf("WithStack(nil): got %#v, expected nil", got)
156162
}
163+
got = AddStack(nil)
164+
if got != nil {
165+
t.Errorf("AddStack(nil): got %#v, expected nil", got)
166+
}
157167
}
158168

159169
func TestWithStack(t *testing.T) {
@@ -173,6 +183,50 @@ func TestWithStack(t *testing.T) {
173183
}
174184
}
175185

186+
func TestAddStack(t *testing.T) {
187+
tests := []struct {
188+
err error
189+
want string
190+
}{
191+
{io.EOF, "EOF"},
192+
{AddStack(io.EOF), "EOF"},
193+
}
194+
195+
for _, tt := range tests {
196+
got := AddStack(tt.err).Error()
197+
if got != tt.want {
198+
t.Errorf("AddStack(%v): got: %v, want %v", tt.err, got, tt.want)
199+
}
200+
}
201+
}
202+
203+
func TestGetStackTracer(t *testing.T) {
204+
orig := io.EOF
205+
if GetStackTracer(orig) != nil {
206+
t.Errorf("GetStackTracer: got: %v, want %v", GetStackTracer(orig), nil)
207+
}
208+
stacked := AddStack(orig)
209+
if GetStackTracer(stacked).(error) != stacked {
210+
t.Errorf("GetStackTracer(stacked): got: %v, want %v", GetStackTracer(stacked), stacked)
211+
}
212+
final := AddStack(stacked)
213+
if GetStackTracer(final).(error) != stacked {
214+
t.Errorf("GetStackTracer(final): got: %v, want %v", GetStackTracer(final), stacked)
215+
}
216+
}
217+
218+
func TestAddStackDedup(t *testing.T) {
219+
stacked := WithStack(io.EOF)
220+
err := AddStack(stacked)
221+
if err != stacked {
222+
t.Errorf("AddStack: got: %+v, want %+v", err, stacked)
223+
}
224+
err = WithStack(stacked)
225+
if err == stacked {
226+
t.Errorf("WithStack: got: %v, don't want %v", err, stacked)
227+
}
228+
}
229+
176230
func TestWithMessageNil(t *testing.T) {
177231
got := WithMessage(nil, "no error")
178232
if got != nil {
@@ -215,6 +269,8 @@ func TestErrorEquality(t *testing.T) {
215269
WithMessage(io.EOF, "whoops"),
216270
WithStack(io.EOF),
217271
WithStack(nil),
272+
AddStack(io.EOF),
273+
AddStack(nil),
218274
}
219275

220276
for i := range vals {

stack.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ import (
88
"strings"
99
)
1010

11+
// StackTracer retrieves the StackTrace
12+
// Generally you would want to use the GetStackTracer function to do that.
13+
type StackTracer interface {
14+
StackTrace() StackTrace
15+
}
16+
1117
// Frame represents a program counter inside a stack frame.
1218
type Frame uintptr
1319

0 commit comments

Comments
 (0)