Go Web 框架自研——初探

beego 框架参考

这是 beego 启动一个 web 服务的基础用法:

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
// controller.go
package beego

import "github.com/beego/beego/v2/server/web"

type UserController struct {
web.Controller
}

func (u *UserController) GetUser() {
u.Ctx.WriteString("你好,我是小张")
}

func (u *UserController) CreateUser() {
user := &User{}
err := u.Ctx.BindJSON(user)
if err != nil {
u.Ctx.WriteString(err.Error())
return
}

_ = u.Ctx.JSONResp(user)
}

type User struct {
Name string `json:"name"`
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// controller_test.go
package beego

import (
"github.com/beego/beego/v2/server/web"
"testing"
)

func TestUserController(t *testing.T) {
web.BConfig.CopyRequestBody = true
c := &UserController{}
web.Router("/user", c, "get:GetUser")
web.Router("/user/create", c, "post:CreateUser")
web.Run(":8081")
}

可以看到,定义了一个 UserController 结构体,这个 controller 组合了 beego 的 web controller,提供了获取用户和创建用户两个方法。然后注册路由的时候把这个 controller 传了进去。

也就是说,beego 是按照 MVC 模式设计的,它要求使用者在业务层面上使用 MVC 模式组织自己的代码,也就是一个业务一个 controller 这个样子。不过我个人觉得,这种设计不是很好,是否使用 MVC 应该让使用者自己选择,而不应该让框架提供者来左右。

再来看一下定义 controller 的时候组合的 beego 的 web controller 定义:

beego web controller

这里有一个 ctx 字段,它的定义是:

1
2
3
4
5
6
7
8
9
// Context Http request context struct including BeegoInput, BeegoOutput, http.Request and http.ResponseWriter.
// BeegoInput and BeegoOutput provides an api to operate request and response more easily.
type Context struct {
Input *BeegoInput
Output *BeegoOutput
Request *http.Request
ResponseWriter *Response
_xsrfToken string
}

可以看到,beego 在 ctx 里面存了请求和响应信息,以及自己的输入输出。我觉得这样有点重复了,很容易让使用者误解,为什么会有两个输入,两个输出,究竟应该使用哪个。

在启动 web 服务的时候,是直接使用的 web.Run 方法,这是 beego 提供的一个默认包变量,它的定义是:

beego server 抽象

可以看到,beego 抽象出了一个 server 层面上的结构体,这个结构体里面有一些字段,其中 LifeCycleCallbacks 也就是声明周期回调函数,用户可以控制服务启动前干些什么,启动后干些什么。Server 字段是 http 包里面的 Server 类型,可以用来启动服务。Handlers 字段的类型定义是这样的:

controllerRegister

这里面有一个 routers 字段,它是一个 map 类型,存的是每个请求方法对应的路由树,用户注册的路由会存到这里面来。

通过这些,可以看到 beego 基本上有四个抽象:

  • 路由(这个是最重要的)
  • server
  • context
  • controller

server 负责把其它抽象组合起来,方便使用者使用,并且负责和 go 的 http 包打交道,在 server 层面上也提供了一些声明周期回调。

context 主要负责请求响应的传递,以及负责用户自定义的数据传递。

路由负责匹配请求和对应的业务处理方法。

controller 则负责业务处理。

整体样子就是:

beego 抽象

其它框架参考

其它框架比如说 gin、iris、echo,用法上基本都相似,因为国内用 gin 也比较多嘛,所以这里以 gin 为例说明:

1
2
3
4
5
6
7
8
9
10
11
12
// user_controller.go
package gin

import "github.com/gin-gonic/gin"

type UserController struct {
}

func (c *UserController) GetUser(ctx *gin.Context) {
panic("一些业务错误")
ctx.String(200, "hello, world")
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// user_controller_test.go
package gin

import (
"github.com/gin-gonic/gin"
"net/http"
"testing"
)

func TestUserController_GetUser(t *testing.T) {
g := gin.Default()
ctrl := &UserController{}
g.GET("/user/info", ctrl.GetUser)
g.POST("/user/:param", func(ctx *gin.Context) {
ctx.String(http.StatusOK, "hello %s", "world")
})
_ = g.Run(":8082")

http.ListenAndServe(":8083", g)
}

可以看到,和 beego 不同的是 gin 没有按照 MVC 模式设计自己的代码,而是让用户在注册路由的时候,传入一个 func(ctx *gin.Context) 类型的函数,框架将其抽象成了type HandlerFunc func(*Context)

这样的好处就是用户可以选择是否使用 MVC 模式来组织自己的代码。如果用户想要使用 MVC 模式,那么只需要按照这个函数类型设计自己的方法就可以了。

gin 的 Context 也和 beego 类似:

gin context

至于 beego 的 httpServer,gin 里面是 Engine:

gin engine

而且 gin 的 Engine,实现了 http 包里面的 Handler 接口:

gin engine

也就意味着,我可以使用 gin 包提供的启动方法,也可以创建一个 gin 的 Engine,然后使用 http 包里面的 ListenAndServe 启动它。

然后 Engine 结构体组合了 RouterGroup,其定义为:

1
2
3
4
5
6
7
8
// RouterGroup is used internally to configure router, a RouterGroup is associated with
// a prefix and an array of handlers (middleware).
type RouterGroup struct {
Handlers HandlersChain
basePath string
engine *Engine
root bool
}

这里面存在和 Engine 的一个双向引用关系,gin 可能是需要在 RouterGroup 上拿到 engine 进行一些操作。

对于 gin 的路由注册:

gin router

可以看到 gin 为不同的请求方法提供了不同的实现,不过最终都会调到 handle 方法上。这里我认为可以只提供一个 Handle 方法,不用提供 GET、POST 等这些实现,这样可以让接口尽可能的小而美。

可以看到 gin 也差不多设计成了这幅样子:

  • Engine
  • 路由 RouterGroup
  • Context
  • HandlerFunc

和 beego 一样,只不过名字不用而已。

gin 抽象

其它几个框架类似,这里就不详细说了。

总结

可以看到这些 web 框架,基本上都是一副鬼样子,有 server、路由、context、业务处理四个抽象。

server 负责和 go 的 http 包打交道,负责 web 服务的启动终止、生命周期管理,以及将路由注册、业务处理这些抽象组织在一起,方便框架者使用。

context 负责数据管理,请求响应内容处理。

业务处理负责组织使用者的业务代码。

路由注册负责绑定号好不同的请求方法和路径对应的业务处理逻辑。