本文内容主要来自:

  1. https://darjun.github.io/2021/06/26/godailylib/resty/
  2. https://blog.csdn.net/xixihahalelehehe/article/details/111373416

resty 库 Github 地址:https://github.com/go-resty/resty

Introduction

resty是 Go 语言的一个 HTTP client 库。resty功能强大,特性丰富。它支持几乎所有的 HTTP 方法(GET/POST/PUT/DELETE/OPTION/HEAD/PATCH 等),并提供了简单易用的 API。

Get Started

安装:

  1. $ go get github.com/go-resty/resty/v2

请求百度首页:

  1. package main
  2. import (
  3. "fmt"
  4. "github.com/go-resty/resty/v2"
  5. "log"
  6. )
  7. func main() {
  8. client := resty.New()
  9. resp, err := client.R().Get("https://baidu.com")
  10. if err != nil {
  11. log.Fatal(err)
  12. }
  13. fmt.Println("Response Info:")
  14. fmt.Println(" Status Code:", resp.StatusCode())
  15. fmt.Println(" Status:", resp.Status())
  16. fmt.Println(" Proto:", resp.Proto())
  17. fmt.Println(" Time:", resp.Time())
  18. fmt.Println(" Received At:", resp.ReceivedAt())
  19. fmt.Println(" Size:", resp.Size())
  20. fmt.Print("\n")
  21. fmt.Println("Headers:")
  22. for key, value := range resp.Header() {
  23. fmt.Println(" ", key, "=", value)
  24. }
  25. fmt.Print("\n")
  26. fmt.Println("Cookies:")
  27. for i, cookie := range resp.Cookies() {
  28. fmt.Printf(" cookie%d: name:%s value:%s\n", i, cookie.Name, cookie.Value)
  29. }
  30. }

运行结果:

  1. Response Info:
  2. Status Code: 200
  3. Status: 200 OK
  4. Proto: HTTP/1.1
  5. Time: 144.5688ms
  6. Received At: 2022-06-05 00:01:23.3790091 +0800 CST m=+0.147991601
  7. Size: 353988
  8. Headers:
  9. Bdpagetype = [1]
  10. Date = [Sat, 04 Jun 2022 16:01:22 GMT]
  11. Expires = [Sat, 04 Jun 2022 16:00:31 GMT]
  12. X-Frame-Options = [sameorigin]
  13. Cache-Control = [private]
  14. Content-Type = [text/html;charset=utf-8]
  15. P3p = [CP=" OTI DSP COR IVA OUR IND COM " CP=" OTI DSP COR IVA OUR IND COM "]
  16. Set-Cookie = [BAIDUID=16006F25F67AD62F3AE123CB60C7ED82:FG=1; expires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.com
  17. BIDUPSID=16006F25F67AD62F3AE123CB60C7ED82; expires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.com PSTM=1654358482; e
  18. xpires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.com BAIDUID=16006F25F67AD62FDA7DF8106B091889:FG=1; max-age=31536000
  19. ; expires=Sun, 04-Jun-23 16:01:22 GMT; domain=.baidu.com; path=/; version=1; comment=bd BDSVRTM=0; path=/ BD_HOME=1; path=/ H_PS_PSSID=36455_3166
  20. 0_36452_36165_36487_36518_36073_36519_26350_36299_36468_36311; path=/; domain=.baidu.com]
  21. Traceid = [1654358482240799975412881669547152689779]
  22. Server = [BWS/1.1]
  23. Connection = [keep-alive]
  24. Bdqid = [0xb2c4e2340000ca73]
  25. X-Ua-Compatible = [IE=Edge,chrome=1]
  26. Cookies:
  27. cookie0: name:BAIDUID value:16006F25F67AD62F3AE123CB60C7ED82:FG=1
  28. cookie1: name:BIDUPSID value:16006F25F67AD62F3AE123CB60C7ED82
  29. cookie2: name:PSTM value:1654358482
  30. cookie3: name:BAIDUID value:16006F25F67AD62FDA7DF8106B091889:FG=1
  31. cookie4: name:BDSVRTM value:0
  32. cookie5: name:BD_HOME value:1
  33. cookie6: name:H_PS_PSSID value:36455_31660_36452_36165_36487_36518_36073_36519_26350_36299_36468_36311

