jitterx

package module
v0.1.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 28, 2026 License: MIT Imports: 10 Imported by: 0

README

jitterx

Go Reference CI Go Report Card

bakhod1r.github.io/jitterx — with a simulation of what jitter actually does to a retry storm.

Jitter for retries, backoff, timeouts and scheduled work in Go. Standard library only, zero allocations on the hot path.

go get github.com/bakhod1r/jitterx

Why

Every client that retries on a fixed schedule retries at the same moment. The service that just fell over gets hit by the whole fleet at once, fails again, and the fleet lines up for another synchronised wave. Jitter breaks the alignment.

Spread periodic work

The case retry libraries do not cover: many processes running the same schedule, hitting the same origin on the same second.

t := jitterx.NewTicker(30*time.Second, nil) // +/- 10% by default
defer t.Stop()

for range t.C {
    refreshCache()
}

Ticker has the shape of time.Ticker — a receive-only C, Stop, Reset — and the same slow-receiver rule: one tick of room, extra ticks dropped rather than queued. Each interval is jittered independently, so it drifts against a fixed schedule; that is the point. It is the wrong tool if you need ticks anchored to absolute wall-clock times.

Same loop, context-aware, without the bookkeeping:

err := jitterx.Every(ctx, time.Minute, nil, func(ctx context.Context) error {
    return sendHeartbeat(ctx)
})

Every stops when fn returns an error or ctx ends.

For a one-off jittered sleep in a loop you already own:

if err := jitterx.Wait(ctx, time.Minute, jitterx.Proportional(0.1, nil)); err != nil {
    return err // ctx ended
}
Deadlines need one-sided jitter

Renewing a 60s lease at Proportional(0.2) pushes half the fleet to 60-72s — past the expiry it was protecting. Early only ever shortens:

renewIn := jitterx.Early(0.2, nil).Jitter(lease) // (48s, 60s]

Retry a call

b := jitterx.New(
    jitterx.WithBase(200*time.Millisecond),
    jitterx.WithMax(10*time.Second),
    jitterx.WithMaxRetries(4),
)

err := jitterx.Do(ctx, b, func(ctx context.Context) error {
    resp, err := doRequest(ctx)
    if err != nil {
        return err // transient: retry
    }
    if resp.StatusCode >= 400 && resp.StatusCode < 500 {
        return jitterx.Permanent(fmt.Errorf("status %d", resp.StatusCode))
    }
    return nil
})

Do stops on success, on a Permanent error, when the retry budget runs out, or when ctx ends. If the context ended first, the returned error joins ctx.Err() with the last error from fn, so errors.Is matches either.

Drive the delays yourself

b := jitterx.New(jitterx.WithBase(time.Second), jitterx.WithMax(time.Minute))

for {
    d := b.Next()
    if d == jitterx.Stop {
        break
    }
    time.Sleep(d)
}

Jitter one duration

// Spread a 30s tick by +/- 10% so instances do not line up.
next := jitterx.Proportional(0.1, nil).Jitter(30 * time.Second)

Strategies

Strategy Range Use for
Full(src) [0, d) Best herd-spreading. Can retry almost instantly. Default.
Equal(src) [d/2, d) Half the delay guaranteed, half random.
Decorrelated(base, src) [base, prev*3] Long outages; no fixed ceiling keeping clients in step.
Proportional(f, src) [d-d*f, d+d*f] Cron ticks, cache TTLs, heartbeats.
Early(f, src) (d-d*f, d] Delays protecting a deadline: lease renewal, token refresh, cache expiry. Never exceeds d.
None() d Disable jitter without branching. Tests.

A nil Source means the default process-wide source. Pass your own to make tests deterministic:

jitterx.Full(jitterx.SourceFunc(func() float64 { return 0.5 }))

Decorrelated ignores the duration handed to Jitter and drives itself from its own previous output. It carries state, so give each Backoff and each goroutine its own instance.

Retry a call that returns a value

user, err := jitterx.DoValue(ctx, b, func(ctx context.Context) (*User, error) {
    return fetchUser(ctx)
})

Let the server pick the delay

A 429 or 503 usually carries Retry-After. The server knows when it will be ready; the backoff does not.

return nil, jitterx.RetryAfter(after, errors.New("rate limited"))

