简介

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: 200Status: 200 OKProto: HTTP/1.1Time: 415.774352msReceived At: 2021-06-26 11:42:45.307157 +0800 CST m=+0.416547795Size: 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=1cookie1: name:BIDUPSID value:BF2EE47AAAF7A20C6971F1E897ABDD43cookie2: name:PSTM value:1624678965cookie3: name:BAIDUID value:BF2EE47AAAF7A20C716E90B86906D6B0:FG=1cookie4: name:BDSVRTM value:0cookie5: name:BD_HOME value:1cookie6: 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 数据,格局如下:

接下来,咱们定义构造,而后应用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 到传入的对象中。这里设置申请信息时应用链式调用的形式,即在一行中实现多个设置

运行:

$ go run main.go4040 librariesfirst 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 的详细信息:

感兴趣可自行用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(User{Name:"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

咱们还能够设置以下这些参数:

  • 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

而后,抉择Developer settings

抉择Personal access tokens,而后点击右上角的Generate new token

填写 Note,示意 token 的用处,这个依据本人状况填写即可。上面复选框用于抉择该 token 有哪些权限,这里不须要勾选:

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

留神,这个 token 只有当初能看见,关掉页面下次再进入就无奈看到了。所以要保留好,另外不要用我的 token,测试完程序后我会删除 token。

响应中的 JSON 格局数据如下所示:

字段十分多,为了不便起见,我这里之解决几个字段:

type Repository struct {  ID              int        `json:"id"`  NodeID          string     `json:"node_id"`  Name            string     `json:"name"`  FullName        string     `json:"full_name"`  Owner           *Developer `json:"owner"`  Private         bool       `json:"private"`  Description     string     `json:"description"`  Fork            bool       `json:"fork"`  Language        string     `json:"language"`  ForksCount      int        `json:"forks_count"`  StargazersCount int        `json:"stargazers_count"`  WatchersCount   int        `json:"watchers_count"`  OpenIssuesCount int        `json:"open_issues_count"`}type Developer struct {  Login      string `json:"login"`  ID         int    `json:"id"`  NodeID     string `json:"node_id"`  AvatarURL  string `json:"avatar_url"`  GravatarID string `json:"gravatar_id"`  Type       string `json:"type"`  SiteAdmin  bool   `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.gorepo1: name:gddo stars:1097 forks:289repo2: name:lint stars:3892 forks:518repo3: 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.gotype 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.gofunc (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.gofunc (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}

运行输入:

$ go run main.goRequest Trace Info:DNSLookup: 2.815171msConnTime: 941.635171msTCPConnTime: 269.069692msTLSHandshake: 669.276011msServerTime: 274.623991msResponseTime: 112.216µsTotalTime: 1.216276906sIsConnReused: falseIsConnWasIdle: falseConnIdleTime: 0sRequestAttempt: 1RemoteAddr: 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】,独特学习,一起提高~