resty的使用很简单:

  • 首先,调用resty.New()创建一个client对象;
  • 调用client对象的R()方法创建一个请求对象;
  • 调用请求对象的Get()/Post()等方法,传入参数 URL,就可以向对应的 URL 发送 HTTP 请求了,返回一个响应对象和 error;
  • 响应对象提供很多方法可以检查响应的状态,首部,Cookie,响应内容等信息。

上面程序中我们获取了:

  • StatusCode():状态码,如 200;
  • Status():状态码和状态信息,如 200 OK;
  • Proto():协议,如 HTTP/1.1;
  • Time():从发送请求到收到响应的时间;
  • ReceivedAt():接收到响应的时刻;
  • Size():响应大小;
  • Header():响应首部信息,以http.Header类型返回,即map[string][]string
  • Cookies():服务器通过Set-Cookie首部设置的 cookie 信息。

除此之外,可以这样获取响应体:

  • 直接打印resp
  • 调用resp.Body(),会返回一个字节数组
  • 调用resp.String(),会返回一个字符串,该字符串是原始响应体去除了两端的空白字符。

设置请求信息

resty提供了链式调用的方法设置请求信息。

查询字符串

我们可以通过三种方式设置查询字符串。

一种是调用请求对象的SetQueryString()设置我们拼接好的查询字符串:

  1. client.R().
  2. SetQueryString("name=dj&age=18").
  3. Get(...)

第二种是调用请求对象的SetQueryParams(),传入map[string]string,由resty来帮我们拼接:

  1. client.R().
  2. SetQueryParams(map[string]string{
  3. "name": "dj",
  4. "age": "18",
  5. }).
  6. Get(...)

最后还有个SetQueryParam()函数可以用来设置单个参数:

  1. client.R().
  2. SetQueryParam("access_token", accessToken).
  3. Get(...)

路径参数

resty提供设置路径参数的方法,我们调用SetPathParams()传入map[string]string参数,然后后面的 URL 路径中就可以使用这个map中的键了:

  1. client.R().
  2. SetPathParams(map[string]string{
  3. "user": "dj",
  4. }).
  5. Get("/v1/users/{user}/details")

注意,路径中的键需要用{}包起来。

请求头

使用SetHeader()设置请求头:

  1. client.R().
  2. SetHeader("Content-Type", "application/json").
  3. Get(...)

如果要设置多个请求头需要多次调用SetHeader()

使用SetHeaders()可以一次性设置多个请求头,参数是一个map[string]string

  1. client.R().
  2. SetHeaders(map[string]string{
  3. "Content-Type": "application/json",
  4. "X-Token": "xxx",
  5. }).
  6. Get(...)

请求体

请求体可以是多种类型:字符串,[]byte,对象,map[string]interface{}等。

如果设置字符串,那么Content-Type请求头默认会被设置为text/plain; charset=utf-8

  1. client.R().
  2. SetBody(`{"name": "dj", "age": 18}`).
  3. Post(...)

如果想让服务端将请求体识别为 JSON 格式,需要手动设置Content-Type

  1. client.R().
  2. SetBody(`{"name": "dj", "age": 18}`).
  3. SetHeader("Content-Type", "application/json").
  4. Post(...)

如果将请求体设置为 对象、map[string]stringmap[string]interface{}等,那么Content-Type请求头默认就会被设置为application/json

  1. client.R().
  2. SetBody(User{
  3. Username: "testuser",
  4. Password: "testpass"
  5. }).
  6. Post("https://myapp.com/login")
  7. client.R().
  8. SetBody(map[string]interface{}{
  9. "username": "testuser",
  10. "password": "testpass"
  11. }).
  12. Post("https://myapp.com/login")

