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
//
Post("http://api.example.com/books", Xml{` `})
Post("http://api.example.com/books", X{` `})
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, Decoder 和 Plugin.
- 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()
})