Skip to content

Getting started

gollback runs Go functions concurrently and hands back what they return. It has three functions:

  • Race runs them all and returns the first result without an error, cancelling the rest.
  • All runs them all and returns every result and every error, in order.
  • Retry calls one until it succeeds, runs out of attempts, or its context is done.

gollback needs Go 1.26 or newer.

Terminal window
go get github.com/vardius/gollback/v2

This program asks several mirrors for the same file and prints whichever answers first. Each function makes one request, with the context Race gives it, so the slower requests are cancelled as soon as one succeeds:

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)
}
  1. From a clone of the repository, race two URLs:

    Terminal window
    go run ./examples/race 'https://go.dev/VERSION?m=text' 'https://golang.google.cn/VERSION?m=text'

    Both serve the same file, the latest Go version; it prints whichever answers first.

  2. Give it a URL that fails and one that works. The failure doesn’t end the race:

    Terminal window
    go run ./examples/race https://go.dev/no-such-page 'https://go.dev/VERSION?m=text'
  3. Give it only failing URLs. Race returns every error, one per line, in the order of the URLs:

    Terminal window
    go run ./examples/race https://go.dev/no-such-page https://go.dev/nor-this-one
    https://go.dev/no-such-page: 404 Not Found
    https://go.dev/nor-this-one: 404 Not Found
  • Each URL became an AsyncFunc[string]: a function that takes a context and returns a string and an error. The type parameter is the result’s type, so Race returns a string, not an interface{}.
  • Race started every function in its own goroutine. The first to return a nil error won.
  • When one won, Race cancelled the context the others had, so their requests stopped.
  • When every function failed, Race returned all their errors, joined with errors.Join.