@@ -12,6 +12,8 @@ package errgroup
12
12
import (
13
13
"context"
14
14
"fmt"
15
+ "runtime"
16
+ "runtime/debug"
15
17
"sync"
16
18
)
17
19
@@ -31,6 +33,10 @@ type Group struct {
31
33
32
34
errOnce sync.Once
33
35
err error
36
+
37
+ mu sync.Mutex
38
+ panicValue any // = PanicError | PanicValue; non-nil if some Group.Go goroutine panicked.
39
+ abnormal bool // some Group.Go goroutine terminated abnormally (panic or goexit).
34
40
}
35
41
36
42
func (g * Group ) done () {
@@ -50,13 +56,22 @@ func WithContext(ctx context.Context) (*Group, context.Context) {
50
56
return & Group {cancel : cancel }, ctx
51
57
}
52
58
53
- // Wait blocks until all function calls from the Go method have returned, then
54
- // returns the first non-nil error (if any) from them.
59
+ // Wait blocks until all function calls from the Go method have returned
60
+ // normally, then returns the first non-nil error (if any) from them.
61
+ //
62
+ // If any of the calls panics, Wait panics with a [PanicValue];
63
+ // and if any of them calls [runtime.Goexit], Wait calls runtime.Goexit.
55
64
func (g * Group ) Wait () error {
56
65
g .wg .Wait ()
57
66
if g .cancel != nil {
58
67
g .cancel (g .err )
59
68
}
69
+ if g .panicValue != nil {
70
+ panic (g .panicValue )
71
+ }
72
+ if g .abnormal {
73
+ runtime .Goexit ()
74
+ }
60
75
return g .err
61
76
}
62
77
@@ -65,18 +80,56 @@ func (g *Group) Wait() error {
65
80
// It blocks until the new goroutine can be added without the number of
66
81
// active goroutines in the group exceeding the configured limit.
67
82
//
68
- // The first call to return a non-nil error cancels the group's context, if the
69
- // group was created by calling WithContext. The error will be returned by Wait.
83
+ // It blocks until the new goroutine can be added without the number of
84
+ // goroutines in the group exceeding the configured limit.
85
+ //
86
+ // The first goroutine in the group that returns a non-nil error, panics, or
87
+ // invokes [runtime.Goexit] will cancel the associated Context, if any.
70
88
func (g * Group ) Go (f func () error ) {
71
89
if g .sem != nil {
72
90
g .sem <- token {}
73
91
}
74
92
93
+ g .add (f )
94
+ }
95
+
96
+ func (g * Group ) add (f func () error ) {
75
97
g .wg .Add (1 )
76
98
go func () {
77
99
defer g .done ()
100
+ normalReturn := false
101
+ defer func () {
102
+ if normalReturn {
103
+ return
104
+ }
105
+ v := recover ()
106
+ g .mu .Lock ()
107
+ defer g .mu .Unlock ()
108
+ if ! g .abnormal {
109
+ if g .cancel != nil {
110
+ g .cancel (g .err )
111
+ }
112
+ g .abnormal = true
113
+ }
114
+ if v != nil && g .panicValue == nil {
115
+ switch v := v .(type ) {
116
+ case error :
117
+ g .panicValue = PanicError {
118
+ Recovered : v ,
119
+ Stack : debug .Stack (),
120
+ }
121
+ default :
122
+ g .panicValue = PanicValue {
123
+ Recovered : v ,
124
+ Stack : debug .Stack (),
125
+ }
126
+ }
127
+ }
128
+ }()
78
129
79
- if err := f (); err != nil {
130
+ err := f ()
131
+ normalReturn = true
132
+ if err != nil {
80
133
g .errOnce .Do (func () {
81
134
g .err = err
82
135
if g .cancel != nil {
@@ -101,19 +154,7 @@ func (g *Group) TryGo(f func() error) bool {
101
154
}
102
155
}
103
156
104
- g .wg .Add (1 )
105
- go func () {
106
- defer g .done ()
107
-
108
- if err := f (); err != nil {
109
- g .errOnce .Do (func () {
110
- g .err = err
111
- if g .cancel != nil {
112
- g .cancel (g .err )
113
- }
114
- })
115
- }
116
- }()
157
+ g .add (f )
117
158
return true
118
159
}
119
160
@@ -135,3 +176,33 @@ func (g *Group) SetLimit(n int) {
135
176
}
136
177
g .sem = make (chan token , n )
137
178
}
179
+
180
+ // PanicError wraps an error recovered from an unhandled panic
181
+ // when calling a function passed to Go or TryGo.
182
+ type PanicError struct {
183
+ Recovered error
184
+ Stack []byte // result of call to [debug.Stack]
185
+ }
186
+
187
+ func (p PanicError ) Error () string {
188
+ // A Go Error method conventionally does not include a stack dump, so omit it
189
+ // here. (Callers who care can extract it from the Stack field.)
190
+ return fmt .Sprintf ("recovered from errgroup.Group: %v" , p .Recovered )
191
+ }
192
+
193
+ func (p PanicError ) Unwrap () error { return p .Recovered }
194
+
195
+ // PanicValue wraps a value that does not implement the error interface,
196
+ // recovered from an unhandled panic when calling a function passed to Go or
197
+ // TryGo.
198
+ type PanicValue struct {
199
+ Recovered any
200
+ Stack []byte // result of call to [debug.Stack]
201
+ }
202
+
203
+ func (p PanicValue ) String () string {
204
+ if len (p .Stack ) > 0 {
205
+ return fmt .Sprintf ("recovered from errgroup.Group: %v\n %s" , p .Recovered , p .Stack )
206
+ }
207
+ return fmt .Sprintf ("recovered from errgroup.Group: %v" , p .Recovered )
208
+ }
0 commit comments