关于golang:Go-每日一库之-resty

113次阅读

共计 11095 个字符,预计需要花费 28 分钟才能阅读完成。

简介

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 main

import (
  "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 = 
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 数据,格局如下:

接下来,咱们定义构造,而后应用 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.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 的详细信息:

感兴趣可自行用 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.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.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.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】,独特学习,一起提高~

正文完
 0