自己动手撸一个支持超时与重试Go HTTP Client

之前写过一篇上下文中断在Go语言中的使用 的文章,简单的介绍了上下文中断在Go语言中使用与原理!那么这篇博客,就来讲讲上下文中断在实际中的使用!大家如果不是很了解的话可以点击前面的链接了解下!

在实际开发过程中,太多故障是因为超时没有设置或者设置的不对而造成的。而这些故障都是因为没有意识到超时设置的重要性而造成的。如果应用不设置超时,则可能会导致请求响应慢,慢请求累积导致连锁反应,甚至应用雪崩。而有些中间件或者框架在超时后会进行重试(如设置超时重试两次),读服务天然适合重试,但写服务大多不能重试,重试次数太多会导致多倍请求流量,即模拟了DDoS攻击,后果可能可想而知!

先看看实现:

package httpclient

import (
  "bytes"
  "context"
  "crypto/tls"
  "encoding/json"
  "io"
  "net"
  "net/http"
  "time"

  "github.com/pkg/errors"
  "github.com/quan-xie/tuba/util/retry"
  "github.com/quan-xie/tuba/util/xtime"
)

const (
  minRead               = 16 * 1024 // 16kb
  defaultRetryCount int = 0
)

type Config struct {
  Dial       xtime.Duration
  Timeout    xtime.Duration
  KeepAlive  xtime.Duration
  retryCount int
}

type HttpClient struct {
  conf       *Config
  client     *http.Client
  dialer     *net.Dialer
  transport  *http.Transport
  retryCount int
  retrier    retry.Retriable
}

// NewHTTPClient returns a new instance of httpClient
func NewHTTPClient(c *Config) *HttpClient {
  dialer := &net.Dialer{
    Timeout:   time.Duration(c.Dial),
    KeepAlive: time.Duration(c.KeepAlive),
  }
  transport := &http.Transport{
    DialContext:     dialer.DialContext,
    TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
  }
  return &HttpClient{
    conf: c,
    client: &http.Client{
      Transport: transport,
    },
    retryCount: defaultRetryCount,
    retrier:    retry.NewNoRetrier(),
  }
}

// SetRetryCount sets the retry count for the httpClient
func (c *HttpClient) SetRetryCount(count int) {
  c.retryCount = count
}

// SetRetryCount sets the retry count for the httpClient
func (c *HttpClient) SetRetrier(retrier retry.Retriable) {
  c.retrier = retrier
}

// Get makes a HTTP GET request to provided URL with context passed in
func (c *HttpClient) Get(ctx context.Context, url string, headers http.Header, res interface{}) (err error) {
  request, err := http.NewRequest(http.MethodGet, url, nil)
  if err != nil {
    return errors.Wrap(err, "GET - request creation failed")
  }

  request.Header = headers

  return c.Do(ctx, request, res)
}

// Post makes a HTTP POST request to provided URL with context passed in
func (c *HttpClient) Post(ctx context.Context, url string, body io.Reader, headers http.Header, res interface{}) (err error) {
  request, err := http.NewRequest(http.MethodPost, url, body)
  if err != nil {
    return errors.Wrap(err, "POST - request creation failed")
  }

  request.Header = headers

  return c.Do(ctx, request, res)
}

// Put makes a HTTP PUT request to provided URL with context passed in
func (c *HttpClient) Put(ctx context.Context, url string, body io.Reader, headers http.Header, res interface{}) (err error) {
  request, err := http.NewRequest(http.MethodPut, url, body)
  if err != nil {
    return errors.Wrap(err, "PUT - request creation failed")
  }

  request.Header = headers

  return c.Do(ctx, request, res)
}

// Patch makes a HTTP PATCH request to provided URL with context passed in
func (c *HttpClient) Patch(ctx context.Context, url string, body io.Reader, headers http.Header, res interface{}) (err error) {
  request, err := http.NewRequest(http.MethodPatch, url, body)
  if err != nil {
    return errors.Wrap(err, "PATCH - request creation failed")
  }

  request.Header = headers

  return c.Do(ctx, request, res)
}

// Delete makes a HTTP DELETE request to provided URL with context passed in
func (c *HttpClient) Delete(ctx context.Context, url string, headers http.Header, res interface{}) (err error) {
  request, err := http.NewRequest(http.MethodDelete, url, nil)
  if err != nil {
    return errors.Wrap(err, "DELETE - request creation failed")
  }

  request.Header = headers

  return c.Do(ctx, request, res)
}

// Do makes an HTTP request with the native `http.Do` interface and context passed in
func (c *HttpClient) Do(ctx context.Context, req *http.Request, res interface{}) (err error) {
  for i := 0; i <= c.retryCount; i++ {
    if err = c.request(ctx, req, res); err != nil {
      err = errors.Wrap(err, "request - request failed")
      backoffTime := c.retrier.NextInterval(i)
      time.Sleep(backoffTime)
      continue
    }
    break
  }
  return
}

func (c *HttpClient) request(ctx context.Context, req *http.Request, res interface{}) (err error) {
  var (
    response *http.Response
    bs       []byte
    cancel   func()
  )
  ctx, cancel = context.WithTimeout(ctx, time.Duration(c.conf.Timeout))
  defer cancel()
  response, err = c.client.Do(req.WithContext(ctx))
  if err != nil {
    select {
    case <-ctx.Done():
      err = ctx.Err()
    }
    return
  }
  defer response.Body.Close()
  if response.StatusCode >= http.StatusInternalServerError {
    err = errors.Wrap(err, "StatusInternalServerError - Status Internal ServerError")
    return
  }
  if bs, err = readAll(response.Body, minRead); err != nil {
    err = errors.Wrap(err, "readAll - readAll failed")
    return
  }
  if res != nil {
    if err = json.Unmarshal(bs, res); err != nil {
      err = errors.Wrap(err, "Unmarshal failed")
    }
  }
  return
}

func readAll(r io.Reader, capacity int64) (b []byte, err error) {
  buf := bytes.NewBuffer(make([]byte, 0, capacity))
  // If the buffer overflows, we will get bytes.ErrTooLarge.
  // Return that as an error. Any other panic remains.
  defer func() {
    e := recover()
    if e == nil {
      return
    }
    if panicErr, ok := e.(error); ok && panicErr == bytes.ErrTooLarge {
      err = panicErr
    } else {
      panic(e)
    }
  }()
  _, err = buf.ReadFrom(r)
  return buf.Bytes(), err
}

其实最核心的是这一段:

   // 这里创建了一个设置了超时时间的context
   ctx, cancel = context.WithTimeout(ctx, time.Duration(c.conf.Timeout))
   defer cancel()
   // 将有超时的context传递给client.Do
   response, err = c.client.Do(req.WithContext(ctx))
   if err != nil {
     select {
     // select 等待超时返回结果,当http请求时间超出我们设定的时间时,context就会中断请求
     case <-ctx.Done():
       err = ctx.Err()
     }
   }

完整代码:https://github.com/quan-xie/tuba/blob/master/transport/httpclient/context_client.go

致谢:

安木哥哥 Code Reivew

趣头条赞助本文

Jobs

打赏作者

您的支持将鼓励我们继续创作!

[微信] 扫描二维码打赏

[支付宝] 扫描二维码打赏

1 thought on “自己动手撸一个支持超时与重试Go HTTP Client”

  1. 感谢分享!已推荐到《开发者头条》:https://toutiao.io/posts/t06eua 欢迎点赞支持!使用开发者头条 App 搜索 4410 即可订阅《谢权blog》

Leave a Reply

Your email address will not be published. Required fields are marked *