Go 每日一库之 resty

将相本无种,男儿当自强。这篇文章主要讲述Go 每日一库之 resty相关的知识,希望能为你提供帮助。
简介 resty是 Go 语言的一个 HTTP client 库。resty功能强大,特性丰富。它支持几乎所有的 HTTP 方法(GET/POST/PUT/DELETE/OPTION/HEAD/PATCH等),并提供了简单易用的 API。
快速使用 本文代码使用 Go Modules。
创建目录并初始化:

$ mkdir resty & & cd resty $ go mod init github.com/darjun/go-daily-lib/resty

安装resty库:
$ go get -u github.com/go-resty/resty/v2

下面我们来获取百度首页信息:
package mainimport ( "fmt" "log""github.com/go-resty/resty/v2" )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.Println("Headers:") for key, value := range resp.Header() fmt.Println(key, "=", value)fmt.Println("Cookies:") for i, cookie := range resp.Cookies() fmt.Printf("cookie%d: name:%s value:%s\\n", i, cookie.Name, cookie.Value)

resty使用比较简单。
  • 首先,调用一个resty.New()创建一个client对象;
  • 调用client对象的R()方法创建一个请求对象;
  • 调用请求对象的Get()/Post()等方法,传入参数 URL,就可以向对应的 URL 发送 HTTP 请求了。返回一个响应对象;
  • 响应对象提供很多方法可以检查响应的状态,首部,Cookie 等信息。
上面程序中我们获取了:
  • StatusCode():状态码,如 200;
  • Status():状态码和状态信息,如 200 OK;
  • Proto():协议,如 HTTP/1.1;
  • Time():从发送请求到收到响应的时间;
  • ReceivedAt():接收到响应的时刻;
  • Size():响应大小;
  • Header():响应首部信息,以http.Header类型返回,即map[string][]string
  • Cookies():服务器通过Set-Cookie首部设置的 cookie 信息。
运行程序输出的响应基本信息:
Response Info: Status Code: 200 Status: 200 OK Proto: HTTP/1.1 Time: 415.774352ms Received At: 2021-06-26 11:42:45.307157 +0800 CST m=+0.416547795 Size: 302456

首部信息:
Headers: Server = [BWS/1.1] Date = [Sat, 26 Jun 2021 03:42:45 GMT] Connection = [keep-alive] Bdpagetype = [1] Bdqid = [0xf5a61d240003b218] Vary = [Accept-Encoding Accept-Encoding] Content-Type = [text/html; charset=utf-8] Set-Cookie = [BAIDUID=BF2EE47AAAF7A20C6971F1E897ABDD43:FG=1; expires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.com BIDUPSID=BF2EE47AAAF7A20C6971F1E897ABDD43; expires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.com PSTM=1624678965; expires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.com BAIDUID=BF2EE47AAAF7A20C716E90B86906D6B0:FG=1; max-age=31536000; expires=Sun, 26-Jun-22 03:42:45 GMT; domain=.baidu.com; path=/; version=1; comment=bd BDSVRTM=0; path=/ BD_HOME=1; path=/ H_PS_PSSID=34099_31253_34133_34072_33607_34135_26350; path=/; domain=.baidu.com] Traceid = [1624678965045126810617700867425882583576] P3p = [CP=" OTI DSP COR IVA OUR IND COM " CP=" OTI DSP COR IVA OUR IND COM "] X-Ua-Compatible = [IE=Edge,chrome=1]

注意其中有一个Set-Cookie首部,这部分内容会出现在 Cookie 部分:
Cookies: cookie0: name:BAIDUID value:BF2EE47AAAF7A20C6971F1E897ABDD43:FG=1 cookie1: name:BIDUPSID value:BF2EE47AAAF7A20C6971F1E897ABDD43 cookie2: name:PSTM value:1624678965 cookie3: name:BAIDUID value:BF2EE47AAAF7A20C716E90B86906D6B0:FG=1 cookie4: name:BDSVRTM value:0 cookie5: name:BD_HOME value:1 cookie6: name:H_PS_PSSID value:34099_31253_34133_34072_33607_34135_26350

自动 Unmarshal 现在很多网站提供 API 接口,返回结构化的数据,如 JSON/XML 格式等。resty可以自动将响应数据 Unmarshal 到对应的结构体对象中。下面看一个例子,我们知道很多 js 文件都托管在 cdn 上,我们可以通过api.cdnjs.com/libraries获取这些库的基本信息,返回一个 JSON 数据,格式如下:
Go 每日一库之 resty

文章图片

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

type Library struct Namestring Latest stringtype Libraries struct Results []*Libraryfunc 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

【Go 每日一库之 resty】可以看到,我们只需要创建一个结果类型的对象,然后调用请求对象的SetResult()方法,resty会自动将响应的数据 Unmarshal 到传入的对象中。这里设置请求信息时使用链式调用的方式,即在一行中完成多个设置。
运行:
$ go run main.go 4040 libraries first library: name:vue latest:https://cdnjs.cloudflare.com/ajax/libs/vue/3.1.2/vue.min.js

一共 4040 个库,第一个就是 Vue??。我们请求https://api.cdnjs.com/libraries/vue就能获取 Vue 的详细信息:
Go 每日一库之 resty

文章图片

感兴趣可自行用resty来拉取这些信息。

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

请求信息 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(...)

resty还提供一种非常实用的设置路径参数接口,我们调用SetPathParams()传入map[string]string参数,然后后面的 URL 路径中就可以使用这个map中的键了:
client.R(). SetPathParams(map[string]string "user": "dj", ). Get("/v1/users/user/details")