That delay overrides the backoff for that one wait. It is honoured as given, so bound it with WithMaxRetryAfter if you do not fully trust the peer. The attempt still counts, and the wait still respects ctx.

Retry every HTTP call

Transport wraps a RoundTripper, so the retries happen below the call sites:

client := &http.Client{Transport: &jitterx.Transport{
    NewBackoff: func() *jitterx.Backoff {
        return jitterx.New(jitterx.WithMaxRetries(3), jitterx.WithMaxElapsed(20*time.Second))
    },
}}

NewBackoff is per request, not per transport, because a Backoff is not safe for concurrent use.

Defaults, all overridable via ShouldRetry:

  • Retries idempotent methods only — GET, HEAD, OPTIONS, TRACE, PUT, DELETE. A POST may already have had its effect before the error was written. If yours carries an idempotency key, opt in explicitly.
  • Retries on transport errors and on 429, 500, 502, 503, 504.
  • Honours Retry-After, in both the seconds and HTTP-date forms. Cap it with WithMaxRetryAfter.
  • Will not retry a request whose body cannot be rewound (GetBody == nil) — a second attempt would send an empty body.
  • When retries run out it returns the last response, not an error, so you can read the status and body.
  • Drains and closes responses it discards, so connections go back to the pool.

Options

Option Default Notes
WithBase(d) 100ms Delay before the first retry.
WithMax(d) 30s Caps every delay, jitter included.
WithMultiplier(f) 2.0 Geometric growth. Values < 1 ignored.
WithStrategy(s) Full(nil)
WithMaxRetries(n) unlimited n delays, so Do calls fn at most n+1 times.
WithMaxElapsed(d) unlimited Budget for the whole sequence. Stops rather than sleeping past it.
WithMaxRetryAfter(d) no cap Caps what a server can ask for via RetryAfter.
WithOnRetry(fn) none Called per retry with attempt, delay, error. Runs inline — logging or a metric only.

NewTicker and Every take a Strategy directly; nil means Proportional(DefaultTickerSpread, nil), i.e. +/- 10%. Avoid Full there — it can return near-zero and fire the ticker almost immediately.

Invalid values are ignored rather than returning an error: New never fails, and misconfiguration falls back to the documented default.

Concurrency

A Ticker is safe to Stop and Reset from any goroutine. A Backoff is not safe for concurrent use — give each retry loop its own. The default random source is safe for concurrent use; a custom Source only needs to be if you share it.

Stability

v0.x: the API may still change. It settles at v1.0.0, after which the usual Go compatibility promise applies.

License

MIT

Documentation

Overview

Package jitterx adds randomness to retries, backoff intervals, timeouts and scheduled work, so independent callers stop lining up on the same instant.

Three pieces, each usable on its own:

  • A Strategy randomises a single duration. Full, Equal, Proportional and Decorrelated cover the usual choices.

  • A Backoff turns a strategy into a sequence: geometric growth from a base, clamped to a max, jittered on the way out.

  • Do runs a function against a Backoff until it succeeds, is told to stop, or the context ends.

  • Wait is the primitive under both: a jittered, cancellable sleep.

  • Ticker and Every put the same jitter on periodic work — cache refreshes, heartbeats, lease renewals, reconcile loops — which is where a fleet on a shared schedule hurts most.

  • Transport wraps an net/http.RoundTripper so every client using it retries, with no change at the call sites.

Picking a strategy:

  • Full spreads a thundering herd best, but can retry almost instantly.
  • Equal keeps half the delay guaranteed and randomises the rest.
  • Decorrelated climbs and drops without a fixed schedule; good for long outages where a strict ceiling would keep every client synchronised.
  • Proportional keeps an interval roughly intact, for cron ticks, cache TTLs and heartbeats.
  • Early never exceeds the duration given, for delays protecting a deadline: lease renewals, token refreshes, cache expiry.

The package has no dependencies beyond the standard library. A Backoff is not safe for concurrent use; give each retry loop its own.

Index

Examples

Constants

View Source
const (
	DefaultBase       = 100 * time.Millisecond
	DefaultMax        = 30 * time.Second
	DefaultMultiplier = 2.0
)

Defaults applied by New when an option is missing or out of range.

View Source
const DefaultTickerSpread = 0.1

