likes
comments
collection
share

快刀斩乱麻:一个超级清凉的 Http Restful Client 的 Golang 实现

作者站长头像
站长
· 阅读数 9

我是 LEE,老李,一个在 IT 行业摸爬滚打 17 年的技术老兵。

事件背景

做为了一个“码字”的小伙伴,经常会使用 Restful Client 工具来访问接口,我现在就经常在用 go-resty 这个项目,经过一段时间用下来感觉还不错,但是,但是最近内部业务组做了一些调整,导致 go-resty 这个项目不能满足我们的需求了,然后做代码兼容非常头疼。所以想用最小的代价,想着实现一个超级清凉的 Http Restful Client 的 Golang 实现。

业务调整内容主要下面几个方面:

  1. 接口特状态码的调整,不会统一的返回 200,而是根据业务情况返回不同的状态码,比如 400、401、403、404、500 等等。
  2. 接口返回的数据格式调整,不会统一的返回 json,而是根据业务情况返回不同的数据格式,比如 json、xml、yaml、protobuf 等等。

TIPS: 别一说到 protobuf 就马上联想到 gRPC,其实 protobuf 也可以用在 HTTP 协议中,只不过 protobuf 是一种编解码的方式,而 gRPC 是一种通信标准,这两者是可以分开使用的。

可是的可是,go-resty 只支持 json、xml 两种解码,导致我们的业务无法兼容,因为现在很多项目都开始使用 pb 作为项目 api 标准,所以我们的项目也需要兼容 pb。 go-resty 就不能使用默认的 SetResult 的方法来解码了,同时 go-resty 的SetResult的方法只有在 Http StatusCode 在 200-299 之间才会调用,如果不在这个范围内,就会返回错误,这样就导致我们的业务无法兼容。

设置解码器

// Example of registering json-iterator
import jsoniter "github.com/json-iterator/go"

json := jsoniter.ConfigCompatibleWithStandardLibrary

client := resty.New().
    SetJSONMarshaler(json.Marshal).
    SetJSONUnmarshaler(json.Unmarshal)

// similarly user could do for XML too with -
client.SetXMLMarshaler(xml.Marshal).
    SetXMLUnmarshaler(xml.Unmarshal)

SetResult 的使用示例:

// Create a Resty Client
client := resty.New()

// POST JSON string
// No need to set content type, if you have client level setting
resp, err := client.R().
      SetHeader("Content-Type", "application/json").
      SetBody(`{"username":"testuser", "password":"testpass"}`).
      SetResult(&AuthSuccess{}).    // or SetResult(AuthSuccess{}).
      Post("https://myapp.com/login")

那我需要什么?

  1. 需要支持自定义的解码器,比如 json、xml、yaml、pb 等等。
  2. 需要支持自定义的状态码,比如 400、401、403、404、500 等等。
  3. 包装不要那么深入,能够像 Builder 模式一样,链式调用,这样代码看起来更清晰。

项目简介

Hasaki github.com/lxzan/hasak…

快刀斩乱麻:一个超级清凉的 Http Restful Client 的 Golang 实现

自己犹豫了一段时间,到底是不是要自己动手开撸。最后还是 lxzan 老兄靠谱,在他 Github 仓库的项目中找到了一个项目,叫做 Hasaki,这个项目就是一个 Golang Restful Client 的实现。

本着打不过就加入的原则,我有幸加入到这个项目中,一起来完善这个项目,让它更加完善。

在随后的一些项目中开始用 Hasaki 替换 go-resty 了,明显感觉之前的提到的一些限制没有了,是挺简单的,如果你愿意的话你都可以直接访问 Http Request/Response 对象,这样就可以在某种程度上把 Hasaki 当做一个普通的 Http Client 来使用了。 甚至真多地方都跟直接使用 net/httphttp.Client 一样,只不过 Hasaki 做了一些封装,让代码看起来更清晰,更易于维护。

使用举例

感觉我好像在吹这个项目有多好,其实我只是想说,只是 Hasaki 这个项目很简单,很清晰,很容易看懂,不想再花太多时间在复杂的代码和逻辑中,不是 go-resty 不好用,而是 Hasaki 更有性价比。难道早点下班回家,不香?

废话不多说,直接上代码,看看 Hasaki 是怎么使用的。

type Req struct {
    Q    string `json:"q"`
    Page int    `json:"page"`
}

type Resp struct {
    Total int `json:"total"`
}

data := Resp{}

err := hasaki.
    Post("https://api.example.com/search").
    Send(&Req{Q: "golang", Page: 1}).
    BindJSON(&data)

if err != nil {
    log.Printf("%+v", err)
    return
}

log.Printf("%+v", data)

是不是超级简单,而且还支持自定义解码器,直接支持 protobuf 了。 当然你可以根据自己实际情况开发直接专属解码,绑定到 Hasaki 中。

具体的实现方式可以参考项目中 github.com/lxzan/hasak… 中的代码,这里就不再赘述了,下面会有章节讲解。 非常非常的简单就一个文件,依葫芦画瓢就可以了。

下面代码是使用 protobuf 的一个样例,实际使用的时候,还是要使用 proto3 文件生成的代码。

type Req struct {
    Q    string `json:"q"`
    Page int    `json:"page"`
}

