Skip to content
v2See what changed

Concurrent calls, typed results

Run Go functions at once and get what they return, typed. The first to succeed wins a Race and cancels the rest, All waits for each one, and Retry calls one until it succeeds.

go get github.com/vardius/gollback/v2
main.go
// The first replica to answer wins,
// and the other request is cancelled.
user, err := gollback.Race(ctx,
func(ctx context.Context) (User, error) {
return primary.User(ctx, id)
},
func(ctx context.Context) (User, error) {
return replica.User(ctx, id)
},
)
  • 2.2×faster than v1 to race a function
  • 2.8×fewer allocations than v1 to race four functions
  • 0allocations to retry, however many attempts
  • 100%test coverage, on deterministic time with testing/synctest

Features

Typed results

An AsyncFunc[T] returns a T, so Race and Retry return one and All returns a []T. No interface{}, no type assertions.

Cancellation built in

Every function gets a context. The moment one wins a race, the others’ context is cancelled, so their requests stop.

Every error kept

When every racer fails, you get all their errors joined. All returns each function’s error in its place.

Lean

Retry allocates nothing, All runs the first function on your goroutine, and tests guard the allocation counts.

Deterministic tests

Tested on a fake clock with testing/synctest: timings are exact and nothing waits for real. 100% coverage.

Small API

Three functions and one type. Nothing to configure and no dependencies beyond the standard library.

Performance

v2 starts one goroutine per function and nothing else, keeps results in one place instead of passing them through channels, and never allocates to retry. Racing one function went from 762 ns to 348 ns. With 16 callers at once, the time is wall time per call across all of them, so it measures throughput.

Time per call to Race, lower is better
Show as a table
Time per call to Race: time and allocations, v1 against v2
Scenariov1v2
1 function762.3 ns13 allocs347.5 ns5 allocs
4 functions1.95 µs22 allocs1.35 µs8 allocs
16 functions5.63 µs58 allocs3.94 µs20 allocs
4 functions, all fail2.19 µs22 allocs1.08 µs11 allocs
4 functions, 16 callers at once445.6 ns22 allocs284.4 ns8 allocs

Apple M4 Max, Go 1.27.0, median of 5 runs.

Upgrading

The functions are the same. They are generic now, so the type assertions go, and a few edge cases changed: all errors when every racer fails, and no results beside errors.

v1
res, err := gollback.Race(ctx,
func(ctx context.Context) (interface{}, error) {
return primary.User(ctx, id)
},
func(ctx context.Context) (interface{}, error) {
return replica.User(ctx, id)
},
)
user := res.(User)
v2
user, err := gollback.Race(ctx,
func(ctx context.Context) (User, error) {
return primary.User(ctx, id)
},
func(ctx context.Context) (User, error) {
return replica.User(ctx, id)
},
)

Race your first requests in five minutes

Install the module, hand Race two functions, and get the faster answer. The guide walks through All and Retry too.