Junedayday Blog

六月天天的个人博客

0%

Go语言学习 - RPC篇:gin框架的基础能力剖析

gin框架

gin是非常流行的一款HTTP框架。相较于原生的HTTP server,gin有很多改进点,主要在于3点:

  1. 上手简单,开发思路与原生HTTP基本一致
  2. 引入多个工具库,提高了开发效率
  3. 生态丰富,有许多开源的组件

围绕着gin框架,我们将展开今天的话题。

示例Gin代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// 请求结构体
type MyRequest struct {
MyInfo string `form:"my_info" json:"my_info"`
}

// 响应结构体
type MyResponse struct {
Errno int `json:"errno"`
Result string `json:"result"`
MyInfo string `form:"my_info" json:"my_info"`
}

// handler
func GetData(c *gin.Context) {
var b MyRequest
err := c.Bind(&b)
if err != nil {
c.JSON(http.StatusOK, MyResponse{
Errno: 1,
})
return
}

c.JSON(http.StatusOK, MyResponse{
Result: "my result",
})
}

func main() {
// gin server
r := gin.Default()
// 中间件
r.Use(gin.CustomRecovery(func(c *gin.Context, recovered interface{}) {
if err, ok := recovered.(string); ok {
c.String(http.StatusInternalServerError, fmt.Sprintf("error: %s", err))
}
c.AbortWithStatus(http.StatusInternalServerError)
}))

r.GET("/data", GetData)
r.Run()
}

关键函数分析

路由注册

1
2
3
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes 

func (group *RouterGroup) POST(relativePath string, handlers ...HandlerFunc) IRoutes

Gin支持不同HTTP方法的路由注册,这对RESTful风格的代码编写带来了很大帮助。对于阅读代码的同学,可以快速地通过路由注册的列表,如r.GET("/data", GetData),找到对应的方法。

Handler函数

1
type HandlerFunc func(*Context)

Handler函数相较于标准库,看似从2个参数RequestResponseWriter 转变成了一个参数 Context,简化了调用,但其实对调用者来说,复杂度并没有降低:

  1. Context 包含大量数据结构
  2. Context 包含了大量的方法

对于一名新手,在摸索出一条最佳实践路径前,学习成本不增反减。这主要是因为gin.Context过重。从编程角度来看,这个对象包含了过多信息,是个大而杂的工具集。

但不可否认的是,gin里提供了很多工具都比原生库好用,例如参数绑定、返回JSON数据。

绑定参数Bind

1
func (c *Context) Bind(obj any) error

Bind中引入了泛型中的any特性,但使用和原先的interface{}完全一致:

调用方可以填任意值。但实际上,Bind中必须为一个指针类型的数据结构,但由于interface{}对入参没有任何编译时的限制,导致传参问题在运行时才会报错。

例如:

1
2
3
4
5
6
var b MyRequest
// 正确
c.Bind(&b)
// 错误:编译正确,但运行时异常
c.Bind(b)
c.Bind(1)

返回JSON数据

1
func (c *Context) JSON(code int, obj any)

该方法是返回HTTP状态码为code,并且将obj数据进行JSON序列化。

它的问题同Bind函数,这里就不再赘述了。

middleware

gin框架提供了middleware的能力,它可以为整个Server提供一个公共能力的封装。有了middleware,整个server处理请求变成了:

middleware预处理 -> handler -> middleware后处理

  • 常见的预处理如

    • 参数校验
    • 用户认证
    • panic恢复
  • 常见的后处理则如

    • 定制HTTP状态码
    • 异常数据封装

总体来说,middleware能帮助用户减少重复性代码的编写,沉淀为公共能力,堪称web编程的一大利器。

gin能力剖析

我们先看看gin的改进点:

  1. mux支持RESTful风格的接口定义
  2. gin.Context提供了大量的工具,简化解析、返回的相关代码
  3. middleware可解决大量重复性的代码

这三点对开发者带来了不小的帮助。但是,我们在使用gin作为开发工具时,仍有一些问题:

  1. 大量的参数类型都是interface{}类型的数据结构,需要调用方自行保证
  2. gin.Context过大,学习和理解的成本很高
  3. 不少问题要在运行时才能发现,编译期无能为力

这些弊端汇总起来,依旧是和handler的函数定义相关:没有充分地利用Go强类型、编译检查的特点,来提高程序的质量、降低开发者的学习成本

更简单的Handler框架

那么,什么样的Handler框架对用户来说效果更好呢?我这边给出一个函数签名:

1
func BetterHandler(ctx context.Context, req *MyRequest) (rsp *MyResponse, err error)

我们依次看一下这些参数及其使用场景:

  1. ctx - 上下文,传递公共参数以及超时控制
  2. req - 请求的参数结构
  3. rsp - 响应的参数结构
  4. err - 错误信息

从整个RPC框架来看,它重点做了2件事:

  1. 自动将http参数解析到ctx和req中
    1. 解析规则按标准约定,如HTTP RESTful
    2. 一般是将Header里的信息放到ctx中,将URL+Body里的信息匹配到req结构体
  2. 自动将rsp和err对应到HTTP响应中
    1. err=nil时,认为请求成功,将rsp序列化后、填入到HTTP Body中
    2. err!=nil时,认为请求去失败,返回约定的协议(如异常状态码、异常HTTP的Body)

BetterHandler是一个很棒的编程体验:

  1. 无需关心解析参数与返回响应这两步的具体实现,统一由框架封装
  2. 函数的输入和输出都是强类型的,开发者有了一个明确的“模板”
  3. 将handler中的业务逻辑与RPC框架中协议部分解耦

也许你一下子无法快速理解,但反复对比下,你会逐渐体会到其中的精妙。但是,使用这个框架前,我们要解决以下两个问题:

  1. URL与Handler的匹配逻辑
  2. 怎么约定解析请求和返回响应的协议

小结

今天,我们一起看了gin框架的相关示例,编程体验比原生http库有了明显提升。gin的生态也给出了不少的优化方案或者插件,但由于框架本身限制,很难治本。

下一讲,我们将来看一个我最为推荐的RPC框架,分析一下其相关利弊。

Github: https://github.com/Junedayday/code_reading

Blog: http://junes.tech/

Bilibili: https://space.bilibili.com/293775192

公众号: golangcoding

二维码