专栏首页GoUpUpGo 每日一库之 gentleman

Go 每日一库之 gentleman

简介

gentleman是一个功能齐全、插件驱动的 HTTP 客户端。gentleman以扩展性为原则,可以基于内置的或第三方插件创建具有丰富特性的、可复用的 HTTP 客户端。相比标准库net/httpgentleman更灵活、易用。

快速使用

先安装:

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

后使用:

package main

import (
  "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的使用遵循下面的流程:

  • 调用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将返回一个随机品种的狗的图片。运行结果:

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插件可以很好地完成这个任务:

package main

import (
  "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"

调用客户端对象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会回显请求的信息,看运行结果:

Status: 200
Body: {
  "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: 200
Body: {
  "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插件用于在发送请求前添加一些通用的首部,如 APIKey;或者删除一些自动加上的首部,如User-Agent。一般header插件应用在cli对象上:

package main

import (
  "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 main

import (
  "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 main

import (
  "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.go
type Plugin interface {
  Enable()
  Disable()
  Disabled() bool
  Remove()
  Removed() bool
  Exec(string, *context.Context, context.Handler)
}

Exec()方法在 HTTP 请求的各个生命周期都会调用,可以在请求前添加一些首部、删除查询字符串,响应返回后进行一些处理等。

通过实现Plugin接口的方式实现插件比较繁琐,且很多插件往往只关注生命周期的某个点,不用处理所有的生命周期事件。gentleman提供了一个Layer结构,可以注册某个生命周期的方法,同时提供NewRequestPlugin/NewResponsePlugin/NewErrorPlugin等便捷函数。

我们现在来实现一个插件,在请求之前输出一行信息,收到响应之后输出一行信息:

package main

import (
  "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

本文分享自微信公众号 - GoUpUp(GoUp-Up),作者:dj

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-04-08

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Go 每日一库之 cli

    cli是一个用于构建命令行程序的库。我们之前也介绍过一个用于构建命令行程序的库cobra。在功能上来说两者差不多,cobra的优势是提供了一个脚手架,方便开发。...

    用户7731323
  • Go 每日一库之 sjson

    在上一篇文章中我们介绍了如何使用gjson快速读取 JSON 串中的值。为了内容的完整性,今天我们介绍一下如何使用sjson快速设置 JSON 串中的值。

    用户7731323
  • Go 每日一库之 gjson

    之前我们介绍过gojsonq,可以方便地从一个 JSON 串中读取值。同时它也支持各种查询、汇总统计等功能。今天我们再介绍一个类似的库gjson。在上一篇文章G...

    用户7731323
  • Linux sort命令简介

    用sort对文件排序,发现这个命令比想象中要复杂和强大,仔细研究了一下文档,记录一下。

    猿哥
  • 关于MySQL的知识点与面试常见问题都在这里

    Java面试通关手册(Java学习指南,欢迎Star,会一直完善下去,欢迎建议和指导):https://github.com/Snailclimb/Java_G...

    用户2164320
  • 代码实现 WordPress 反垃圾评论功能

    垃圾评论,垃圾评论,你是哥心中的“恨”。每次打开后台看到上面工具栏的评论气泡出现了数字(表示有评论),打开一看却是什么“儿童服装”……除了WordPress 官...

    Jeff
  • 2018 AI技术、硬件与应用的全面综述:机器学习如何进化成AI

    【导读】普度大学机器学习、软硬件专家Eugenio Culurciello,在其主页分享了一篇博文,详细描述了自己对机器学习、深度神经网络、人工智能的个人见解。...

    WZEARW
  • 人工智能在空调焊接产线中的应用

    曾在 52CV 发表 “最新图文识别技术综述”,研究领域涉及图像、语音、文本信号处理和机器人等,身处传统产业领域,致力于AI技术在工业生产中的落地开花。

    CV君
  • Linux删除重复行

    第一,用sort+uniq,注意,单纯uniq是不行的。 sort -n test.txt | uniq

    阳光岛主
  • 实用工具SDelete

    安全地删除没有任何特殊属性的文件相对而言简单而直接:安全删除程序使用安全删除模式简单地覆盖文件。较为复杂的是安全地删除 Windows NT/2K 压缩、加密和...

    张善友

扫码关注云+社区

领取腾讯云代金券