gentleman
是一個功能齊全、插件驅動的 HTTP 客戶端。gentleman
以擴展性爲原則,能夠基於內置的或第三方插件建立具備豐富特性的、可複用的 HTTP 客戶端。相比標準庫net/http
,gentleman
更靈活、易用。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
目前有兩個版本v1
和v2
,v2
已經穩定,推薦使用,示例中使用的就是v2
。gentleman
的使用遵循下面的流程:json
gentleman.New()
建立一個 HTTP 客戶端cli
,此cli
對象可複用;cli.URL()
設置要請求的 URL 基礎地址;cli.Request()
建立一個請求對象req
;req.Path()
設置請求的路徑,基於前面設置的 URL;req.Header()
設置請求首部(Header
),上面代碼設置首部Client
爲gentleman
;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"}
因爲是隨機的,每次運行結果可能都不相同,status
爲success
表示運行成功,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
或請求對象req
的Use()
方法使用插件。區別在於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-91ac4c1477b8
。thecatapi
要求在請求首部中設置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
能夠很好的管理這個信息。咱們能夠基於上面代碼,給請求帶上參數page
和limit
使之分頁返回:
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
,咱們能夠傳入參數user
或book
組成完整的路徑。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
中有Request
和Response
等信息,能夠在發起請求前對請求進行一些操做以及得到響應時對響應進行一些操做。上面只是簡單地輸出信息。
使用gentleman
能夠實現靈活、便捷的 HTTP 客戶端,它提供了豐富的插件,用起來吧~
你們若是發現好玩、好用的 Go 語言庫,歡迎到 Go 每日一庫 GitHub 上提交 issue😄
我
-
歡迎關注個人微信公衆號【GoUpUp】,共同窗習,一塊兒進步~