DefaultTickerSpread is the +/- fraction NewTicker jitters by when no strategy is given. Ten percent spreads a fleet without meaningfully moving the average interval.

View Source
const Stop = time.Duration(-1)

Stop is returned by Next once the configured attempt limit is spent.

Variables

This section is empty.

Functions

func DefaultShouldRetry

func DefaultShouldRetry(req *http.Request, resp *http.Response, err error) bool

DefaultShouldRetry retries idempotent requests that failed in transport or came back 429, 500, 502, 503 or 504.

Non-idempotent methods are never retried: a POST may already have had its effect before the error was written, and sending it again would apply that effect twice. If yours is safe to repeat — it carries an idempotency key, or the endpoint deduplicates — say so with your own Transport.ShouldRetry.

func Do

func Do(ctx context.Context, b *Backoff, fn func(context.Context) error) error

Do calls fn until it succeeds, returns a Permanent error, exhausts b, or ctx ends. It resets b on entry, so the same Backoff can be reused across calls.

On failure it returns the last error from fn; if ctx ended first, the returned error joins ctx.Err() with that last error, so errors.Is matches either.

Example

Retry an HTTP request, treating 4xx as permanent.

package main

import (
	"context"
	"errors"
	"fmt"
	"net/http"
	"time"

	"github.com/bakhod1r/jitterx"
)

func main() {
	b := jitterx.New(
		jitterx.WithBase(200*time.Millisecond),
		jitterx.WithMax(10*time.Second),
		jitterx.WithMaxRetries(4),
		jitterx.WithStrategy(jitterx.Equal(nil)),
	)

	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	var body int
	err := jitterx.Do(ctx, b, func(ctx context.Context) error {
		req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://example.com", nil)
		if err != nil {
			return jitterx.Permanent(err)
		}
		resp, err := http.DefaultClient.Do(req)
		if err != nil {
			return err // transient: retry
		}
		defer resp.Body.Close()

		if resp.StatusCode >= 400 && resp.StatusCode < 500 {
			return jitterx.Permanent(fmt.Errorf("status %d", resp.StatusCode))
		}
		if resp.StatusCode >= 500 {
			return fmt.Errorf("status %d", resp.StatusCode)
		}
		body = resp.StatusCode
		return nil
	})
	_ = body
	_ = errors.Is(err, context.DeadlineExceeded)
}

func DoValue

func DoValue[T any](ctx context.Context, b *Backoff, fn func(context.Context) (T, error)) (T, error)

DoValue is Do for a function that produces a value. On success it returns the value from the attempt that succeeded; on failure, the zero value and the same error Do would return.

Example

DoValue retries a call that produces a value, and honours a Retry-After header when the server sends one.

package main

import (
	"context"
	"errors"
	"log"
	"net/http"
	"time"

	"github.com/bakhod1r/jitterx"
)

func main() {
	b := jitterx.New(
		jitterx.WithBase(200*time.Millisecond),
		jitterx.WithMaxElapsed(30*time.Second),
		jitterx.WithMaxRetryAfter(5*time.Minute),
		jitterx.WithOnRetry(func(attempt int, d time.Duration, err error) {
			log.Printf("attempt %d failed (%v), retrying in %v", attempt, err, d)
		}),
	)

	user, err := jitterx.DoValue(context.Background(), b, func(ctx context.Context) (*User, error) {
		req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://example.com/me", nil)
		if err != nil {
			return nil, jitterx.Permanent(err)
		}
		resp, err := http.DefaultClient.Do(req)
		if err != nil {
			return nil, err
		}
		defer resp.Body.Close()

		if resp.StatusCode == http.StatusTooManyRequests {
			after, _ := time.ParseDuration(resp.Header.Get("Retry-After") + "s")
			return nil, jitterx.RetryAfter(after, errors.New("rate limited"))
		}
		return decodeUser(resp)
	})
	_, _ = user, err
}

type User struct{}

func decodeUser(*http.Response) (*User, error) { return &User{}, nil }

func Every

func Every(ctx context.Context, interval time.Duration, s Strategy, fn func(context.Context) error) error

Every calls fn on a jittered interval until fn returns an error or ctx ends, then returns that error or ctx.Err(). A nil strategy means Proportional with DefaultTickerSpread.

