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 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使用比較簡單。

上面程序中我們獲取了:

運行程序輸出的響應基本信息:

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 數據,格式如下:

接下來,我們定義結構,然後使用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(").
  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

我們還可以設置以下這些參數:

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())

我們可以獲取以下信息:

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】,共同學習,一起進步~

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/ye9hlsGP9q-CkTCChZOY2g