github.com/Aoi-hosizora/ahlib@v1.5.1-0.20230404072829-241b93cf91c7/xerror/xerror.go (about) 1 package xerror 2 3 import ( 4 "context" 5 "errors" 6 "strings" 7 "sync" 8 ) 9 10 // ========== 11 // interfaces 12 // ========== 13 14 // Wrapper is an interface used to identify errors which has Unwrap method, can be used in errors.Unwrap function. 15 type Wrapper interface { 16 Unwrap() error 17 } 18 19 // Matcher is an interface used to identify errors which has Is method, can be used in errors.Is function. 20 type Matcher interface { 21 Is(error) bool 22 } 23 24 // Assigner is an interface used to identify errors which has As method, can be used in errors.As function. 25 type Assigner interface { 26 As(interface{}) bool 27 } 28 29 // =========== 30 // multi error 31 // =========== 32 33 // MultiError is an interface representing error groups, types implement this interface can be returned by xerror.Combine or 34 // several methods in github.com/uber-go/multierr package. 35 type MultiError interface { 36 Errors() []error 37 } 38 39 // multiError is an unexported error type implements MultiError interface, can be returned by xerror.Combine. 40 type multiError struct { 41 errs []error 42 } 43 44 var ( 45 _ MultiError = (*multiError)(nil) 46 ) 47 48 // Errors implements MultiError interface. 49 func (m *multiError) Errors() []error { 50 return m.errs // items are all non-nillable, if used in a safe manner 51 } 52 53 // Is implements Matcher interface. 54 func (m *multiError) Is(target error) bool { 55 for _, err := range m.errs { 56 if errors.Is(err, target) { 57 return true 58 } 59 } 60 return false 61 } 62 63 // As implements Assigner interface. 64 func (m *multiError) As(target interface{}) bool { 65 for _, err := range m.errs { 66 if errors.As(err, target) { 67 return true 68 } 69 } 70 return false 71 } 72 73 // Error implements error interface. 74 func (m *multiError) Error() string { 75 switch len(m.errs) { 76 case 0: 77 return "" 78 case 1: 79 return m.errs[0].Error() // non-nillable 80 } 81 sb := strings.Builder{} 82 for _, err := range m.errs { 83 if sb.Len() > 0 { 84 sb.WriteString("; ") 85 } 86 sb.WriteString(err.Error()) // non-nillable 87 } 88 return sb.String() 89 } 90 91 // Combine combines given errors to a single error, there are some situations: 92 // 1. If pass empty errors, or all errors passed are nil, it will return a nil error. 93 // 2. If pass a single non-nil error, it will return this single error directly. 94 // 3. If more than one error passed are non-nil, it returns a MultiError containing all these non-nil errors. 95 // 4. If some errors are MultiError, the internal errors contained will be flatted. 96 func Combine(errs ...error) error { 97 switch len(errs) { 98 case 0: 99 return nil 100 case 1: 101 return errs[0] // maybe nil 102 } 103 notnil := make([]error, 0) 104 for _, err := range errs { 105 if err == nil { 106 continue 107 } 108 if me, ok := err.(MultiError); ok { 109 notnil = append(notnil, me.Errors()...) 110 } else { 111 notnil = append(notnil, err) 112 } 113 } 114 switch len(notnil) { 115 case 0: 116 return nil 117 case 1: 118 return notnil[0] // single error (non-nil) 119 default: 120 return &multiError{errs: notnil} // multiple errors (all non-nil) 121 } 122 } 123 124 // Separate separates given error to multiple errors that given error is composed of (that is MultiError). If given error is 125 // nil, a nil slice is returned. 126 func Separate(err error) []error { 127 if err == nil { 128 return nil 129 } 130 me, ok := err.(MultiError) 131 if !ok { 132 return []error{err} 133 } 134 errs := me.Errors() 135 out := make([]error, len(errs)) 136 copy(out, errs) 137 return out 138 } 139 140 // =========== 141 // error group 142 // =========== 143 144 // ErrorGroup is a sync.WaitGroup wrapper that can used to synchronization, error propagation, and context cancellation for 145 // groups of goroutines, refers to https://pkg.go.dev/golang.org/x/sync/errgroup for more details. 146 // 147 // A zero ErrorGroup is also valid, which will create a cancelable context automatically for context cancellation when error. 148 type ErrorGroup struct { 149 ctx context.Context 150 cancel context.CancelFunc 151 152 wg sync.WaitGroup 153 err error 154 errMutex sync.RWMutex 155 errOnce sync.Once 156 157 mu sync.RWMutex 158 goExecutor func(f func()) 159 } 160 161 // NewErrorGroup returns a new ErrorGroup with cancelable context derived from given context, and the default goroutine executor. 162 func NewErrorGroup(ctx context.Context) *ErrorGroup { 163 ctx, cancel := context.WithCancel(ctx) 164 return &ErrorGroup{ctx: ctx, cancel: cancel, goExecutor: defaultExecutor} 165 } 166 167 // defaultExecutor is the default goroutine executor for ErrorGroup, including create goroutine by `go` keyword and panic 168 // recovery with no logging. 169 var defaultExecutor = func(f func()) { 170 go func() { 171 defer func() { 172 _ = recover() 173 }() 174 f() 175 }() 176 } 177 178 // SetGoExecutor sets goroutine executor, can be used to change the behavior of `go` keyword, you can use this executor to 179 // add recover behavior for goroutine. 180 // 181 // Example: 182 // // custom recover behavior 183 // eg := NewErrorGroup(context.Background()) 184 // eg.SetGoExecutor(func(f func()) { 185 // go func() { 186 // defer func() { 187 // if v := recover(); v != nil { 188 // log.Printf("Warning: Panic with %v", v) 189 // } 190 // }() 191 // f() 192 // }() 193 // }) 194 // 195 // // use xgopool goroutine pool 196 // eg := NewErrorGroup(context.Background()) 197 // gp := xgopool.New(int32(runtime.NumCPU() * 10)) 198 // gp.SetPanicHandler(func(_ context.Context, v interface{}) { 199 // log.Printf("Warning: Panic with %v", v) 200 // }) 201 // eg.SetGoExecutor(gp.Go) 202 func (eg *ErrorGroup) SetGoExecutor(executor func(f func())) { 203 if executor != nil { 204 eg.mu.Lock() 205 eg.goExecutor = executor 206 eg.mu.Unlock() 207 } 208 } 209 210 // Go calls given function in a new goroutine using specific executor. The first call to return a non-nil error cancels the 211 // group, its error will be returned by Wait. 212 // 213 // If using a zero ErrorGroup, ctx will be Background, otherwise it will be the context derived from given context passed 214 // to NewErrorGroup. 215 // 216 // Example: 217 // eg := NewErrorGroup(context.Background()) 218 // 219 // // in select statement 220 // eg.Go(func(ctx context.Context) error { 221 // select { 222 // case ...: 223 // case <-ctx.Done(): 224 // } 225 // return nil 226 // }) 227 // 228 // // in cancelable http requesting 229 // eg.Go(func(ctx context.Context) error { 230 // req, _ := http.NewRequestWithContext(ctx, "GET", "...", nil) 231 // // ... 232 // return nil 233 // }) 234 func (eg *ErrorGroup) Go(f func(ctx context.Context) error) { 235 if f == nil { 236 return 237 } 238 239 // get executor and context 240 eg.mu.RLock() 241 executor := eg.goExecutor 242 ctx := eg.ctx 243 eg.mu.RUnlock() 244 if executor == nil { 245 eg.mu.Lock() 246 if eg.goExecutor == nil { 247 eg.goExecutor = defaultExecutor 248 } 249 executor = eg.goExecutor 250 eg.mu.Unlock() 251 } 252 if ctx == nil { 253 eg.mu.Lock() 254 if eg.ctx == nil { 255 ctx_ := context.Background() 256 ctx_, cancel := context.WithCancel(ctx_) 257 eg.ctx = ctx_ 258 eg.cancel = cancel 259 } 260 ctx = eg.ctx 261 eg.mu.Unlock() 262 } 263 264 // execute with goroutine 265 eg.wg.Add(1) 266 executor(func() { 267 defer eg.wg.Done() 268 269 eg.errMutex.RLock() 270 can := eg.err == nil // check whether error is nil 271 eg.errMutex.RUnlock() 272 if !can { 273 return // err has already been recorded, reject to call function 274 } 275 276 err := f(ctx) // call given function 277 if err != nil { 278 eg.errOnce.Do(func() { 279 eg.errMutex.Lock() 280 eg.err = err // record the first error 281 eg.errMutex.Unlock() 282 if eg.cancel != nil { 283 eg.cancel() // also to cancel the context 284 } 285 }) 286 } 287 }) 288 } 289 290 // Reset resets states of ErrorGroup, including context and error. This method must be called if you want to reuse ErrorGroup 291 // after Wait, when non-nil error is returned by Wait. 292 func (eg *ErrorGroup) Reset(ctx context.Context) { 293 eg.mu.Lock() 294 defer eg.mu.Unlock() 295 296 ctx, cancel := context.WithCancel(ctx) 297 eg.ctx = ctx // reset 298 eg.cancel = cancel 299 eg.err = nil 300 eg.errOnce = sync.Once{} 301 } 302 303 // Wait blocks until all function calls from the Go method have returned, then returns the first non-nil error (if any) 304 // from them. Note that if any error is returned, the current ErrorGroup can not be used again before calling Reset. 305 func (eg *ErrorGroup) Wait() error { 306 eg.wg.Wait() 307 if eg.cancel != nil { 308 eg.cancel() 309 } 310 return eg.err 311 }