fn is not called immediately: the first call comes after the first interval.

Example

Every is the same loop without the ticker bookkeeping, and it stops on context cancellation.

package main

import (
	"context"
	"errors"
	"fmt"
	"time"

	"github.com/bakhod1r/jitterx"
)

func main() {
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	err := jitterx.Every(ctx, time.Minute, nil, sendHeartbeat)
	fmt.Println(errors.Is(err, context.Canceled))
}

func sendHeartbeat(context.Context) error { return nil }

func Permanent

func Permanent(err error) error

Permanent wraps err so Do stops immediately and returns the cause. Use it for failures no amount of waiting will fix: a 400, a validation error, a missing record. Permanent(nil) is nil.

func RetryAfter

func RetryAfter(d time.Duration, err error) error

RetryAfter wraps err with a delay the server asked for, overriding whatever the Backoff would have chosen for that one wait. Use it for a 429 or 503 carrying a Retry-After header: the server knows when it will be ready and the client does not.

The delay is honoured as given unless WithMaxRetryAfter caps it. The attempt still counts against WithMaxRetries, and the wait still respects the context. RetryAfter(d, nil) is nil.

func Wait

func Wait(ctx context.Context, d time.Duration, s Strategy) error

Wait sleeps for a jittered d, returning early with ctx.Err() if ctx ends first. A nil Strategy waits exactly d, so jitter is opt-in.

It is the primitive under Do and Every, exported because a hand-rolled loop usually wants the same thing: a sleep that respects cancellation.

Types

type Backoff

type Backoff struct {
	// contains filtered or unexported fields
}

Backoff produces a sequence of jittered delays: the raw delay grows geometrically from base, is clamped to max, and is then handed to the Strategy.

A Backoff is not safe for concurrent use. Give each retry loop its own.

func New

func New(opts ...Option) *Backoff

New builds a Backoff. Without options it uses DefaultBase, DefaultMax, DefaultMultiplier, full jitter and no attempt limit.

func (*Backoff) Attempt

func (b *Backoff) Attempt() int

Attempt reports how many delays have been handed out since the last Reset.

func (*Backoff) Next

func (b *Backoff) Next() time.Duration

Next advances the attempt counter and returns the next delay, or Stop once a limit is reached: the retry count from WithMaxRetries, or the time budget from WithMaxElapsed.

The budget is checked against the delay about to be handed out, so Next stops rather than returning a delay that would sleep past the budget.

Example

Drive the delays yourself instead of handing control to Do.

package main

import (
	"fmt"
	"time"

	"github.com/bakhod1r/jitterx"
)

func main() {
	b := jitterx.New(
		jitterx.WithBase(time.Second),
		jitterx.WithMax(time.Minute),
		jitterx.WithStrategy(jitterx.None()), // deterministic, for the output below
	)

	for {
		d := b.Next()
		if d == jitterx.Stop {
			break
		}
		fmt.Println(d)
		if b.Attempt() == 4 {
			break
		}
	}
}
Output:
1s
2s
4s
8s

func (*Backoff) Reset

func (b *Backoff) Reset()

Reset returns the Backoff, and any stateful Strategy it holds, to its initial state.

type Option

type Option func(*Backoff)

Option configures a Backoff. Invalid values are ignored rather than returning an error, so New never fails and misconfiguration degrades to the documented default.

func WithBase

func WithBase(d time.Duration) Option

WithBase sets the delay before the first retry. Values <= 0 are ignored.

func WithMax

func WithMax(d time.Duration) Option

WithMax caps every delay, jitter included. Values <= 0 are ignored.

func WithMaxElapsed

func WithMaxElapsed(d time.Duration) Option

WithMaxElapsed gives the whole retry sequence a time budget. Next returns Stop once the budget is spent, or once the delay it is about to hand out would sleep past it. Values <= 0 mean unlimited, which is the default.

The clock starts in New and restarts on Reset, which Do calls on entry.

func WithMaxRetries

func WithMaxRetries(n int) Option

WithMaxRetries limits the Backoff to n delays, after which Next returns Stop. Under Do that means fn is called at most n+1 times: once, then once per retry. Values <= 0 mean unlimited, which is the default.

func WithMaxRetryAfter

func WithMaxRetryAfter(d time.Duration) Option

