resty
是 Go 語言的一個 HTTP client 庫。resty
功能強大,特性豐富。它支持幾乎全部的 HTTP 方法(GET/POST/PUT/DELETE/OPTION/HEAD/PATCH等),並提供了簡單易用的 API。html
本文代碼使用 Go Modules。vue
建立目錄並初始化:node
$ mkdir resty && cd resty $ go mod init github.com/darjun/go-daily-lib/resty
安裝resty
庫:git
$ go get -u github.com/go-resty/resty/v2
下面咱們來獲取百度首頁信息:github
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
使用比較簡單。golang
resty.New()
建立一個client
對象;client
對象的R()
方法建立一個請求對象;Get()/Post()
等方法,傳入參數 URL,就能夠向對應的 URL 發送 HTTP 請求了。返回一個響應對象;上面程序中咱們獲取了:ajax
StatusCode()
:狀態碼,如 200;Status()
:狀態碼和狀態信息,如 200 OK;Proto()
:協議,如 HTTP/1.1;Time()
:從發送請求到收到響應的時間;ReceivedAt()
:接收到響應的時刻;Size()
:響應大小;Header()
:響應首部信息,以http.Header
類型返回,即map[string][]string
;Cookies()
:服務器經過Set-Cookie
首部設置的 cookie 信息。運行程序輸出的響應基本信息:chrome
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
首部信息:json
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 部分:api
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
如今不少網站提供 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
介紹完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😄
歡迎關注個人微信公衆號【GoUpUp】,共同窗習,一塊兒進步~