// 这里是一个样例,实际使用的时候,要使用 proto3 文件生成的代码
type Resp struct {
    state         protoimpl.MessageState
    sizeCache     protoimpl.SizeCache
    unknownFields protoimpl.UnknownFields

    Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
    Age  int32  `protobuf:"varint,2,opt,name=age,proto3" json:"age,omitempty"`
}

data := Resp{}

err := hasaki.
    Post("https://api.example.com/search").
    Send(&Req{Q: "golang", Page: 1}).
    Bind(&data, pd.Decode)

if err != nil {
    log.Printf("%+v", err)
    return
}

log.Printf("%+v", data)

这种一竿子打到底的方式,用几次就会上瘾。

不过 Hasaki 在处理请求的时候还有一个问题,就是一旦使用了 BindBindJSON 方法,就会导致 Hasaki 无法处理无发直接获得 StatusCode 了,这个非常不友好,可能 Respose 的代码需要优化下。

举例一个栗子:

如果我需要获取 StatusCode,就需要就要打断 BindJSON 的链式调用,然后再去获取 StatusCode,这样就会导致代码看起来不太优雅了。

type Req struct {
    Q    string `json:"q"`
    Page int    `json:"page"`
}

type Resp struct {
    Total int `json:"total"`
}

data := Resp{}

resp := hasaki.
    Post("https://api.example.com/search").
    Send(&Req{Q: "golang", Page: 1})

// 这里打断了 BindJSON 的链式调用
log.Printf("%+v", resp.StatusCode())

err := resp.BindJSON(&data)

if err != nil {
    log.Printf("%+v", err)
    return
}

log.Printf("%+v", data)

后面能不能跟 lxzan 兄弟沟通下,看看能不能优化下这个问题。

关于 Middleware 的使用,我觉得也是一个很有意思的点,他没有那么多入口,就只有 2 个标记点:

  1. BeforeRequest:在请求之前执行。 WithBefore 方法
  2. AfterResponse:在响应之后执行。 WithAfter 方法

举例一个栗子:

before := hasaki.WithBefore(func(ctx context.Context, request *http.Request) (context.Context, error) {
    return context.WithValue(ctx, "t0", time.Now()), nil
})

after := hasaki.WithAfter(func(ctx context.Context, response *http.Response) (context.Context, error) {
    t0 := ctx.Value("t0").(time.Time)
    log.Printf("latency=%s", time.Since(t0).String())
    return ctx, nil
})

var url = "https://api.github.com/search/repositories"
cli, _ := hasaki.NewClient(before, after)
cli.Get(url).Send(nil)

看到这里不尽想唠叨一句:对面的 go-resty 都获得延迟的方法和属性了,这边还要自己写,这个是不是有点不太友好。通过跟 lxzan 兄沟通后面才知道,这个是有意为之的, Hasaki 没有直接提供这个方法,而是让用户自己去实现,这样可以更加灵活,这样也可以避免 Hasaki 代码过于臃肿和无限可能。

代码解析

我觉得最优意思的是将编解码器作为一个 plugin 的方式,而不是直接写在代码中,这样就方便用户可以通过 http 协议传递任何自己想要的编解码了,而不会受到 Hasaki 的限制。

这里用一个例子,简单介绍下 Hasaki 的编解码插件怎么写,这里以编解码 yaml 为例,其他的编解码器也是类似的。

yaml 解码器

package yaml

import (
	"bytes"
	"github.com/lxzan/hasaki"
	"github.com/lxzan/hasaki/internal"
	"github.com/pkg/errors"
	"github.com/valyala/bytebufferpool"
	"gopkg.in/yaml.v3"
	"io"
)

var Encoder = new(encoder)  // 这里是一个全局变量,SetEncoder 方法中会用到,将编码器注册到 Hasaki 中

type encoder struct{}

func (c encoder) Encode(v any) (io.Reader, error) { // 这个提供 Request 中将 Body 中的内容编码成 io.Reader
	if v == nil {
		return nil, nil
	}
	w := bytebufferpool.Get() // 这里使用了 bytebufferpool 来提高性能, 也可以使用 bytes.Buffer,但是性能会差一些
	err := yaml.NewEncoder(w).Encode(v)
	r := &internal.CloserWrapper{B: w, R: bytes.NewReader(w.B)} // 这里使用了 internal.CloserWrapper 来包装一下,方便后面使用
	return r, errors.WithStack(err)
}

func (c encoder) ContentType() string { // 返回编码器的 ContentType,表示在 http 请求的时候相应的 Content-Type
	return hasaki.MimeYaml
}

// 这个解码器是用来解析 Response 中的 Body 的,将 Body 中的内容解析成用户指定的对象
func Decode(r io.Reader, v any) error {
	return errors.WithStack(yaml.NewDecoder(r).Decode(v))
}

看到这里实现一个编解码器是不是很简单,只需要实现两个部分就可以了。编解码器在后期就可以直接使用,也就是说我可以在 Hasaki 中注册任何我想要的编解码器,实现任何我想要的功能了。 点赞 !!!!

总结

通过一段时间使用,同时参与这个项目的过程中,我觉得这个项目还是挺不错的,代码很简单,很清晰,很容易看懂。从骨子透着一股子的简单,不过度封装,这也是我喜欢的风格。不要太复杂,太复杂的东西,我都不太喜欢。当然 Hasaki 这个项目还在孵化过程中,如果看到这里的小伙伴想参与或者贡献代码,可以直接在项目中开 Issue 或者 PR,我想 lxzan 都会很乐意接受你的贡献和反馈。