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

我是 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 之间才会调用,如果不在这个范围内,就会返回错误,这样就导致我们的业务无法兼容。

设置解码器

go 复制代码
// 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 的使用示例:

go 复制代码
// 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...

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

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

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

使用举例

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

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

go 复制代码
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 文件生成的代码。

go 复制代码
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,这样就会导致代码看起来不太优雅了。

go 复制代码
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 方法

举例一个栗子:

go 复制代码
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 解码器

go 复制代码
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 都会很乐意接受你的贡献和反馈。

相关推荐
安的列斯凯奇5 小时前
SpringBoot篇 单元测试 理论篇
spring boot·后端·单元测试
架构文摘JGWZ5 小时前
FastJson很快,有什么用?
后端·学习
BinaryBardC5 小时前
Swift语言的网络编程
开发语言·后端·golang
邓熙榆6 小时前
Haskell语言的正则表达式
开发语言·后端·golang
专职8 小时前
spring boot中实现手动分页
java·spring boot·后端
Ciderw9 小时前
Go中的三种锁
开发语言·c++·后端·golang·互斥锁·
m0_748246359 小时前
SpringBoot返回文件让前端下载的几种方式
前端·spring boot·后端
m0_748230449 小时前
创建一个Spring Boot项目
java·spring boot·后端
卿着飞翔9 小时前
Java面试题2025-Mysql
java·spring boot·后端
C++小厨神9 小时前
C#语言的学习路线
开发语言·后端·golang