本文内容主要来自:
- https://darjun.github.io/2021/06/26/godailylib/resty/
- 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
安装:
$ go get github.com/go-resty/resty/v2
请求百度首页:
package main
import (
"fmt"
"github.com/go-resty/resty/v2"
"log"
)
func main() {
client := resty.New()
resp, err := client.R().Get("https://baidu.com")
if err != nil {
log.Fatal(err)
}
fmt.Println("Response Info:")
fmt.Println(" Status Code:", resp.StatusCode())
fmt.Println(" Status:", resp.Status())
fmt.Println(" Proto:", resp.Proto())
fmt.Println(" Time:", resp.Time())
fmt.Println(" Received At:", resp.ReceivedAt())
fmt.Println(" Size:", resp.Size())
fmt.Print("\n")
fmt.Println("Headers:")
for key, value := range resp.Header() {
fmt.Println(" ", key, "=", value)
}
fmt.Print("\n")
fmt.Println("Cookies:")
for i, cookie := range resp.Cookies() {
fmt.Printf(" cookie%d: name:%s value:%s\n", i, cookie.Name, cookie.Value)
}
}
运行结果:
Response Info:
Status Code: 200
Status: 200 OK
Proto: HTTP/1.1
Time: 144.5688ms
Received At: 2022-06-05 00:01:23.3790091 +0800 CST m=+0.147991601
Size: 353988
Headers:
Bdpagetype = [1]
Date = [Sat, 04 Jun 2022 16:01:22 GMT]
Expires = [Sat, 04 Jun 2022 16:00:31 GMT]
X-Frame-Options = [sameorigin]
Cache-Control = [private]
Content-Type = [text/html;charset=utf-8]
P3p = [CP=" OTI DSP COR IVA OUR IND COM " CP=" OTI DSP COR IVA OUR IND COM "]
Set-Cookie = [BAIDUID=16006F25F67AD62F3AE123CB60C7ED82:FG=1; expires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.com
BIDUPSID=16006F25F67AD62F3AE123CB60C7ED82; expires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.com PSTM=1654358482; e
xpires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.com BAIDUID=16006F25F67AD62FDA7DF8106B091889:FG=1; max-age=31536000
; 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
0_36452_36165_36487_36518_36073_36519_26350_36299_36468_36311; path=/; domain=.baidu.com]
Traceid = [1654358482240799975412881669547152689779]
Server = [BWS/1.1]
Connection = [keep-alive]
Bdqid = [0xb2c4e2340000ca73]
X-Ua-Compatible = [IE=Edge,chrome=1]
Cookies:
cookie0: name:BAIDUID value:16006F25F67AD62F3AE123CB60C7ED82:FG=1
cookie1: name:BIDUPSID value:16006F25F67AD62F3AE123CB60C7ED82
cookie2: name:PSTM value:1654358482
cookie3: name:BAIDUID value:16006F25F67AD62FDA7DF8106B091889:FG=1
cookie4: name:BDSVRTM value:0
cookie5: name:BD_HOME value:1
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()
设置我们拼接好的查询字符串:
client.R().
SetQueryString("name=dj&age=18").
Get(...)
第二种是调用请求对象的SetQueryParams()
,传入map[string]string
,由resty
来帮我们拼接:
client.R().
SetQueryParams(map[string]string{
"name": "dj",
"age": "18",
}).
Get(...)
最后还有个SetQueryParam()
函数可以用来设置单个参数:
client.R().
SetQueryParam("access_token", accessToken).
Get(...)
路径参数
resty
提供设置路径参数的方法,我们调用SetPathParams()
传入map[string]string
参数,然后后面的 URL 路径中就可以使用这个map
中的键了:
client.R().
SetPathParams(map[string]string{
"user": "dj",
}).
Get("/v1/users/{user}/details")
注意,路径中的键需要用{}
包起来。
请求头
使用SetHeader()
设置请求头:
client.R().
SetHeader("Content-Type", "application/json").
Get(...)
如果要设置多个请求头需要多次调用SetHeader()
。
使用SetHeaders()
可以一次性设置多个请求头,参数是一个map[string]string
:
client.R().
SetHeaders(map[string]string{
"Content-Type": "application/json",
"X-Token": "xxx",
}).
Get(...)
请求体
请求体可以是多种类型:字符串,[]byte
,对象,map[string]interface{}
等。
如果设置字符串,那么Content-Type
请求头默认会被设置为text/plain; charset=utf-8
。
client.R().
SetBody(`{"name": "dj", "age": 18}`).
Post(...)
如果想让服务端将请求体识别为 JSON 格式,需要手动设置Content-Type
:
client.R().
SetBody(`{"name": "dj", "age": 18}`).
SetHeader("Content-Type", "application/json").
Post(...)
如果将请求体设置为 对象、map[string]string
、map[string]interface{}
等,那么Content-Type
请求头默认就会被设置为application/json
。
client.R().
SetBody(User{
Username: "testuser",
Password: "testpass"
}).
Post("https://myapp.com/login")
client.R().
SetBody(map[string]interface{}{
"username": "testuser",
"password": "testpass"
}).
Post("https://myapp.com/login")
此外,resty
还提供了一个SetFormData()
方法用来方便地设置表单数据,其参数是一个map[string]string
。
client.R().
SetFormData(map[string]string{
"key1": "value1",
"key2", "value2",
}).
Post(...)
Content-Type
请求头默认会被设置为application/x-www-form-urlencoded
。
Content-Length
设置携带Content-Length
首部,resty
自动计算:
client.R().
SetBody(User{Name:"dj", Age:18}).
SetContentLength(true).
Get(...)
自动 Unmarshal
resty
可以自动将响应数据 Unmarshal 到对应的结构体对象中。
下面看一个例子,我们知道很多 js 文件都托管在 cdn 上,可以通过https://api.cdnjs.com/libraries
获取这些库的基本信息,返回一个 JSON 数据,格式如下:
接下来,我们定义结构体,然后使用resty
拉取信息,自动 Unmarshal:
type Library struct {
Name string
Latest string
}
type Libraries struct {
Results []*Library
}
func main() {
client := resty.New()
libraries := &Libraries{}
client.R().SetResult(libraries).Get("https://api.cdnjs.com/libraries")
fmt.Printf("%d libraries\n", len(libraries.Results))
for _, lib := range libraries.Results {
fmt.Println("first library:")
fmt.Printf("name:%s latest:%s\n", lib.Name, lib.Latest)
break
}
}
可以看到,我们只需要创建一个结果类型的对象,然后调用请求对象的SetResult()
方法,resty
会自动将响应的数据 Unmarshal 到传入的对象中。
运行结果:
4040 libraries
first library:
name:vue latest:https://cdnjs.cloudflare.com/ajax/libs/vue/3.1.2/vue.min.js
设置响应数据格式
一般情况下,resty
会根据响应中的Content-Type
来推断数据格式。
但是有时候响应中无Content-Type
首部或与内容格式不一致,我们可以通过调用请求对象的ForceContentType()
强制让resty
按照特定的格式来解析响应:
client.R().
ForceContentType("application/json").
Get(...)
设置代理
resty
提供了SetProxy()
方法为请求添加代理,还可以调用RemoveProxy()
移除代理。
client := resty.New()
client.SetProxy("http://proxyserver:8888")
client.RemoveProxy()
重试机制
由于网络抖动带来的接口稳定性的问题resty
提供了重试功能来解决。
client := resty.New()
client.
SetRetryCount(3).
SetRetryWaitTime(5 * time.Second).
SetRetryMaxWaitTime(20 * time.Second).
SetRetryAfter(func(client *resty.Client, resp *resty.Response) (time.Duration, error) {
return 0, errors.New("quota exceeded")
})
client.AddRetryCondition(
func(r *resty.Response, err error) bool {
return r.StatusCode() == http.StatusTooManyRequests
},
)
SetRetryCount
设置重试次数;SetRetryWaitTime
和SetRetryMaxWaitTime
设置等待时间;SetRetryAfter
是一个重试后的回调方法;AddRetryCondition
设置重试的条件。
中间件(拦截器)
resty
提供了中间件特性(或者可以说拦截器)。
OnBeforeRequest
和OnAfterResponse
回调方法,可以在请求之前和响应之后加入自定义逻辑。
参数包含了resty.Client
和当前请求的resty.Request
对象。
成功时返回nil
,失败时返回error
对象。
client := resty.New()
client.OnBeforeRequest(func(c *resty.Client, req *resty.Request) error {
return nil
})
client.OnAfterResponse(func(c *resty.Client, resp *resty.Response) error {
return nil
})
辅助功能:trace
resty
提供了一个辅助功能:trace,可以记录请求的每一步的耗时和其他信息。
可以在请求对象上调用EnableTrace()
方法启用 trace:
client.R().EnableTrace().Get("https://baidu.com")
在完成请求之后,可以通过调用请求对象的TraceInfo()
方法获取信息:
ti := resp.Request.TraceInfo()
fmt.Println("Request Trace Info:")
fmt.Println("DNSLookup:", ti.DNSLookup)
fmt.Println("ConnTime:", ti.ConnTime)
fmt.Println("TCPConnTime:", ti.TCPConnTime)
fmt.Println("TLSHandshake:", ti.TLSHandshake)
fmt.Println("ServerTime:", ti.ServerTime)
fmt.Println("ResponseTime:", ti.ResponseTime)
fmt.Println("TotalTime:", ti.TotalTime)
fmt.Println("IsConnReused:", ti.IsConnReused)
fmt.Println("IsConnWasIdle:", ti.IsConnWasIdle)
fmt.Println("ConnIdleTime:", ti.ConnIdleTime)
fmt.Println("RequestAttempt:", ti.RequestAttempt)
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
提供一个结构体,我们可以设置各个阶段的回调函数:
// src/net/http/httptrace.go
type ClientTrace struct {
GetConn func(hostPort string)
GotConn func(GotConnInfo)
PutIdleConn func(err error)
GotFirstResponseByte func()
Got100Continue func()
Got1xxResponse func(code int, header textproto.MIMEHeader) error // Go 1.11
DNSStart func(DNSStartInfo)
DNSDone func(DNSDoneInfo)
ConnectStart func(network, addr string)
ConnectDone func(network, addr string, err error)
TLSHandshakeStart func() // Go 1.8
TLSHandshakeDone func(tls.ConnectionState, error) // Go 1.8
WroteHeaderField func(key string, value []string) // Go 1.11
WroteHeaders func()
Wait100Continue func()
WroteRequest func(WroteRequestInfo)
}
可以从字段名简单了解回调的含义。
resty
在启用 trace 后设置了如下回调:
// src/github.com/go-resty/resty/trace.go
func (t *clientTrace) createContext(ctx context.Context) context.Context {
return httptrace.WithClientTrace(
ctx,
&httptrace.ClientTrace{
DNSStart: func(_ httptrace.DNSStartInfo) {
t.dnsStart = time.Now()
},
DNSDone: func(_ httptrace.DNSDoneInfo) {
t.dnsDone = time.Now()
},
ConnectStart: func(_, _ string) {
if t.dnsDone.IsZero() {
t.dnsDone = time.Now()
}
if t.dnsStart.IsZero() {
t.dnsStart = t.dnsDone
}
},
ConnectDone: func(net, addr string, err error) {
t.connectDone = time.Now()
},
GetConn: func(_ string) {
t.getConn = time.Now()
},
GotConn: func(ci httptrace.GotConnInfo) {
t.gotConn = time.Now()
t.gotConnInfo = ci
},
GotFirstResponseByte: func() {
t.gotFirstResponseByte = time.Now()
},
TLSHandshakeStart: func() {
t.tlsHandshakeStart = time.Now()
},
TLSHandshakeDone: func(_ tls.ConnectionState, _ error) {
t.tlsHandshakeDone = time.Now()
},
},
)
}
然后在获取TraceInfo
时,根据各个时间点计算耗时:
// src/github.com/go-resty/resty/request.go
func (r *Request) TraceInfo() TraceInfo {
ct := r.clientTrace
if ct == nil {
return TraceInfo{}
}
ti := TraceInfo{
DNSLookup: ct.dnsDone.Sub(ct.dnsStart),
TLSHandshake: ct.tlsHandshakeDone.Sub(ct.tlsHandshakeStart),
ServerTime: ct.gotFirstResponseByte.Sub(ct.gotConn),
IsConnReused: ct.gotConnInfo.Reused,
IsConnWasIdle: ct.gotConnInfo.WasIdle,
ConnIdleTime: ct.gotConnInfo.IdleTime,
RequestAttempt: r.Attempt,
}
if ct.gotConnInfo.Reused {
ti.TotalTime = ct.endTime.Sub(ct.getConn)
} else {
ti.TotalTime = ct.endTime.Sub(ct.dnsStart)
}
if !ct.connectDone.IsZero() {
ti.TCPConnTime = ct.connectDone.Sub(ct.dnsDone)
}
if !ct.gotConn.IsZero() {
ti.ConnTime = ct.gotConn.Sub(ct.getConn)
}
if !ct.gotFirstResponseByte.IsZero() {
ti.ResponseTime = ct.endTime.Sub(ct.gotFirstResponseByte)
}
if ct.gotConnInfo.Conn != nil {
ti.RemoteAddr = ct.gotConnInfo.Conn.RemoteAddr()
}
return ti
}
运行结果:
Request Trace Info:
DNSLookup: 2.815171ms
ConnTime: 941.635171ms
TCPConnTime: 269.069692ms
TLSHandshake: 669.276011ms
ServerTime: 274.623991ms
ResponseTime: 112.216µs
TotalTime: 1.216276906s
IsConnReused: false
IsConnWasIdle: false
ConnIdleTime: 0s
RequestAttempt: 1
RemoteAddr: 18.235.124.214:443
可以看到 TLS 消耗了近一半的时间。