此外,resty还提供了一个SetFormData()方法用来方便地设置表单数据,其参数是一个map[string]string

  1. client.R().
  2. SetFormData(map[string]string{
  3. "key1": "value1",
  4. "key2", "value2",
  5. }).
  6. Post(...)

Content-Type请求头默认会被设置为application/x-www-form-urlencoded

Content-Length

设置携带Content-Length首部,resty自动计算:

  1. client.R().
  2. SetBody(User{Name:"dj", Age:18}).
  3. SetContentLength(true).
  4. Get(...)

自动 Unmarshal

resty可以自动将响应数据 Unmarshal 到对应的结构体对象中。

下面看一个例子,我们知道很多 js 文件都托管在 cdn 上,可以通过https://api.cdnjs.com/libraries获取这些库的基本信息,返回一个 JSON 数据,格式如下:
Go HTTP 请求库 - resty - 图1

接下来,我们定义结构体,然后使用resty拉取信息,自动 Unmarshal:

  1. type Library struct {
  2. Name string
  3. Latest string
  4. }
  5. type Libraries struct {
  6. Results []*Library
  7. }
  8. func main() {
  9. client := resty.New()
  10. libraries := &Libraries{}
  11. client.R().SetResult(libraries).Get("https://api.cdnjs.com/libraries")
  12. fmt.Printf("%d libraries\n", len(libraries.Results))
  13. for _, lib := range libraries.Results {
  14. fmt.Println("first library:")
  15. fmt.Printf("name:%s latest:%s\n", lib.Name, lib.Latest)
  16. break
  17. }
  18. }

可以看到,我们只需要创建一个结果类型的对象,然后调用请求对象的SetResult()方法,resty会自动将响应的数据 Unmarshal 到传入的对象中。

运行结果:

  1. 4040 libraries
  2. first library:
  3. name:vue latest:https://cdnjs.cloudflare.com/ajax/libs/vue/3.1.2/vue.min.js

设置响应数据格式

一般情况下,resty会根据响应中的Content-Type来推断数据格式。
但是有时候响应中无Content-Type首部或与内容格式不一致,我们可以通过调用请求对象的ForceContentType()强制让resty按照特定的格式来解析响应:

  1. client.R().
  2. ForceContentType("application/json").
  3. Get(...)

设置代理

resty提供了SetProxy()方法为请求添加代理,还可以调用RemoveProxy()移除代理。

  1. client := resty.New()
  2. client.SetProxy("http://proxyserver:8888")
  3. client.RemoveProxy()

重试机制

由于网络抖动带来的接口稳定性的问题resty提供了重试功能来解决。

  1. client := resty.New()
  2. client.
  3. SetRetryCount(3).
  4. SetRetryWaitTime(5 * time.Second).
  5. SetRetryMaxWaitTime(20 * time.Second).
  6. SetRetryAfter(func(client *resty.Client, resp *resty.Response) (time.Duration, error) {
  7. return 0, errors.New("quota exceeded")
  8. })
  9. client.AddRetryCondition(
  10. func(r *resty.Response, err error) bool {
  11. return r.StatusCode() == http.StatusTooManyRequests
  12. },
  13. )
  • SetRetryCount设置重试次数;
  • SetRetryWaitTimeSetRetryMaxWaitTime设置等待时间;
  • SetRetryAfter是一个重试后的回调方法;
  • AddRetryCondition设置重试的条件。

中间件(拦截器)

resty提供了中间件特性(或者可以说拦截器)。

