Beego的硬伤

由于自动化生成路由是 Beego 默认也是最常用的使用模式, 那么我们就从这种模式谈谈 Beego 的硬伤(无法修复或者难以修复的问题).

自动化路由

Beego 使用了一种奇怪的方式去自动生成路由: 解析 routers/router.go 文件, 去拿到里边每个 controller , 再去每个 controller 中解析每个方法的注释, 最终在 routers 目录下自动生成一个名为 commentsRouter_controllers.go 的文件.

这种做法乍一看没什么毛病, 然而这个路由生成行为只有一种方式可以触发: 使用 bee run 或者 go build && ./project_name开发模式下把项目运行起来. 而不是通过一个单独的解耦的不需要把项目运行起来的命令去自动生成, 并且在 prod 模式是不会自动生成文件的.

还有一个问题就是如果直接使用 go run 去运行项目的话, 那么就会生成一个诡异的路由文件名: commentsRouter_________________________Users_tuotoo_GoProj_src_git_spiritframe_com_tuotoo_tbee_controllers.go. (WTF???)

这就导致了如果使用了 CI 的话, 就需要将这个 commentsRouter_controllers.go 文件上传到代码库中, 因为这种代码生成方式对 CI 是不友好的. 但是如果将这个文件上传到代码库中的话, 那么在多人协作时, 由于其诡异的生成规则又会造成代码冲突的问题. 使得出现冲突时, 不得不将该文件删掉, 再重新生成, 再提交代码…

另一个问题就是 routers/router.go 的写法是固定的, 必须是如下这种格式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func init() {
ns := beego.NewNamespace("/v1",
beego.NSNamespace("/object",
beego.NSInclude(
&controllers.ObjectController{},
),
),
beego.NSNamespace("/user",
beego.NSInclude(
&controllers.UserController{},
),
),
)
beego.AddNamespace(ns)
}

就算稍微改一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func init() {
beego.AddNamespace(beego.NewNamespace("/v1",
beego.NSNamespace("/object",
beego.NSInclude(
&controllers.ObjectController{},
),
),
beego.NSNamespace("/user",
beego.NSInclude(
&controllers.UserController{},
),
),
))
}

虽然是一个意思的代码, 但是修改后的代码会导致 swagger 文档生成失败…(WTF???)

不支持从外部导入 controller

假如我们有一个 object 库, 其中有一个写好的 ObjectController, 当我们想把这个 controller 导入项目中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func init() {
ns := beego.NewNamespace("/v1",
beego.NSNamespace("/object",
beego.NSInclude(
&object.ObjectController{},
),
),
beego.NSNamespace("/user",
beego.NSInclude(
&controllers.UserController{},
),
),
)
beego.AddNamespace(ns)
}

乍一看, 没什么毛病. 然而在应用启动时, Beego 会无法生成 commentsRouter_controllers.go 且陷入死循环(从 CPU 占用上看是这样的).

解决办法是在 object 库中新建一个 routers/router.go, 编写以下内容:

1
2
3
4
5
6
7
8
9
func init() {
beego.NewNamespace("/v1",
beego.NSNamespace("/object",
beego.NSInclude(
&objControllers.ObjectController{},
),
),
)
}

并将该 routers 模块在项目中初始化…且项目中的 object 路由必须保持原样, 不能删掉. 也就是说, 我们必须在两个库中定义两个一毛一样的 namespace.
( WTF???无法理解这种莫名其妙的设计… )

这样的话, 在项目运行时 Beego 会在 object 项目的 routers 目录下自动生成一个 commentsRouter____object_controllers.go 文件, 这就更莫名其妙了…往上层库添加自动生成的代码…

这就更别说外部的 controller 需要传入额外的参数了, 毫无疑问也是不支持的…

糟糕的 swagger 生成

关于 swagger 生成, 一个好消息是, Beego 提供了一个解耦的命令来执行这个动作: bee generate docs, 这使得它不那么糟糕.

不支持其他库的 type 解析. 假如我们用了 sql.NullString 等类型的话, 那么会导致 swagger 生成失败. 不过, 这个问题还算不大, 可以通过以下方式进行修复:
这块代码修改为

1
2
3
4
if !ok {
beeLogger.Log.Warnf("Unknown type without TypeSec: %v\n", d)
return
}

另一个问题就是 Beego 的 swagger 不支持 vendor, 即使你将依赖放在了 vendor 下, Beego 仍然会去 GOPATH 寻找源码来进行解析.

两个 namespace 引用同一个 controller, 如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func init() {
ns := beego.NewNamespace("/v1",
beego.NSNamespace("/object",
beego.NSInclude(
&objControllers.ObjectController{},
),
),
beego.NSNamespace("/user",
beego.NSInclude(
&controllers.UserController{},
),
),
beego.NSNamespace("/user2",
beego.NSInclude(
&controllers.UserController{},
),
),
)
beego.AddNamespace(ns)
}

在这种情况下 Beego 无法正确的进行 API 分组:

