美文网首页Go语言程序员
Sugar - Golang的声明式Http客户端

Sugar - Golang的声明式Http客户端

作者: 宇宙最强架构师 | 来源:发表于2018-02-08 17:31 被阅读379次

    Sugar是一个Go语言编写的声明式Http客户端,提供了一些优雅的接口,目的是减少冗余的拼装代码。

    没有比较就没有优越感,我们先来一段基于Go标准库的代码,逻辑很简单,通过github的接口查询仓库信息并对返回的JSON进行反序列化处理:

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

    这段代码真是太复杂了!一眼望去都是if err != nil。如此简单的逻辑竟然需要写这么多代码,并且这些代码几乎没有什么含金量!

    现在我们来看一下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)
        }
    }
    

    从程序员的角度我觉得这真是太酷了!

    那么如果服务器返回的是XML格式的数据我们要怎么办?

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

    你没看错,除了Read方法中的参数类型可能不一样,其它代码无需变动!

    如果我们想把请求和响应输出到日志怎么办?

    func init(){
        Use(Logger)
    }
    

    生命太短暂,做更有价值的事!

    项目传送门:Github

    以下是正式的文档:

    特性

    • 优雅的接口设计
    • 插件功能
    • 链式调用
    • 高度可定制

    下载

    dep ensure -add github.com/pojozhang/sugar
    

    使用

    首先导入包,为了看起来更简洁,此处用省略包名的方式导入。

    import . "github.com/pojozhang/sugar"
    

    一切就绪!

    发送请求

    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

    你可以任意组合参数。

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

    Apply

    Apply方法传入的参数会被应用到之后所有的请求中,可以使用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")
    

    以上两段代码是等价的。

    解析响应

    一个请求发送后会返回*Response类型的返回值,其中包含了一些有用的语法糖。

    Raw

    Raw()会返回一个*http.Response和一个error,就和Go自带的SDK一样(所以叫Raw)。

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

    ReadBytes

    ReadBytes()可以直接从返回的body读取字节切片。需要注意的是,该方法返回前会自动释放body资源。

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

    Read

    Read()方法通过注册在系统中的Decoder对返回值进行解析。
    以下两个例子是在不同的情况下分别解析成字符串或者JSON,解析过程对调用者来说是透明的。

    // 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负责把调用者传入参数组装成一个请求体。
    • Decoder负责把服务器返回的数据解析成一个结构体。
    • Plugin起到拦截器的作用。

    Encoder

    你可以通过实现Encoder接口来实现自己的编码器。

    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

    你可以实现Decoder接口来实现自己的解码器。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

    插件是一个特殊的组件,你可以在请求发送前或收到响应后进行一些额外的处理。

    // 内置Logger插件的实现
    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()
    })
    

    相关文章

      网友评论

        本文标题:Sugar - Golang的声明式Http客户端

        本文链接:https://www.haomeiwen.com/subject/eckttftx.html