OnBeforeRequestOnAfterResponse回调方法,可以在请求之前和响应之后加入自定义逻辑。
参数包含了resty.Client和当前请求的resty.Request对象。
成功时返回nil,失败时返回error对象。

  1. client := resty.New()
  2. client.OnBeforeRequest(func(c *resty.Client, req *resty.Request) error {
  3. return nil
  4. })
  5. client.OnAfterResponse(func(c *resty.Client, resp *resty.Response) error {
  6. return nil
  7. })

辅助功能:trace

resty提供了一个辅助功能:trace,可以记录请求的每一步的耗时和其他信息。

可以在请求对象上调用EnableTrace()方法启用 trace:

  1. client.R().EnableTrace().Get("https://baidu.com")

在完成请求之后,可以通过调用请求对象的TraceInfo()方法获取信息:

  1. ti := resp.Request.TraceInfo()
  2. fmt.Println("Request Trace Info:")
  3. fmt.Println("DNSLookup:", ti.DNSLookup)
  4. fmt.Println("ConnTime:", ti.ConnTime)
  5. fmt.Println("TCPConnTime:", ti.TCPConnTime)
  6. fmt.Println("TLSHandshake:", ti.TLSHandshake)
  7. fmt.Println("ServerTime:", ti.ServerTime)
  8. fmt.Println("ResponseTime:", ti.ResponseTime)
  9. fmt.Println("TotalTime:", ti.TotalTime)
  10. fmt.Println("IsConnReused:", ti.IsConnReused)
  11. fmt.Println("IsConnWasIdle:", ti.IsConnWasIdle)
  12. fmt.Println("ConnIdleTime:", ti.ConnIdleTime)
  13. fmt.Println("RequestAttempt:", ti.RequestAttempt)
  14. fmt.Println("RemoteAddr:", ti.RemoteAddr.String())
  • DNSLookup:DNS 查询时间,如果提供的是一个域名而非 IP,就需要向 DNS 系统查询对应 IP 才能进行后续操作;
  • ConnTime:获取一个连接的耗时,可能从连接池获取,也可能新建;
  • TCPConnTime:TCP 连接耗时,从 DNS 查询结束到 TCP 连接建立;
  • TLSHandshake:TLS 握手耗时;
  • ServerTime:服务器处理耗时,计算从连接建立到客户端收到第一个字节的时间间隔;
  • ResponseTime:响应耗时,从接收到第一个响应字节,到接收到完整响应之间的时间间隔;
  • TotalTime:整个流程的耗时;
  • IsConnReused:TCP 连接是否复用了;
  • IsConnWasIdle:连接是否是从空闲的连接池获取的;
  • ConnIdleTime:连接空闲时间;
  • RequestAttempt:请求执行流程中的请求次数,包括重试次数;
  • RemoteAddr:远程的服务地址,IP:PORT格式。

resty对这些区分得很细。实际上resty是使用标准库net/http/httptrace提供的功能,httptrace提供一个结构体,我们可以设置各个阶段的回调函数:

  1. // src/net/http/httptrace.go
  2. type ClientTrace struct {
  3. GetConn func(hostPort string)
  4. GotConn func(GotConnInfo)
  5. PutIdleConn func(err error)
  6. GotFirstResponseByte func()
  7. Got100Continue func()
  8. Got1xxResponse func(code int, header textproto.MIMEHeader) error // Go 1.11
  9. DNSStart func(DNSStartInfo)
  10. DNSDone func(DNSDoneInfo)
  11. ConnectStart func(network, addr string)
  12. ConnectDone func(network, addr string, err error)
  13. TLSHandshakeStart func() // Go 1.8
  14. TLSHandshakeDone func(tls.ConnectionState, error) // Go 1.8
  15. WroteHeaderField func(key string, value []string) // Go 1.11
  16. WroteHeaders func()
  17. Wait100Continue func()
  18. WroteRequest func(WroteRequestInfo)
  19. }

可以从字段名简单了解回调的含义。

