Go 每日一庫之 gentleman

簡介

gentleman是一個功能齊全、插件驅動的 HTTP 客戶端。gentleman以擴展性爲原則,能夠基於內置的或第三方插件建立具備豐富特性的、可複用的 HTTP 客戶端。相比標準庫net/httpgentleman更靈活、易用。git

快速使用

先安裝:程序員

$ go get gopkg.in/h2non/gentleman.v2

後使用:github

package mainimport (  "fmt"  "gopkg.in/h2non/gentleman.v2")func main() {  cli := gentleman.New()  cli.URL("https://dog.ceo")  req := cli.Request()  req.Path("/api/breeds/image/random")  req.SetHeader("Client", "gentleman")  res, err := req.Send()  if err != nil {    fmt.Printf("Request error: %vn", err)    return  }  if !res.Ok {    fmt.Printf("Invalid server response: %dn", res.StatusCode)    return  }  fmt.Printf("Body: %s", res.String())}

gentleman目前有兩個版本v1v2v2已經穩定,推薦使用,示例中使用的就是v2gentleman的使用遵循下面的流程:json

  • 調用gentleman.New()建立一個 HTTP 客戶端clicli對象可複用
  • 調用cli.URL()設置要請求的 URL 基礎地址;
  • 調用cli.Request()建立一個請求對象req
  • 調用req.Path()設置請求的路徑,基於前面設置的 URL;
  • 調用req.Header()設置請求首部(Header),上面代碼設置首部Clientgentleman
  • 調用req.Send()發送請求,獲取響應對象res
  • 對響應對象res進行處理。

上面的測試 API 是我從public-apis找的。public-apis是 GitHub 上一個收集各類開放 API 的倉庫。本文後面部分的 API 也來自於這個倉庫。從https://dog.ceo咱們能夠獲取各類和相關的信息,上面請求的路徑/api/breeds/image/random將返回一個隨機品種的狗的圖片。運行結果:api

Body: {"message":"https://images.dog.ceo/breeds/malamute/n02110063_10567.jpg","status":"success"}

因爲是隨機的,每次運行結果可能都不相同,statussuccess表示運行成功,message對應的值爲圖片的 URL。感興趣本身在瀏覽器中打開返回的 URL,我獲取的圖片以下:瀏覽器

插件

gentleman中的特性不少都是經過插件來實現的。gentleman內置了不少經常使用的插件。若是要實現的特性沒法經過內置插件來完成,還有第三方插件可供選擇,固然還能夠自定義插件!gentleman的插件都是存放在plugins子目錄中的,下面介紹幾個經常使用的插件。微信

body

客戶端有時須要發送 JSON、XML 等格式的數據,body插件能夠很好地完成這個任務:cookie

package mainimport (  "fmt"  "gopkg.in/h2non/gentleman.v2"  "gopkg.in/h2non/gentleman.v2/plugins/body")func main() {  cli := gentleman.New()  cli.URL("http://httpbin.org/post")  data := map[string]string{"foo": "bar"}  cli.Use(body.JSON(data))  req := cli.Request()  req.Method("POST")  res, err := req.Send()  if err != nil {    fmt.Printf("Request error: %s\n", err)    return  }  if !res.Ok {    fmt.Printf("Invalid server response: %d\n", res.StatusCode)    return  }  fmt.Printf("Status: %d\n", res.StatusCode)  fmt.Printf("Body: %s", res.String())}

注意插件的導入方式:import "gopkg.in/h2non/gentleman.v2/plugins/body"app

調用客戶端對象cli或請求對象reqUse()方法使用插件。區別在於cli.Use()調用以後,全部經過該cli建立的請求對象都使用該插件,req.Use()只對該請求生效,在本例中使用req.Use(body.JSON(data))也是能夠的。上面使用body.JSON()插件,每次發送請求時,都將data轉爲 JSON 設置到請求體中,並設置相應的首部(Content-Type/Content-Length)。req.Method("POST")設置使用 POST 方法。本次請求使用的 URL http://httpbin.org/post會回顯請求的信息,看運行結果:dom

Status: 200Body: {  "args": {},   "data": "{\"foo\":\"bar\"}\n",   "files": {},   "form": {},   "headers": {    "Accept-Encoding": "gzip",     "Content-Length": "14",     "Content-Type": "application/json",     "Host": "httpbin.org",     "User-Agent": "gentleman/2.0.4",     "X-Amzn-Trace-Id": "Root=1-5e8dd0c7-ab423c10fb530deade846500"  },   "json": {    "foo": "bar"  },   "origin": "124.77.254.163",   "url": "http://httpbin.org/post"}