WithMaxRetryAfter caps the delay a server can ask for through RetryAfter. Without it a server's value is honoured as given, which is correct but leaves how long you wait in someone else's hands. Values <= 0 mean no cap, which is the default.

func WithMultiplier

func WithMultiplier(f float64) Option

WithMultiplier sets the geometric growth factor. Values < 1 are ignored, since they would shrink the delay as attempts pile up.

func WithOnRetry

func WithOnRetry(fn func(attempt int, delay time.Duration, err error)) Option

WithOnRetry registers a hook called once per retry, before the wait, with the 1-based attempt number, the delay about to be slept, and the error that caused it. It is not called for the final failure, where nothing is retried.

The hook runs on the calling goroutine and blocks the retry, so keep it to logging or a metric.

func WithStrategy

func WithStrategy(s Strategy) Option

WithStrategy sets the jitter strategy. A nil strategy is ignored.

Stateful strategies (Decorrelated) must not be shared between Backoff instances or goroutines.

type Source

type Source interface {
	Float64() float64
}

Source supplies the randomness a Strategy draws on. Implementations must return a value in [0, 1). The default source is safe for concurrent use; a custom one only needs to be safe if the Backoff using it is shared.

type SourceFunc

type SourceFunc func() float64

SourceFunc adapts a plain function into a Source.

func (SourceFunc) Float64

func (f SourceFunc) Float64() float64

Float64 implements Source.

type Strategy

type Strategy interface {
	Jitter(d time.Duration) time.Duration
}

Strategy turns an intended delay into a randomised one. Every strategy returns a non-negative duration, and returns 0 for a non-positive input.

func Decorrelated

func Decorrelated(base time.Duration, src Source) Strategy

Decorrelated returns AWS' decorrelated jitter strategy, seeded at base.

Unlike the other strategies it ignores its argument: the next delay is drawn from [base, prev*3]. Pair it with a Backoff for the upper clamp — nothing in the strategy itself caps growth.

A nil Source means the default process-wide source.

func Early

func Early(f float64, src Source) Strategy

Early picks uniformly from (d-d*f, d]: the result never exceeds d. Use it wherever the duration is protecting a deadline rather than pacing a retry — a cache TTL that must not serve past its contract, a lease renewal that must fire before expiry, a token refresh that must beat the clock. Spreading those in both directions, as Proportional does, pushes half the fleet past the very deadline the delay exists to respect.

A factor <= 0 leaves d unchanged; a factor >= 1 can reach zero. A nil Source means the default process-wide source.

Example

Early keeps a jittered delay under the deadline it protects, so a lease is always renewed before it expires.

package main

import (
	"fmt"
	"time"

	"github.com/bakhod1r/jitterx"
)

func main() {
	lease := time.Minute
	renewIn := jitterx.Early(0.2, nil).Jitter(lease) // (48s, 60s]

	fmt.Println(renewIn <= lease)
}
Output:
true

func Equal

func Equal(src Source) Strategy

Equal picks uniformly from [d/2, d). Half the delay is guaranteed, so a retry never fires immediately, and half is randomised.

A nil Source means the default process-wide source.

func Full

func Full(src Source) Strategy

Full picks uniformly from [0, d). This is AWS' "full jitter": the best choice for spreading a thundering herd, at the cost of occasionally retrying almost immediately.

A nil Source means the default process-wide source.

func None

func None() Strategy

None returns the delay unchanged. Useful as a default in tests and for disabling jitter without branching at the call site.

func Proportional

func Proportional(f float64, src Source) Strategy

Proportional picks uniformly from [d-d*f, d+d*f]. Use it for scheduled work (cron ticks, cache TTLs, heartbeats) where the interval should stay roughly intact but callers must not line up. A factor above 1 is allowed; the result is still clamped at 0.

A nil Source means the default process-wide source.

Example

Spread a periodic task so many instances do not tick together.

package main

import (
	"fmt"
	"time"

	"github.com/bakhod1r/jitterx"
)

func main() {
	interval := 30 * time.Second
	spread := jitterx.Proportional(0.1, nil) // +/- 10%

	next := spread.Jitter(interval)
	fmt.Println(next >= 27*time.Second && next <= 33*time.Second)
}
Output:
true

type StrategyFunc

type StrategyFunc func(time.Duration) time.Duration