resty在启用 trace 后设置了如下回调:

  1. // src/github.com/go-resty/resty/trace.go
  2. func (t *clientTrace) createContext(ctx context.Context) context.Context {
  3. return httptrace.WithClientTrace(
  4. ctx,
  5. &httptrace.ClientTrace{
  6. DNSStart: func(_ httptrace.DNSStartInfo) {
  7. t.dnsStart = time.Now()
  8. },
  9. DNSDone: func(_ httptrace.DNSDoneInfo) {
  10. t.dnsDone = time.Now()
  11. },
  12. ConnectStart: func(_, _ string) {
  13. if t.dnsDone.IsZero() {
  14. t.dnsDone = time.Now()
  15. }
  16. if t.dnsStart.IsZero() {
  17. t.dnsStart = t.dnsDone
  18. }
  19. },
  20. ConnectDone: func(net, addr string, err error) {
  21. t.connectDone = time.Now()
  22. },
  23. GetConn: func(_ string) {
  24. t.getConn = time.Now()
  25. },
  26. GotConn: func(ci httptrace.GotConnInfo) {
  27. t.gotConn = time.Now()
  28. t.gotConnInfo = ci
  29. },
  30. GotFirstResponseByte: func() {
  31. t.gotFirstResponseByte = time.Now()
  32. },
  33. TLSHandshakeStart: func() {
  34. t.tlsHandshakeStart = time.Now()
  35. },
  36. TLSHandshakeDone: func(_ tls.ConnectionState, _ error) {
  37. t.tlsHandshakeDone = time.Now()
  38. },
  39. },
  40. )
  41. }

然后在获取TraceInfo时,根据各个时间点计算耗时:

  1. // src/github.com/go-resty/resty/request.go
  2. func (r *Request) TraceInfo() TraceInfo {
  3. ct := r.clientTrace
  4. if ct == nil {
  5. return TraceInfo{}
  6. }
  7. ti := TraceInfo{
  8. DNSLookup: ct.dnsDone.Sub(ct.dnsStart),
  9. TLSHandshake: ct.tlsHandshakeDone.Sub(ct.tlsHandshakeStart),
  10. ServerTime: ct.gotFirstResponseByte.Sub(ct.gotConn),
  11. IsConnReused: ct.gotConnInfo.Reused,
  12. IsConnWasIdle: ct.gotConnInfo.WasIdle,
  13. ConnIdleTime: ct.gotConnInfo.IdleTime,
  14. RequestAttempt: r.Attempt,
  15. }
  16. if ct.gotConnInfo.Reused {
  17. ti.TotalTime = ct.endTime.Sub(ct.getConn)
  18. } else {
  19. ti.TotalTime = ct.endTime.Sub(ct.dnsStart)
  20. }
  21. if !ct.connectDone.IsZero() {
  22. ti.TCPConnTime = ct.connectDone.Sub(ct.dnsDone)
  23. }
  24. if !ct.gotConn.IsZero() {
  25. ti.ConnTime = ct.gotConn.Sub(ct.getConn)
  26. }
  27. if !ct.gotFirstResponseByte.IsZero() {
  28. ti.ResponseTime = ct.endTime.Sub(ct.gotFirstResponseByte)
  29. }
  30. if ct.gotConnInfo.Conn != nil {
  31. ti.RemoteAddr = ct.gotConnInfo.Conn.RemoteAddr()
  32. }
  33. return ti
  34. }

运行结果:

  1. Request Trace Info:
  2. DNSLookup: 2.815171ms
  3. ConnTime: 941.635171ms
  4. TCPConnTime: 269.069692ms
  5. TLSHandshake: 669.276011ms
  6. ServerTime: 274.623991ms
  7. ResponseTime: 112.216µs
  8. TotalTime: 1.216276906s
  9. IsConnReused: false
  10. IsConnWasIdle: false
  11. ConnIdleTime: 0s
  12. RequestAttempt: 1
  13. RemoteAddr: 18.235.124.214:443

可以看到 TLS 消耗了近一半的时间。