注意,路径中的键需要用包起来。
设置首部:
client.R(). SetHeader("Content-Type", "application/json"). Get(...)

设置请求消息体:
client.R(). SetHeader("Content-Type", "application/json"). SetBody(`"name": "dj", "age":18`). Get(...)

消息体可以是多种类型:字符串,[]byte,对象,map[string]interface等。
设置携带Content-Length首部,resty自动计算:
client.R(). SetBody(UserName:"dj", Age:18). SetContentLength(true). Get(...)

有些网站需要先获取 token,然后才能访问它的 API。设置 token:
client.R(). SetAuthToken("youdontknow"). Get(...)

案例
最后,我们通过一个案例来将上面介绍的这些串起来。现在我们想通过 GitHub 提供的 API 获取组织的仓库信息,API 文档见文后链接。GitHub API 请求地址为https://api.github.com,获取仓库信息的请求格式如下:
GET /orgs/org/repos

我们还可以设置以下这些参数:
Go 每日一库之 resty

文章图片

  • accept:首部,这个必填,需要设置为application/vnd.github.v3+json
  • org:组织名,路径参数;
  • type:仓库类型,查询参数,例如public/private/forks(fork的仓库)等;
  • sort:仓库的排序规则,查询参数,例如created/updated/pushed/full_name等。默认按创建时间排序;
  • direction:升序asc或降序dsc,查询参数;
  • per_page:每页多少条目,最大 100,默认 30,查询参数;
  • page:当前请求第几页,与per_page一起做分页管理,默认 1,查询参数。
GitHub API 必须设置 token 才能访问。登录 GitHub 账号,点开右上角头像,选择Settings
Go 每日一库之 resty

文章图片

然后,选择Developer settings

Go 每日一库之 resty

文章图片

选择Personal access tokens,然后点击右上角的Generate new token

Go 每日一库之 resty

文章图片

填写 Note,表示 token 的用途,这个根据自己情况填写即可。下面复选框用于选择该 token 有哪些权限,这里不需要勾选:

Go 每日一库之 resty

文章图片

点击下面的Generate token按钮即可生成 token:

Go 每日一库之 resty

文章图片

注意,这个 token 只有现在能看见,关掉页面下次再进入就无法看到了。所以要保存好,另外不要用我的 token,测试完程序后我会删除 token????。

响应中的 JSON 格式数据如下所示:
Go 每日一库之 resty

文章图片

字段非常多,为了方便起见,我这里之处理几个字段:

type Repository struct IDint`json:"id"` NodeIDstring`json:"node_id"` Namestring`json:"name"` FullNamestring`json:"full_name"` Owner*Developer `json:"owner"` Privatebool`json:"private"` Descriptionstring`json:"description"` Forkbool`json:"fork"` Languagestring`json:"language"` ForksCountint`json:"forks_count"` StargazersCount int`json:"stargazers_count"` WatchersCountint`json:"watchers_count"` OpenIssuesCount int`json:"open_issues_count"`type Developer struct Loginstring `json:"login"` IDint`json:"id"` NodeIDstring `json:"node_id"` AvatarURLstring `json:"avatar_url"` GravatarID string `json:"gravatar_id"` Typestring `json:"type"` SiteAdminbool`json:"site_admin"`

然后使用resty设置路径参数,查询参数,首部,Token 等信息,然后发起请求:
func main() client := resty.New()var result []*Repository client.R(). SetAuthToken("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"). SetHeader("Accept", "application/vnd.github.v3+json"). SetQueryParams(map[string]string "per_page":"3", "page":"1", "sort":"created", "direction": "asc", ). SetPathParams(map[string]string "org": "golang", ). SetResult(& result). Get("https://api.github.com/orgs/org/repos")for i, repo := range result fmt.Printf("repo%d: name:%s stars:%d forks:%d\\n", i+1, repo.Name, repo.StargazersCount, repo.ForksCount)

上面程序拉取以创建时间升序排列的 3 个仓库:
$ go run main.go repo1: name:gddo stars:1097 forks:289 repo2: name:lint stars:3892 forks:518 repo3: name:glog stars:2738 forks:775

Trace 介绍完resty的主要功能之后,我们再来看看resty提供的一个辅助功能:trace。我们在请求对象上调用EnableTrace()方法启用 trace。启用 trace 可以记录请求的每一步的耗时和其他信息。resty支持链式调用,也就是说我们可以在一行中完成创建请求,启用 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.clientTraceif ct == nil return TraceInfoti := 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

运行输出:
$ go run main.go 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 消耗了近一半的时间。
总结 本文我介绍了 Go 语言一款非常方便易用的 HTTP Client 库。resty提供非常实用的,丰富的 API。链式调用,自动 Unmarshal,请求参数/路径设置这些功能非常方便好用,让我们的工作事半功倍。限于篇幅原因,很多高级特性未能一一介绍,如提交表单,上传文件等等等等。只能留待感兴趣的大家去探索了。
大家如果发现好玩、好用的 Go 语言库,欢迎到 Go 每日一库 GitHub 上提交 issue????
参考
  1. Go 每日一库 GitHub:https://github.com/darjun/go-daily-lib
  2. resty GitHub:github.com/go-resty/resty
  3. GitHub API:https://docs.github.com/en/rest/overview/resources-in-the-rest-api
我 我的博客:https://darjun.github.io
欢迎关注我的微信公众号【GoUpUp】,共同学习,一起进步~

    推荐阅读