photo_2018-05-20_15-04-42
photo_2018-05-20_15-04-42

Swagger 中一米长的报错:

photo_2018-05-20_15-06-56
photo_2018-05-20_15-06-56

每次打开 Swagger 做的第一件事就是把错误信息折叠起来, 否则没法看…

无法测试

由于 Beego 从设计上就没有兼容官方库 net/http , 以及从注释去解析路由等原因导致了 Beego 项目中的 controller 无法测试的问题. 虽然可以将数据逻辑放在 models 中, 对 models 编写测试, 但业务逻辑的无法测试导致项目中有 50% 的逻辑是无法测试的, 无法测试路由/传参/返回值…关于这个问题作者在2014年表过态, 后来就没消息了…

无法通过编写测试来保证代码的质量, 这大概就是 Beego 最大的缺陷了.

不过不可否认的是, Beego 是一款非常成功的 web 框架, 极低的入门成本, 快速的开发速度以及成功的宣传, 间接为 Go 吸引来了非常多的爱好者.

下面是 Biu 的软文, 不想看的可以关闭 tab 了 :-)


Biu 的诞生

由于 Beego 存在以上缺陷, 于是我开始寻找另外一款好用的 go web 框架. 它必须: 稳定的接口, 支持自动生成 swagger 文档, 简单易用的路由, 可以方便的进行定制. 然而, 多数流行的 web 框架都不支持自动生成 swagger 文档(前后端对接神器/懒人必备).

然后选择了 go-restful 作为基础框架, 并对其进行定制, 最终产生了 Biu 这个项目.

go-restful 是容器集群管理系统中的王者 kubernetes 的 API Server 所使用的 Web 框架, 其稳定性以及后续的维护都是有保障的, 堪称工业级别的框架了. 代码文档也是非常之详尽, 框架的架构方案和所提供的接口扩展性非常之强, 在路由算法上采用了成熟的JSR311算法, 最重要的是框架作者对于 Issue 的回复也很快.

一个基于 Biu 的项目可参考 biu-example.

Beego 一个 namespace 对应一个 controller 的做法是个非常棒的工程实践, Biu 的 router 保留了个设定.

1
2
3
4
5
6
7
biu.AddServices("/v1", nil,
biu.NS{
NameSpace: "foo",
Controller: Foo{},
Desc: "Foo Controller",
},
)

唯一的不同之处就是: Biu 不需要生成一个新的路由, 所编写的路由文件就是真实的路由.

在 Biu 中, 一个 controller 也称为一个 service, 可通过 biu.AddServices 添加到根路由中, 每个 controller 只需实现 func (ctl) WebService(ws biu.WS) 方法, 并在该方法内定义该 service 下每个 handler 的配置即可, 如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Foo controller
type Foo struct{}

// WebService implements CtlInterface
func (ctl Foo) WebService(ws biu.WS) {
ws.Route(ws.GET("/").Doc("Get Bar").
Param(ws.QueryParameter("num", "number").DataType("integer")).
DefaultReturns("Bar", Bar{}), &biu.RouteOpt{
ID: "2F996F8F-9D08-4BE0-9D4D-ACB328D8F387",
To: ctl.getBar,
Errors: map[int]string{
100: "num not Number",
},
})

// add more routes as you like:
// ws.Route(ws.POST("/foo"),nil)
// ...
}

也就是将 Beego 在每个 handler 前面的注释改成放到 func (ctl) WebService(ws biu.WS) 里面, 并且通过显式的方法来进行传入, 而非约定第一个值为 xxx 第二个值为 xxx …

Biu 中还对自定义 JSON 错误码进行了支持, 并且设置的错误码是会出现在 swagger 中的.

紧接着在 handler 这一层:

1
2
3
4
5
6
func (ctl Foo) getBar(ctx biu.Ctx) {
num, err := ctx.Query("num").Int()
ctx.Must(err, 100)

ctx.ResponseJSON(Bar{Msg: "bar", Num: num})
}

Biu 内置了 errc 的支持, 将

1
2
3
4
5
6
if err != nil {
log(err)
...
ctx.ResponseJSON({Code: 100, Msg: Errors[100]})
return
}

缩减为一句 ctx.Must(err, 100) 完事, msg 会自动使用 WebService 中定义的 Error[100] 对应的字符串.

biu.Ctx 实际上是一个:

1
2
3
4
5
6
type Ctx struct {
*restful.Request
*restful.Response
*restful.FilterChain
ErrCatcher errc.Catcher
}

所以在 go-restful 的 handler 中的方法全都可以直接在 biu 的 handler 中使用.

Biu 中的测试

既然吐槽了 Beego 对 Controller 的测试问题, 那么 Biu 是怎么解决这个问题的呢?

实际上, go-restful 的 container 就已经实现了 http.Handler 接口, 所以可以直接使用 httptest.NewServer 对其进行测试. Biu 中也内置了 biu.NewTestServer() 方法, 可以方便的返回一个 test server. 接下来就只需简单的对 test server 发起请求, 验证请求结果就可以了.