StrategyFunc adapts a plain function into a Strategy.

func (StrategyFunc) Jitter

func (f StrategyFunc) Jitter(d time.Duration) time.Duration

Jitter implements Strategy.

type Ticker

type Ticker struct {
	// C delivers the ticks.
	C <-chan time.Time
	// contains filtered or unexported fields
}

Ticker delivers ticks at a jittered interval. It is the drop-in shape of time.Ticker — a receive-only C, Stop and Reset — for periodic work that many processes run on the same schedule: cache refreshes, heartbeats, lease renewals, reconcile loops.

Each interval is jittered independently, so the ticker drifts relative to a fixed schedule; that is the point. If you need ticks anchored to absolute wall-clock times, this is the wrong tool.

Like time.Ticker, C has room for one tick and sends that would block are dropped, so a slow receiver cannot build a backlog. Stop releases the ticker's goroutine but does not close C.

func NewTicker

func NewTicker(interval time.Duration, s Strategy) *Ticker

NewTicker returns a Ticker that fires about every interval, jittered by s. A nil strategy means Proportional with DefaultTickerSpread.

It panics if interval is not positive, matching time.NewTicker.

Strategies that can return zero, such as Full, will make the ticker fire almost immediately every so often. That is usually not what a scheduler wants; Proportional or Equal are the fitting choices here.

Example

Refresh a cache on a jittered schedule, so many instances do not all stampede the origin at the same second.

package main

import (
	"time"

	"github.com/bakhod1r/jitterx"
)

func main() {
	t := jitterx.NewTicker(30*time.Second, nil) // +/- 10% by default
	defer t.Stop()

	for range t.C {
		refreshCache()
	}
}

func refreshCache() {}

func (*Ticker) Reset

func (t *Ticker) Reset(d time.Duration)

Reset changes the interval. It panics if d is not positive, and returns once the new interval is in effect. Reset on a stopped Ticker does nothing.

func (*Ticker) Stop

func (t *Ticker) Stop()

Stop halts the ticker and releases its goroutine. It does not close C, so a tick already buffered stays readable. Stop is idempotent and safe to call from any goroutine.

type Transport

type Transport struct {
	// Base sends the requests. Nil means [http.DefaultTransport].
	Base http.RoundTripper

	// NewBackoff builds a Backoff for one request. It is called per request
	// because a Backoff is not safe for concurrent use, and a transport is
	// shared by every goroutine using the client. Nil means New().
	NewBackoff func() *Backoff

	// ShouldRetry decides whether a result is worth another attempt. Exactly
	// one of resp and err is non-nil. Nil means [DefaultShouldRetry].
	ShouldRetry func(req *http.Request, resp *http.Response, err error) bool
}

Transport is an http.RoundTripper that retries failed requests with jittered backoff. Wrap it around another transport and every client using it retries, with no change at the call sites:

client := &http.Client{Transport: &jitterx.Transport{
    NewBackoff: func() *jitterx.Backoff {
        return jitterx.New(jitterx.WithMaxRetries(3))
    },
}}

When the retries run out, Transport returns the last response rather than an error: a caller needs the status and body to decide what to do. It returns an error only when the final attempt did, or when the context ended.

A response that will not be returned is drained and closed first, so the connection goes back to the pool instead of being dropped.

Example

Transport puts retries underneath an http.Client, so call sites do not change.

package main

import (
	"context"
	"fmt"
	"net/http"
	"time"

	"github.com/bakhod1r/jitterx"
)

func main() {
	client := &http.Client{
		Timeout: 30 * time.Second,
		Transport: &jitterx.Transport{
			NewBackoff: func() *jitterx.Backoff {
				return jitterx.New(
					jitterx.WithBase(200*time.Millisecond),
					jitterx.WithMaxRetries(3),
					jitterx.WithMaxRetryAfter(time.Minute),
				)
			},
		},
	}

	req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "https://example.com", nil)
	if err != nil {
		return
	}
	resp, err := client.Do(req)
	if err != nil {
		return
	}
	defer resp.Body.Close()

	// Retries are spent by now: this is the final status, whatever it is.
	fmt.Println(resp.StatusCode >= 200)
}

func (*Transport) RoundTrip

func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error)

RoundTrip implements http.RoundTripper.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL