goretry provides a clean fluent API for retrying operations in Go. No external dependencies. Race-free. Generics-native.
| Feature | Detail | |
|---|---|---|
| π― | Context first | ctx.Done() is checked between every attempt β no blocking sleeps |
| π | Fluent API | retry.New().Attempts(5).Backoff(ExponentialJitter(200ms)).Do(ctx, fn) |
| 𧬠| Generics | DoValue[T] returns typed values β no closures outside the retry loop |
| π | Permanent errors | retry.Permanent(err) stops the loop before wasting more attempts |
| π | Conditional retry | RetryIf(func(error) bool) β only retry errors you want retried |
| β±οΈ | MaxDelay | Cap any backoff strategy at a maximum wait duration |
| π | 4 backoff strategies | Constant, Linear, Exponential, ExponentialJitter β or bring your own |
| πͺ | OnRetry hook | Plug in logging or metrics per attempt |
| β‘ | Zero allocations | ~2.6 ns/op, 0 allocs on the success path |
| πͺΆ | Zero dependencies | Pure stdlib. Ships in <200 LOC |
go get github.com/chmenegatti/go-retryRequires Go 1.18+
import retry "github.com/chmenegatti/go-retry"
err := retry.New().
Attempts(5).
Backoff(retry.ExponentialJitter(100 * time.Millisecond)).
Do(ctx, func() error {
return callUnstableService()
})user, err := retry.DoValue(ctx,
retry.New().Attempts(3),
func() (*User, error) {
return db.FindUser(id)
},
)err := retry.New().Attempts(5).Do(ctx, func() error {
resp, err := http.Get(url)
if err != nil {
return err // transient β will retry
}
if resp.StatusCode == 401 {
return retry.Permanent(ErrUnauthorized) // fatal β stop now
}
return nil
})
// Original error is always preserved
errors.Is(err, ErrUnauthorized) // trueerr := retry.New().
Attempts(5).
RetryIf(func(err error) bool {
// Only retry network timeouts, not application errors
var netErr net.Error
return errors.As(err, &netErr) && netErr.Timeout()
}).
Do(ctx, fn)retry.New().
Attempts(10).
Backoff(retry.Exponential(100 * time.Millisecond)).
MaxDelay(5 * time.Second). // never wait longer than 5s
Do(ctx, fn)retry.New().
Attempts(5).
OnRetry(func(attempt int, err error) {
log.Printf("attempt %d failed: %v", attempt, err)
metrics.Inc("retry.attempt")
}).
Do(ctx, fn)π HTTP request with server-error retry
body, err := retry.DoValue(ctx,
retry.New().
Attempts(4).
Backoff(retry.ExponentialJitter(500*time.Millisecond)).
MaxDelay(10*time.Second).
RetryIf(func(err error) bool {
var apiErr *APIError
if errors.As(err, &apiErr) {
return apiErr.Code >= 500 // only retry server errors
}
return true
}),
func() ([]byte, error) {
resp, err := http.Get("https://api.example.com/data")
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode == 401 {
return nil, retry.Permanent(ErrUnauthorized)
}
if resp.StatusCode >= 400 {
return nil, &APIError{Code: resp.StatusCode}
}
return io.ReadAll(resp.Body)
},
)ποΈ Database operation
err := retry.New().
Attempts(3).
Backoff(retry.Constant(500 * time.Millisecond)).
RetryIf(func(err error) bool {
return isDeadlock(err) // only retry transient DB conflicts
}).
Do(ctx, func() error {
return db.ExecContext(ctx, "UPDATE orders SET status=? WHERE id=?", "shipped", id)
})β‘ gRPC transient failure
resp, err := retry.DoValue(ctx,
retry.New().
Attempts(5).
Backoff(retry.ExponentialJitter(100*time.Millisecond)).
RetryIf(func(err error) bool {
code := status.Code(err)
return code == codes.Unavailable || code == codes.DeadlineExceeded
}),
func() (*pb.Response, error) {
return client.Call(ctx, req)
},
)type Backoff func(attempt int) time.Duration // bring your own!| Strategy | Formula | Use case |
|---|---|---|
Constant(d) |
d |
Simple polling, queue consumers |
Linear(base) |
attempt Γ base |
Gradual ramp-up |
Exponential(base) |
base Γ 2^(attempt-1) |
Standard retry without jitter |
ExponentialJitter(base) |
[d/2, d] random |
Recommended β avoids thundering herd |
| Method | Default | Description |
|---|---|---|
New() |
β | 3 attempts, 100ms constant backoff |
.Attempts(n) |
3 |
Total call count (including first) |
.Backoff(b) |
Constant(100ms) |
Delay strategy between attempts |
.MaxDelay(d) |
none | Hard cap applied on top of any backoff |
.RetryIf(fn) |
retry all | Predicate β return false to stop retrying |
.OnRetry(fn) |
none | Hook before each sleep (not on final failure) |
.Do(ctx, fn) |
β | Execute and return error |
DoValue[T](ctx, r, fn) |
β | Execute and return (T, error) |
Permanent(err) |
β | Wrap error to abort retry loop immediately |
IsPermanent(err) |
β | Check if any error in the chain is permanent |
flowchart TD
Start(["Do / DoValue"]) --> Exec["Execute fn()"]
Exec --> Ok{"err == nil?"}
Ok -->|"Yes"| ReturnNil(["return nil"])
Ok -->|"No"| Perm{"Permanent(err)?"}
Perm -->|"Yes"| ReturnPerm(["return err immediately"])
Perm -->|"No"| Cond{"RetryIf(err)?"}
Cond -->|"false"| ReturnCond(["return err immediately"])
Cond -->|"true"| Last{"Last attempt?"}
Last -->|"Yes"| ReturnErr(["return err"])
Last -->|"No"| Hook["OnRetry hook (if set)"]
Hook --> Sleep["Wait: min(backoff(n), MaxDelay)"]
Sleep -->|"Timer fired"| Exec
Sleep -->|"ctx.Done()"| ReturnCtx(["return ctx.Err()"])
| Library | API Style | Generics | Permanent | RetryIf | MaxDelay | Dependencies |
|---|---|---|---|---|---|---|
| goretry (this) | Fluent/chain | β | β | β | β | 0 |
cenkalti/backoff |
Functional | β | β | β | β | 1 |
avast/retry-go |
Functional opts | β | β | β | β | 0 |
- Simplicity over features: every method on
Retryis essential and independently useful. - No global state: calling
New()always returns a fresh independent instance. - Context always wins: the library never blocks on a timer longer than necessary.
- Errors are values:
Permanentis just an error wrapper β no special types to import. - Zero allocations on the hot path: successful calls don't allocate.
go test -race -cover ./...
# ok github.com/chmenegatti/go-retry coverage: 98.4% of statements
go test -bench=. -benchmem ./...
# BenchmarkRetrySuccess-12 443903150 2.626 ns/op 0 B/op 0 allocs/op
# BenchmarkRetryFailure-12 5 201000864 ns/op 502 B/op 6 allocs/opβ 0 allocations on the success path β the library is invisible when not needed.
- Fork and create a branch:
git checkout -b feat/my-feature - Run tests:
go test -race ./... - Commit:
git commit -am 'feat: ...' - Open a Pull Request
MIT β see LICENSE.
