@@ -26,7 +26,12 @@ func ParSlice[V any](ctx context.Context, s []V, f ErrFunc[V]) error {
2626 errGroup .SetLimit (runtime .GOMAXPROCS (0 ))
2727
2828 for _ , v := range s {
29+ // Snapshot v now so the goroutine sees the intended value
30+ // even if the caller mutates the slice s later. This is a
31+ // shallow copy only, if V contains pointers, their contents can
32+ // still change.
2933 v := v
34+
3035 errGroup .Go (func () error {
3136 return f (ctx , v )
3237 })
@@ -43,39 +48,58 @@ func ParSlice[V any](ctx context.Context, s []V, f ErrFunc[V]) error {
4348func ParSliceErrCollect [V any ](ctx context.Context , s []V ,
4449 f ErrFunc [V ]) (map [int ]error , error ) {
4550
46- errGroup , ctx := errgroup .WithContext (ctx )
51+ errGroup , groupCtx := errgroup .WithContext (ctx )
4752 errGroup .SetLimit (runtime .GOMAXPROCS (0 ))
4853
49- var instanceErrorsMutex sync.Mutex
50- instanceErrors := make (map [int ]error , len (s ))
54+ var (
55+ instanceErrorsMu sync.Mutex
56+ instanceErrors = make (map [int ]error , len (s ))
57+ )
5158
5259 for idx := range s {
60+ // Snapshot s[idx] now so the goroutine sees the intended value
61+ // even if the caller mutates the slice later. This is a shallow
62+ // copy only, if V contains pointers, their contents can still
63+ // change.
64+ v := s [idx ]
65+
5366 errGroup .Go (func () error {
54- err := f (ctx , s [idx ])
67+ // If already canceled, skip work without signaling an
68+ // error.
69+ select {
70+ case <- groupCtx .Done ():
71+ return nil
72+ default :
73+ }
74+
75+ err := f (groupCtx , v )
5576 if err != nil {
56- instanceErrorsMutex .Lock ()
77+ instanceErrorsMu .Lock ()
5778 instanceErrors [idx ] = err
58- instanceErrorsMutex .Unlock ()
79+ instanceErrorsMu .Unlock ()
5980 }
6081
61- // Avoid returning an error here, as that would cancel
62- // the errGroup and terminate all slice element
63- // processing instances . Instead, collect the error and
64- // return it later .
82+ // Do not return an error here. If we did, errGroup
83+ // would cancel the context and stop all other element
84+ // processors . Instead, record the error locally
85+ // and return it after all goroutines finish .
6586 return nil
6687 })
6788 }
6889
69- // Now we will wait/block for all goroutines to finish.
70- //
71- // The goroutines that are executing in parallel should not return an
72- // error, but the Wait call may return an error if the context is
73- // canceled or timed out.
74- err := errGroup .Wait ()
75- if err != nil {
90+ // Wait for all goroutines to finish. In this design, goroutines do not
91+ // return errors, so Wait should normally return nil. We handle context
92+ // cancellation separately with an explicit ctx.Err() check.
93+ if err := errGroup .Wait (); err != nil {
7694 return nil , fmt .Errorf ("failed to wait on error group in " +
7795 "ParSliceErrorCollect: %w" , err )
7896 }
7997
98+ // If the caller's context was canceled or timed out, surface that.
99+ // Return whatever per-item errors were collected before cancellation.
100+ if err := ctx .Err (); err != nil {
101+ return instanceErrors , err
102+ }
103+
80104 return instanceErrors , nil
81105}
0 commit comments