Sugar - Golang的聲明式Http客戶端

Sugar是一個(gè)Go語言編寫的聲明式Http客戶端,提供了一些優(yōu)雅的接口,目的是減少冗余的拼裝代碼。

沒有比較就沒有優(yōu)越感,我們先來一段基于Go標(biāo)準(zhǔn)庫的代碼,邏輯很簡單,通過github的接口查詢倉庫信息并對返回的JSON進(jìn)行反序列化處理:

func main() {
    resp, err := http.Get(fmt.Sprintf("https://api.github.com/repos/%s/%s", "pojozhang", "sugar"))
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close()

    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        panic(err)
    }

    var result map[string]interface{}
    if err := json.Unmarshal(body, &result); err != nil {
        panic(err)
    }
}

這段代碼真是太復(fù)雜了!一眼望去都是if err != nil。如此簡單的邏輯竟然需要寫這么多代碼,并且這些代碼幾乎沒有什么含金量!

現(xiàn)在我們來看一下Sugar的解決方案:

func main() {
    var result map[string]interface{}
    _, err := Get("https://api.github.com/repos/:owner/:repo", P{"owner": "pojozhang", "repo": "sugar"}).Read(&result)
    if err != nil {
        panic(err)
    }
}

從程序員的角度我覺得這真是太酷了!

那么如果服務(wù)器返回的是XML格式的數(shù)據(jù)我們要怎么辦?

_, err := Get("https://api.github.com/repos/:owner/:repo", P{"owner": "pojozhang", "repo": "sugar"}).Read(&result)

你沒看錯,除了Read方法中的參數(shù)類型可能不一樣,其它代碼無需變動!

如果我們想把請求和響應(yīng)輸出到日志怎么辦?

func init(){
    Use(Logger)
}

生命太短暫,做更有價(jià)值的事!

項(xiàng)目傳送門:Github

以下是正式的文檔:

特性

  • 優(yōu)雅的接口設(shè)計(jì)
  • 插件功能
  • 鏈?zhǔn)秸{(diào)用
  • 高度可定制

下載

dep ensure -add github.com/pojozhang/sugar

使用

首先導(dǎo)入包,為了看起來更簡潔,此處用省略包名的方式導(dǎo)入。

import . "github.com/pojozhang/sugar"

一切就緒!

發(fā)送請求

Plain Text

// POST /books HTTP/1.1
// Host: api.example.com
// Content-Type: text/plain
Post("http://api.example.com/books", "bookA")

Path

// GET /books/123 HTTP/1.1
// Host: api.example.com
Get("http://api.example.com/books/:id", Path{"id": 123})
Get("http://api.example.com/books/:id", P{"id": 123})

Query

// GET /books?name=bookA HTTP/1.1
// Host: api.example.com
Get("http://api.example.com/books", Query{"name": "bookA"})
Get("http://api.example.com/books", Q{"name": "bookA"})

// list
// GET /books?name=bookA&name=bookB HTTP/1.1
// Host: api.example.com
Get("http://api.example.com/books", Query{"name": List{"bookA", "bookB"}})
Get("http://api.example.com/books", Q{"name": L{"bookA", "bookB"}})

Cookie

// GET /books HTTP/1.1
// Host: api.example.com
// Cookie: name=bookA
Get("http://api.example.com/books", Cookie{"name": "bookA"})
Get("http://api.example.com/books", C{"name": "bookA"})

Header

// GET /books HTTP/1.1
// Host: api.example.com
// Name: bookA
Get("http://api.example.com/books", Header{"name": "bookA"})
Get("http://api.example.com/books", H{"name": "bookA"})

Json

// POST /books HTTP/1.1
// Host: api.example.com
// Content-Type: application/json;charset=UTF-8
// {"name":"bookA"}
Post("http://api.example.com/books", Json{`{"name":"bookA"}`})
Post("http://api.example.com/books", J{`{"name":"bookA"}`})

// map
Post("http://api.example.com/books", Json{Map{"name": "bookA"}})
Post("http://api.example.com/books", J{M{"name": "bookA"}})

// list
Post("http://api.example.com/books", Json{List{Map{"name": "bookA"}}})
Post("http://api.example.com/books", J{L{M{"name": "bookA"}}})

Xml

// POST /books HTTP/1.1
// Host: api.example.com
// Authorization: Basic dXNlcjpwYXNzd29yZA==
// Content-Type: application/xml; charset=UTF-8
// <book name="bookA"></book>
Post("http://api.example.com/books", Xml{`<book name="bookA"></book>`})
Post("http://api.example.com/books", X{`<book name="bookA"></book>`})

Form

// POST /books HTTP/1.1
// Host: api.example.com
// Content-Type: application/x-www-form-urlencoded
Post("http://api.example.com/books", Form{"name": "bookA"})
Post("http://api.example.com/books", F{"name": "bookA"})

// list
Post("http://api.example.com/books", Form{"name": List{"bookA", "bookB"}})
Post("http://api.example.com/books", F{"name": L{"bookA", "bookB"}})

Basic Auth

// DELETE /books HTTP/1.1
// Host: api.example.com
// Authorization: Basic dXNlcjpwYXNzd29yZA==
Delete("http://api.example.com/books", User{"user", "password"})
Delete("http://api.example.com/books", U{"user", "password"})

Multipart

// POST /books HTTP/1.1
// Host: api.example.com
// Content-Type: multipart/form-data; boundary=19b8acc2469f1914a24fc6e0152aac72f1f92b6f5104b57477262816ab0f
//
// --19b8acc2469f1914a24fc6e0152aac72f1f92b6f5104b57477262816ab0f
// Content-Disposition: form-data; name="name"
//
// bookA
// --19b8acc2469f1914a24fc6e0152aac72f1f92b6f5104b57477262816ab0f
// Content-Disposition: form-data; name="file"; filename="text"
// Content-Type: application/octet-stream
//
// hello sugar!
// --19b8acc2469f1914a24fc6e0152aac72f1f92b6f5104b57477262816ab0f--
f, _ := os.Open("text")
Post("http://api.example.com/books", MultiPart{"name": "bookA", "file": f})
Post("http://api.example.com/books", MP{"name": "bookA", "file": f})

Mix

你可以任意組合參數(shù)。

Patch("http://api.example.com/books/:id", Path{"id": 123}, Json{`{"name":"bookA"}`}, User{"user", "password"})

Apply

Apply方法傳入的參數(shù)會被應(yīng)用到之后所有的請求中,可以使用Reset()方法重置。

Apply(User{"user", "password"})
Get("http://api.example.com/books")
Get("http://api.example.com/books")
Reset()
Get("http://api.example.com/books")
Get("http://api.example.com/books", User{"user", "password"})
Get("http://api.example.com/books", User{"user", "password"})
Get("http://api.example.com/books")

以上兩段代碼是等價(jià)的。

解析響應(yīng)

一個(gè)請求發(fā)送后會返回*Response類型的返回值,其中包含了一些有用的語法糖。

Raw

Raw()會返回一個(gè)*http.Response和一個(gè)error,就和Go自帶的SDK一樣(所以叫Raw)。

resp, err := Post("http://api.example.com/books", "bookA").Raw()
...

ReadBytes

ReadBytes()可以直接從返回的body讀取字節(jié)切片。需要注意的是,該方法返回前會自動釋放body資源。

bytes, resp, err := Get("http://api.example.com/books").ReadBytes()
...

Read

Read()方法通過注冊在系統(tǒng)中的Decoder對返回值進(jìn)行解析。
以下兩個(gè)例子是在不同的情況下分別解析成字符串或者JSON,解析過程對調(diào)用者來說是透明的。

// plain text
var text = new(string)
resp, err := Get("http://api.example.com/text").Read(text)

// json
var books []book
resp, err := Get("http://api.example.com/json").Read(&books)

文件下載 (@Since v2.1.0)

我們也可以通過Read()方法下載文件。

f,_ := os.Create("tmp.png")
defer f.Close()
resp, err := Get("http://api.example.com/logo.png").Read(f)

自定義

Sugar中有三大組件 Encoder, DecoderPlugin.

  • Encoder負(fù)責(zé)把調(diào)用者傳入?yún)?shù)組裝成一個(gè)請求體。
  • Decoder負(fù)責(zé)把服務(wù)器返回的數(shù)據(jù)解析成一個(gè)結(jié)構(gòu)體。
  • Plugin起到攔截器的作用。

