我是 LEE,老李,一个在 IT 行业摸爬滚打 17 年的技术老兵。
事件背景
做为了一个"码字"的小伙伴,经常会使用 Restful Client 工具来访问接口,我现在就经常在用 go-resty 这个项目,经过一段时间用下来感觉还不错,但是,但是最近内部业务组做了一些调整,导致 go-resty 这个项目不能满足我们的需求了,然后做代码兼容非常头疼。所以想用最小的代价,想着实现一个超级清凉的 Http Restful Client 的 Golang 实现。
业务调整内容主要下面几个方面:
- 接口特状态码的调整,不会统一的返回 200,而是根据业务情况返回不同的状态码,比如 400、401、403、404、500 等等。
- 接口返回的数据格式调整,不会统一的返回 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")
那我需要什么?
- 需要支持自定义的解码器,比如 json、xml、yaml、pb 等等。
- 需要支持自定义的状态码,比如 400、401、403、404、500 等等。
- 包装不要那么深入,能够像 Builder 模式一样,链式调用,这样代码看起来更清晰。
项目简介
Hasaki
github.com/lxzan/hasak...
自己犹豫了一段时间,到底是不是要自己动手开撸。最后还是 lxzan
老兄靠谱,在他 Github 仓库的项目中找到了一个项目,叫做 Hasaki,这个项目就是一个 Golang Restful Client 的实现。
本着打不过就加入的原则,我有幸加入到这个项目中,一起来完善这个项目,让它更加完善。
在随后的一些项目中开始用 Hasaki
替换 go-resty
了,明显感觉之前的提到的一些限制没有了,是挺简单的,如果你愿意的话你都可以直接访问 Http Request/Response 对象,这样就可以在某种程度上把 Hasaki 当做一个普通的 Http Client 来使用了。 甚至真多地方都跟直接使用 net/http
的 http.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
在处理请求的时候还有一个问题,就是一旦使用了 Bind
和 BindJSON
方法,就会导致 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 个标记点:
- BeforeRequest:在请求之前执行。
WithBefore
方法 - 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 都会很乐意接受你的贡献和反馈。