作为一名开发者,我们最常见的日常工作就是web类编程:即对于CRUD请求,开发相关的业务代码。
在Go语言中,常见的RPC包括HTTP/gRPC/Thrift等,但绝大多数的开发场景仍是基于HTTP。本文对RPC的讨论,主要是基于HTTP的场景。
如果我们能熟练地掌握一套主流RPC框架,至少能提升20%的开发效率,而优秀的框架能带来更大的帮助。提效是为了有更多时间提升个人能力,我们今天就先对RPC框架有一个概览。
对一个web程序来说,它的核心功能就是处理一个请求。一个RPC的处理流程可以简单划分为3块:
这个看似简单的流程,在实际开发过程中会遇到很多问题。抛开业务逻辑,我们重点看一下1、3两步:
解析请求数据:
返回结果:
简单来说,这两步的功能可以概括为:如何将数据按定义的标准,进行序列化与反序列化。
常见的序列化工具如json/xml/protobuf等,新手主要了解 json 即可。
接下来,我们来看看标准库对请求的处理。
我们先来看标准HTTP库,它的实现是我们学习RPC的基础:
http.HandleFunc("/router", func(writer http.ResponseWriter, request *http.Request) {
type MyRequest struct {
Name string `json:"name"`
}
type MyResponse struct {
Errno int `json:"errno"`
}
var resp = new(MyResponse)
// 1. 解析参数
var req MyRequest
b, err := ioutil.ReadAll(request.Body)
if err != nil {
resp.Errno = 1
b, _ = json.Marshal(resp)
writer.Write(b)
return
}
json.Unmarshal(b, &req)
// 2. 业务逻辑处理
// 3. 返回结果
b, _ = json.Marshal(resp)
writer.Write(b)
return
})
响应 http.ResponseWriter
与 请求*http.Request
。这两个参数里面包含了许多信息,我这里列举最常用的几个:
我们梳理一下,一个新的HTTP接口的开发是什么样的逻辑:
示例就是/router
这个路由匹配,但实际情况中会更复杂:
对于第二点,我们自然也可以通过在handler函数中增加if-else
的逻辑来覆盖,但这么写下来,显然会增加handler函数的复杂程度。
从RPC的编程术语来说,我们称这个匹配逻辑为mux
,即多路复用。于是,我们就发现了http标准库中的2大优化点:
解析参数可以分解为3个问题:
有经验的朋友能深刻体会其中的繁琐(这部分工作不难,但很费开发与排查问题的时间)。比如说,在写业务层代码时,发现某个参数没有解析到,我们要分析的点非常多,包括协议问题、字段名称、字段类型、解析的工具库等等。
对于程序员来说,当然是希望尽可能地将这部分高度重复的工作进行简化,提升效率。
返回数据的代码看过去很简单,就是将数据序列化后返回。
但是,难点在于异常情况下的处理:例如,当handler中某个逻辑出错时,我们要怎么返回数据呢?最常见的方案,就是增加一个特殊的字段进行标记,如错误码errno
,不为0时表示错误,为0时才表示正确、再去解析数据结构。
上述3点没有什么技术上的难度,但在稍微复杂点的web程序时,会遇到什么问题呢?我们再次一起看看handler这个函数签名:
handler func(http.ResponseWriter, *http.Request)
如果你随意编写一个handler,也可以轻松编译通过,例如:
func(writer http.ResponseWriter, request *http.Request) {
return
}
因此,最主要的问题是在于:没办法对开发者在编写HTTP接口时,提供一定的强制规范。
由于handler这层的无法强制性地标准化,容易出现下限很低的失误,例如:
对于解析参数和返回数据,往往需要大量的重复编码。这部分虽然可以通过封装一些库来缓解,但每个handler都至少仍有2个调用:
Bind
WriteResponse
而对于有异常情况的,如发生error,WriteResponse
的调用量相应增加
由于handler
内的 解析请求和返回响应 没有任何代码限制,所以可以采用任意开源或自研的组件。
这些组件的实现各异,一旦扩散后很难收敛,很容易遇上不兼容的问题:
整个handler的可测试性是很低的,构造一个单测堪比写一大串业务代码,调试时很复杂。
所以,开发者往往更愿意靠 启动go程序+postman发请求 这样相对重量级的接口测试。
随着平台的迭代,我们经常会去修改一些接口。
但在Go语言中,它无法直接生成接口文档(如swagger文档)。普遍的方案会利用注释,但注释依旧无法和代码里的实现保证强一致性(如接口文档为OrderV1,但实际已经升级到了OrderV2)。
对于接口调用方,有4个工作是必须做的:
每个服务调用方,都需要重复地做这部分的工作。
这个问题可以通过统一建设公共库(SDK)来减轻,但SDK库如何与服务端的实现保证一致,是比较复杂的问题:例如新增了一个url+handler的处理逻辑,如何保证SDK会自动更新?
业务逻辑往往是复杂的,我们更多的时间是投入在业务逻辑处理上,但传统的方式容易出现各种兼容性问题,比如:
开发者可能只是发现某个内部bug,改了某个字段的数据结构,但却导致所有调用方整个解析失败(如json.Unmarshal)。
也许,有的朋友看了上述问题,会觉得不以为然:如果能搞好工具库和标准,以上问题都能解决。
没错,上述问题都不致命,否则业界也早就出现明确的标准了。但是我们要考虑到两点:
就像是你要从上海到北京出差,你当然可以自驾、歪歪扭扭地沿着高速公路到达目的地,有很高的选择自由度;但有了更快的高铁路线,何乐而不为呢?毕竟,从出差这件事来看,最重要的是保证准时地到达目的地,
那么RPC的“高铁方案”是怎么样的呢?下一节我们继续展开。
Github: https://github.com/Junedayday/code_reading Blog: http://junes.tech/ Bilibili: https://space.bilibili.com/293775192
扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有