Encoder

你可以通過實(shí)現(xiàn)Encoder接口來實(shí)現(xiàn)自己的編碼器。

type MyEncoder struct {
}

func (r *MyEncoder) Encode(context *RequestContext, chain *EncoderChain) error {
    myParams, ok := context.Param.(MyParam)
    if !ok {
    return chain.Next()
    }
    ...
    req := context.Request
    ...
    return nil
}

RegisterEncoders(&MyEncoder{})

Get("http://api.example.com/books", MyParam{})

Decoder

你可以實(shí)現(xiàn)Decoder接口來實(shí)現(xiàn)自己的解碼器。Read()方法會使用解碼器去解析返回值。

type MyDecoder struct {
}

func (d *MyDecoder) Decode(context *ResponseContext, chain *DecoderChain) error {
    // decode data from body if a content type named `my-content-type` is set in header
    for _, contentType := range context.Response.Header[ContentType] {
    if strings.Contains(strings.ToLower(contentType), "my-content-type") {
        body, err := ioutil.ReadAll(context.Response.Body)
        if err != nil {
        return err
        }
        json.Unmarshal(body, context.Out)
        ...
        return nil
    }
    }
    return chain.Next()
}

RegisterDecoders(&MyDecoder{})

Plugin

插件是一個(gè)特殊的組件,你可以在請求發(fā)送前或收到響應(yīng)后進(jìn)行一些額外的處理。

// 內(nèi)置Logger插件的實(shí)現(xiàn)
Use(func(c *Context) error {
    b, _ := httputil.DumpRequest(c.Request, true)
    log.Println(string(b))
    defer func() {
        if c.Response != nil {
        b, _ := httputil.DumpResponse(c.Response, true)
        log.Println(string(b))
    }
    }()
    return c.Next()
})
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,697評論 19 139
  • Spring Boot 參考指南 介紹 轉(zhuǎn)載自:https://www.gitbook.com/book/qbgb...
    毛宇鵬閱讀 47,288評論 6 342
  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語法,類相關(guān)的語法,內(nèi)部類的語法,繼承相關(guān)的語法,異常的語法,線程的語...
    子非魚_t_閱讀 34,853評論 18 399
  • 又來了!這孩子我真是被他氣到崩潰,寫作業(yè)不是問題,問題是檢查作業(yè)。只要提到“檢查”這個(gè)字眼,他瞬間炸毛!眼淚攻勢夾...
    溫暖李花開閱讀 379評論 0 0
  • 來自得到古典的《超級個(gè)體》1-1 舊的職業(yè)價(jià)值坐標(biāo)系在松動 新坐標(biāo)體系:圈子、能力與特色 ...
    Lisa的小世界閱讀 251評論 0 1

友情鏈接更多精彩內(nèi)容