Skip to content

Race

func Race[T any](ctx context.Context, fns ...AsyncFunc[T]) (T, error)

Race starts every function in its own goroutine and returns the first result that comes with a nil error. Use it when several sources can answer the same question and you want the fastest good answer: replicas of a database, mirrors of a file, regions of a service.

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) },
)

The first function to return a nil error wins. A function that fails doesn’t end the race: the others keep going, and the first of them to succeed still wins.

Race returns as soon as there is a winner. It doesn’t wait for the others.

Every function gets a context derived from the one you pass. When a function wins, Race cancels that context, so the others’ requests, queries and timers stop. A function that respects its context returns early with context.Canceled; Race has already returned, and that result is dropped.

If no function succeeds, Race returns the zero value and all of their errors, joined with errors.Join in the order of the functions, not the order they failed in. Look for a particular one with errors.Is or errors.As:

_, err := gollback.Race(ctx, fromCache, fromDatabase)
if errors.Is(err, sql.ErrNoRows) {
// at least one source says there is no such row
}

With no functions at all, Race returns ErrNoCallbacks.

If the context you passed is cancelled, or its deadline passes, before any function succeeds, Race returns at once with that context’s error, joined with the errors of the functions that had failed by then, and the functions see their context cancelled too. errors.Is(err, context.DeadlineExceeded) tells you the deadline passed. If the context is done before you call Race, no function is called. Give a race a deadline the same way you give one to a request:

ctx, cancel := context.WithTimeout(ctx, 500*time.Millisecond)
defer cancel()
user, err := gollback.Race(ctx, fromPrimary, fromReplica)
examples/race/main.go
// Command race fetches the same file from several mirrors and prints whichever answers first.
//
// go run ./examples/race 'https://go.dev/VERSION?m=text' 'https://golang.google.cn/VERSION?m=text'
package main
import (
"context"
"fmt"
"io"
"log"
"net/http"
"os"
"time"
"github.com/vardius/gollback/v2"
)
// fetchFirst asks every mirror at once. The first good answer wins and the other requests are cancelled.
func fetchFirst(ctx context.Context, client *http.Client, urls ...string) (string, error) {
fetches := make([]gollback.AsyncFunc[string], len(urls))
for i, url := range urls {
fetches[i] = func(ctx context.Context) (string, error) {
return get(ctx, client, url)
}
}
return gollback.Race(ctx, fetches...)
}
func get(ctx context.Context, client *http.Client, url string) (string, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return "", err
}
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("%s: %s", url, resp.Status)
}
body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
if err != nil {
return "", fmt.Errorf("%s: %w", url, err)
}
return string(body), nil
}
func main() {
log.SetFlags(0)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
body, err := fetchFirst(ctx, http.DefaultClient, os.Args[1:]...)
if err != nil {
log.Fatal(err)
}
fmt.Print(body)
}