發送 XML 格式與上面的很是相似:

type User struct {  Name string `xml:"name"`  Age  int    `xml:"age"`}func main() {  cli := gentleman.New()  cli.URL("http://httpbin.org/post")  req := cli.Request()  req.Method("POST")  u := User{Name: "dj", Age: 18}  req.Use(body.XML(u))  // ...}

後半部分同樣的代碼我就省略了,運行結果:

Status: 200Body: {  "args": {},   "data": "<User><name>dj</name><age>18</age></User>",   "files": {},   "form": {},   "headers": {    "Accept-Encoding": "gzip",     "Content-Length": "41",     "Content-Type": "application/xml",     "Host": "httpbin.org",     "User-Agent": "gentleman/2.0.4",     "X-Amzn-Trace-Id": "Root=1-5e8dd339-830dba04536ceef247156746"  },   "json": null,   "origin": "222.64.16.70",   "url": "http://httpbin.org/post"}

header

header插件用於在發送請求前添加一些通用的首部,如 APIKey;或者刪除一些自動加上的首部,如User-Agent。通常header插件應用在cli對象上:

package mainimport (  "fmt"  "gopkg.in/h2non/gentleman.v2"  "gopkg.in/h2non/gentleman.v2/plugins/headers")func main() {  cli := gentleman.New()  cli.URL("https://api.thecatapi.com")  cli.Use(headers.Set("x-api-key", "479ce48d-db30-46a4-b1a0-91ac4c1477b8"))  cli.Use(headers.Del("User-Agent"))  req := cli.Request()  req.Path("/v1/breeds")  res, err := req.Send()  if err != nil {    fmt.Printf("Request error: %s\n", err)    return  }  if !res.Ok {    fmt.Printf("Invalid server response: %d\n", res.StatusCode)    return  }  fmt.Printf("Status: %d\n", res.StatusCode)  fmt.Printf("Body: %s", res.String())}

上面咱們使用了https://api.thecatapi.com,這個 API 能夠獲取的品種信息,支持返回所有品種,搜索,分頁等操做。API 使用須要申請 APIKey,我本身申請了一個479ce48d-db30-46a4-b1a0-91ac4c1477b8thecatapi要求在請求首部中設置x-api-key爲咱們申請到的 APIKey。

headers能夠很方便的實現這個功能,只須要在cli對象上設置一次便可。另外,gentleman會自動在請求中添加一個User-Agent首部,內容是gentleman的版本信息。細心的童鞋可能已經發現了,在上一節的輸出中有User-Agent: gentleman/2.0.4這個首部。在本例中,咱們使用header.Del()刪除這個首部。

輸出內容太多,我這裏就不貼了。

query

HTTP 請求一般會在 URL 的?後帶上查詢字符串(query string),gentleman的內置插件query能夠很好的管理這個信息。咱們能夠基於上面代碼,給請求帶上參數pagelimit使之分頁返回:

package mainimport (  "fmt"  "gopkg.in/h2non/gentleman.v2"  "gopkg.in/h2non/gentleman.v2/plugins/headers"  "gopkg.in/h2non/gentleman.v2/plugins/query")func main() {  cli := gentleman.New()  cli.URL("https://api.thecatapi.com")  cli.Use(headers.Set("x-api-key", "479ce48d-db30-46a4-b1a0-91ac4c1477b8"))  cli.Use(query.Set("attach_breed", "beng"))  cli.Use(query.Set("limit", "2"))  cli.Use(headers.Del("User-Agent"))  req := cli.Request()  req.Path("/v1/breeds")  req.Use(query.Set("page", "1"))  res, err := req.Send()  if err != nil {    fmt.Printf("Request error: %s\n", err)    return  }  if !res.Ok {    fmt.Printf("Invalid server response: %d\n", res.StatusCode)    return  }  fmt.Printf("Status: %d\n", res.StatusCode)  fmt.Printf("Body: %s", res.String())}

品種和每頁顯示數量最好仍是在cli對象中設置,每一個請求對象共用:

cli.Use(query.Set("attach_breed", "beng"))cli.Use(query.Set("limit", "2"))

當前請求的頁數在req對象上設置:

req.Use(query.Set("page", "1"))

其餘的代碼與上一個示例徹底同樣。除了設置query string,還能夠經過query.Del()刪除某個鍵值對。

url

路徑參數有些時候頗有用,由於咱們在開發中時常會碰到類似的路徑,只是中間某個部分不同,例如/info/user/1/info/book/1等。重複寫這些路徑不只很枯燥,並且容易出錯。因而,偷懶的程序員發明了路徑參數,形如/info/:class/1,咱們能夠傳入參數userbook組成完整的路徑。gentleman內置了插件url用來處理路徑參數問題:

package mainimport (  "fmt"  "os"  "gopkg.in/h2non/gentleman.v2"  "gopkg.in/h2non/gentleman.v2/plugins/headers"  "gopkg.in/h2non/gentleman.v2/plugins/url")func main() {  cli := gentleman.New()  cli.URL("https://api.thecatapi.com/")  cli.Use(headers.Set("x-api-key", "479ce48d-db30-46a4-b1a0-91ac4c1477b8"))  cli.Use(url.Path("/v1/:type"))  for _, arg := range os.Args[1:] {    req := cli.Request()    req.Use(url.Param("type", arg))    res, err := req.Send()    if err != nil {      fmt.Printf("Request error: %s\n", err)      return    }    if !res.Ok {      fmt.Printf("Invalid server response: %d\n", res.StatusCode)      return    }    fmt.Printf("Status: %d\n", res.StatusCode)    fmt.Printf("Body: %s\n", res.String())  }}

thecatapi除了能夠獲取貓的品種,還有用戶投票、各類分類信息。它們的請求路徑都差很少,/v1/breeds/v1/votes/v1/categories。咱們使用url簡化程序編寫。上面程序在客戶端對象cli上使用插件url.Path("/v1/:type"),調用url.Param("type", arg)用命令行中的參數分別替換type進行 HTTP 請求。運行程序:

$ go run main.go breeds votes categories

其餘

gentleman內置了將近 20 個插件,有身份認證相關的auth、有cookies、有壓縮相關的compression、有代理相關的proxy、有重定向相關的redirect、有超時相關的timeout、有重試的retry、有服務發現的consul等等等等。感興趣可自行去探索。

自定義

若是內置的和第三方的插件都不能知足咱們的需求,咱們還能夠自定義插件。自定義的插件須要實現下面的接口:

// src/gopkg.in/h2non/gentleman.v2/plugin/plugin.gotype Plugin interface {  Enable()  Disable()  Disabled() bool  Remove()  Removed() bool  Exec(string, *context.Context, context.Handler)}

Exec()方法在 HTTP 請求的各個生命週期都會調用,能夠在請求前添加一些首部、刪除查詢字符串,響應返回後進行一些處理等。

經過實現Plugin接口的方式實現插件比較繁瑣,且不少插件每每只關注生命週期的某個點,不用處理全部的生命週期事件。gentleman提供了一個Layer結構,能夠註冊某個生命週期的方法,同時提供NewRequestPlugin/NewResponsePlugin/NewErrorPlugin等便捷函數。

咱們如今來實現一個插件,在請求以前輸出一行信息,收到響應以後輸出一行信息:

package mainimport (  "fmt"  "gopkg.in/h2non/gentleman.v2"  c "gopkg.in/h2non/gentleman.v2/context"  "gopkg.in/h2non/gentleman.v2/plugin")func main() {  cli := gentleman.New()  cli.URL("https://httpbin.org")  cli.Use(plugin.NewRequestPlugin(func(ctx *c.Context, h c.Handler) {    fmt.Println("request")    h.Next(ctx)  }))  cli.Use(plugin.NewResponsePlugin(func(ctx *c.Context, h c.Handler) {    fmt.Println("response")    h.Next(ctx)  }))  req := cli.Request()  req.Path("/headers")  res, err := req.Send()  if err != nil {    fmt.Printf("Request error: %s\n", err)    return  }  if !res.Ok {    fmt.Printf("Invalid server response: %d\n", res.StatusCode)    return  }  fmt.Printf("Status: %d\n", res.StatusCode)  fmt.Printf("Body: %s", res.String())}

因爲NewRequestPlugin/NewResponsePlugin這些便利函數,咱們只須要實現一個類型爲func(ctx *c.Context, h c.Handler)的函數便可,在ctx中有RequestResponse等信息,能夠在發起請求前對請求進行一些操做以及得到響應時對響應進行一些操做。上面只是簡單地輸出信息。

總結

使用gentleman能夠實現靈活、便捷的 HTTP 客戶端,它提供了豐富的插件,用起來吧~

你們若是發現好玩、好用的 Go 語言庫,歡迎到 Go 每日一庫 GitHub 上提交 issue😄

參考

  1. gentleman GitHub:https://github.com/h2non/gentleman
  2. Go 每日一庫 GitHub:https://github.com/darjun/go-daily-lib


-

個人博客:https://darjun.github.io

歡迎關注個人微信公衆號【GoUpUp】,共同窗習,一塊兒進步~

相關文章
相關標籤/搜索