跳到主要内容

实现重试机制

问题

如何用 Go 实现一个健壮的重试机制?

答案

基础重试

func Retry(attempts int, sleep time.Duration, fn func() error) error {
for i := 0; i < attempts; i++ {
if err := fn(); err == nil {
return nil
} else if i < attempts-1 {
log.Printf("第 %d 次失败,%v 后重试: %v", i+1, sleep, err)
time.Sleep(sleep)
} else {
return fmt.Errorf("重试 %d 次后失败: %w", attempts, err)
}
}
return nil
}

指数退避 + 抖动(推荐)

type RetryConfig struct {
MaxAttempts int
InitDelay time.Duration
MaxDelay time.Duration
Multiplier float64 // 退避倍数(通常 2)
}

func RetryWithBackoff(ctx context.Context, cfg RetryConfig, fn func() error) error {
delay := cfg.InitDelay

for attempt := 0; attempt < cfg.MaxAttempts; attempt++ {
err := fn()
if err == nil {
return nil
}

// 最后一次不再等待
if attempt == cfg.MaxAttempts-1 {
return fmt.Errorf("重试 %d 次后失败: %w", cfg.MaxAttempts, err)
}

// 加抖动:避免多个客户端同时重试(惊群效应)
jitter := time.Duration(rand.Int63n(int64(delay) / 2))
sleepDuration := delay + jitter

if sleepDuration > cfg.MaxDelay {
sleepDuration = cfg.MaxDelay
}

log.Printf("第 %d 次失败,%v 后重试: %v", attempt+1, sleepDuration, err)

select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(sleepDuration):
}

delay = time.Duration(float64(delay) * cfg.Multiplier)
}
return nil
}

// 使用
func main() {
cfg := RetryConfig{
MaxAttempts: 5,
InitDelay: 100 * time.Millisecond,
MaxDelay: 10 * time.Second,
Multiplier: 2.0,
}

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

err := RetryWithBackoff(ctx, cfg, func() error {
return callExternalAPI()
})
}

可选重试(按错误类型判断)

// 只重试临时性错误
type RetryableError interface {
Retryable() bool
}

func RetryOnRetryable(ctx context.Context, attempts int, fn func() error) error {
for i := 0; i < attempts; i++ {
err := fn()
if err == nil {
return nil
}

// 检查是否可重试
var retryable RetryableError
if errors.As(err, &retryable) && !retryable.Retryable() {
return err // 不可重试,直接返回
}

// 网络超时也可重试
if errors.Is(err, context.DeadlineExceeded) {
continue
}

select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(time.Duration(i+1) * time.Second):
}
}
return fmt.Errorf("重试次数用尽")
}

HTTP 客户端重试

func HTTPGetWithRetry(ctx context.Context, url string, maxRetries int) (*http.Response, error) {
var lastErr error
delay := 500 * time.Millisecond

for i := 0; i <= maxRetries; i++ {
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
resp, err := http.DefaultClient.Do(req)
if err == nil && resp.StatusCode < 500 {
return resp, nil
}
if resp != nil {
resp.Body.Close()
lastErr = fmt.Errorf("HTTP %d", resp.StatusCode)
} else {
lastErr = err
}

if i < maxRetries {
jitter := time.Duration(rand.Int63n(int64(delay)))
time.Sleep(delay + jitter)
delay *= 2
}
}
return nil, lastErr
}

退避策略对比

策略公式特点
固定间隔d简单,可能惊群
线性退避d * n逐步拉长
指数退避d * 2^n快速拉长
指数+抖动d * 2^n + random避免惊群(推荐)

常见面试问题

Q1: 重试和幂等性的关系?

答案:重试必须配合幂等性设计。如果接口不幂等(如扣款),重试可能导致重复执行。确保被调用方支持幂等(唯一请求 ID、数据库唯一约束等)。

Q2: 什么情况不应该重试?

答案

  • 4xx 错误(客户端参数错误,重试也不会成功)
  • 业务逻辑错误(余额不足、权限不够)
  • 非幂等操作(无法保证幂等时不要重试)

相关链接