Go Web 应用路由管理

前言

在学习 golang web 开发的过程中,看着 gin 框架中关于路由的示例,就思考着在实际项目开发过程中肯定不会将路由、路由群组的加载都集中在 main 函数中,如果路由很少还好,否则业务一旦多起来,所有业务的路由都混杂在一起,耦合度那就太高了,根本不利于后期维护。于是,研究了几个开源的 golang 项目,这里总结一下常见的路由管理模式(路由的组织方式)。

路由是什么

Web 应用开发中的路由就是 URL 与 资源的对应关系,资源可以是静态文件、API、服务、数据等等。路由分前端路由和服务端路由,本文讨论的是服务端路由。

集中式管理

顾名思义,就是将项目中的路由集中声明在约定的路由文件中,然后一起装载到 http 服务中,比如开源项目 Go语言中文网 中的路由组织方式:

http/controller/ 目录下每一个 controller 中都有个方法 RegisterRoute 用来管理当前 controller 下的路由信息

// ...

type ArticleController struct{}

// 注册路由
func (self ArticleController) RegisterRoute(g *echo.Group) {
    g.GET("/articles", self.ReadList)
    g.GET("/articles/crawl", self.Crawl)

    g.GET("/articles/:id", self.Detail)

    g.Match([]string{"GET", "POST"}, "/articles/new", self.Create, middleware.NeedLogin(), middleware.Sensivite(), middleware.BalanceCheck(), middleware.PublishNotice(), middleware.CheckCaptcha())
    g.Match([]string{"GET", "POST"}, "/articles/modify", self.Modify, middleware.NeedLogin(), middleware.Sensivite())
}

// ...

然后在 http/controller/routes.go 中集中将各个 controller 中的方法 RegisterRoute 注册到路由群组中

package controller

import echo "github.com/labstack/echo/v4"

func RegisterRoutes(g *echo.Group) {
    new(IndexController).RegisterRoute(g)
    new(AccountController).RegisterRoute(g)
    new(TopicController).RegisterRoute(g)
    new(ArticleController).RegisterRoute(g)
    new(ProjectController).RegisterRoute(g)
    new(ResourceController).RegisterRoute(g)
    new(ReadingController).RegisterRoute(g)
    new(WikiController).RegisterRoute(g)
    new(UserController).RegisterRoute(g)
    new(LikeController).RegisterRoute(g)
    new(FavoriteController).RegisterRoute(g)
    new(MessageController).RegisterRoute(g)
    new(SidebarController).RegisterRoute(g)
    new(CommentController).RegisterRoute(g)
    new(SearchController).RegisterRoute(g)
    // ...
}

最后在 main 函数中调用各个 routes.go 中函数 RegisterRoutes 将路由群组注册到 http 服务中

// ...

func main(){
    // ...

    e := echo.New()

    serveStatic(e)

    e.Use(thirdmw.EchoLogger())
    e.Use(mw.Recover())
    e.Use(pwm.Installed(filterPrefixs))
    e.Use(pwm.HTTPError())
    e.Use(pwm.AutoLogin())

    // 评论后不会立马显示出来,暂时缓存去掉
    // frontG := e.Group("", thirdmw.EchoCache())
    frontG := e.Group("")
    controller.RegisterRoutes(frontG)

    adminG := e.Group("/admin", pwm.NeedLogin(), pwm.AdminAuth())
    admin.RegisterRoutes(adminG)

    // appG := e.Group("/app", thirdmw.EchoCache())
    appG := e.Group("/app")
    app.RegisterRoutes(appG)

    // ...
}

// ...

golang 就是这么简单,没那么多奇技淫巧,简简单单几行代码就把路由搞定了,不像 java 那么多类,类之间那么复杂,即使 springboot 用起来算是上手容易了,但是后面开发过程中也要了解很多注解之类的,阅读源码理清里面的点点滴滴,只能呵呵了…

搜了一些其他的开源项目比如 golang123DocHub 中的路由组织方式:

都是把路由声明在一个路由文件中,例如:

// ...

// Route 路由
func Route(router *gin.Engine) {
    apiPrefix := config.ServerConfig.APIPrefix

    api := router.Group(apiPrefix, middleware.RefreshTokenCookie)
    {
        api.GET("/siteinfo", common.SiteInfo)
        api.POST("/signin", user.Signin)
        api.POST("/signup", user.Signup)
        api.POST("/signout", middleware.SigninRequired, user.Signout)
        api.POST("/upload", middleware.SigninRequired, common.UploadHandler)
        api.POST("crawlnotsavecontent", middleware.EditorRequired, crawler.CrawlNotSaveContent)

        api.POST("/active/sendmail", user.ActiveSendMail)
        api.POST("/active/user/:id/:secret", user.ActiveAccount)

        api.POST("/reset/sendmail", user.ResetPasswordMail)
        api.GET("/reset/verify/:id/:secret", user.VerifyResetPasswordLink)
        api.POST("/reset/password/:id/:secret", user.ResetPassword)

        // ...
    }

    adminAPI := router.Group(apiPrefix+"/admin", middleware.RefreshTokenCookie, middleware.AdminRequired)
    {
        adminAPI.POST("/keyvalueconfig", keyvalueconfig.SetKeyValue)

        adminAPI.GET("/users", user.AllList)

        adminAPI.GET("/books/categories", category.BookCategoryList)
        adminAPI.POST("/books/categories/create", category.CreateBookCategory)

        adminAPI.GET("/categories", category.List)
        adminAPI.POST("/categories/create", category.Create)
        adminAPI.PUT("/categories/update", category.Update)

        // ...
    }
}

// ...

集中式声明路由的方式中,个人比较喜欢 Go语言中文网 的方式,具体的路由是在各自的 controller 中定义的,如果需要在 controller 中新增路由则只需修改当前的 controller 即可,而后面的两个开源项目每新增一个路由都需要修改至少两个地方。但是这种将路由集中声明在一起的方式,如果需要添加新的 controller 都至少需要修改两个地方,如果项目比较复杂,涉及的开发人员较多,而路由需要频繁修改更新的话,那么约定的路由文件发生冲突的可能性会大大增加。

总结
优点:将路由集中声明在约定的路由文件中,能够很直观的观察到所有路由规划,思路上清晰,逻辑上简单,上手容易,易于在团队内推广
缺点:新增 controller 需要修改至少两处地方,项目复杂、开发人员多时有增大冲突的风险

分散式管理

相对于将路由都集中声明在一起与其实现逻辑分开来,我更喜欢 java 中类似 servlet3.0 和 spring 中直接将路由以注解的方式和其实现的业务直接声明在一起,类似的还有 nodejs 中 nestjs 框架,通过 装饰器 模式来实现将路由和业务实现声明在一起。这种声明方式的好处就是,路由和业务实现在一起,无论是新增还是修改,在开发体验上都是一气呵成的,一般在 main 函数中也看不到注册具体路由的声明,而至于 路由 和 实现函数 怎么绑定在一起,就交由框架去管理吧,开发人员只负责按约定规则编写即可

可惜 golang 中没有 注解和装饰器 这种语法 ~

在 github 上查了一些开源项目,没有发现类似的实现或者相关轮子,于是自己简单的实现了一下(针对 gin 框架实现的,很容易改成其他 go web 框架使用)。目标就是将路由分散的声明放在当前的 controller 中。

之前库为 github.com/niqingyang/ginboot,2019/10/19 迁移到了 gokit 中

Ginboot

GinBoot 是一个能够简化 Gin 框架下路由管理的库

核心思想是通过 GinBoot 注册 Gin 的实例和路由群组,然后在每个 Controller 的 init 中各自注册自己的路由回调函数,再统一由 GinBoot 控制群组和路由回调函数的加载顺序,减少路由间的耦合

GinBoot 是非线程安全的(因为 GinBoot 的业务需要在 Gin 服务启动前加载并运行完毕,所以无需考虑并发,也不建议在此期间出现并发逻辑)

安装

$ go get -u github.com/gokit/ginboot

快速开始

  1. 在 main 函数中按下面模板编写,为 ginboot 注册 gin 实例,并调用 Strap 函数引导注册的回调函数初始化路由
package main

import (
    "github.com/gin-gonic/gin"
    "github.com/gokit/ginboot"
    _ "ginboot/example/controller" // 加载 controller 包下的 init 函数,注册路由相关的回调函数
)

func main() {
    engine := gin.Default()

    // 注册默认的 Gin
    ginboot.RegisterDefaultEngine(engine)
    // 引导回调函数注册路由
    ginboot.Strap()

    engine.Run()
}
  1. 定义 controller,编写 init 函数,在其中通过 ginboot.InjectRouteXXX() 向 ginboot 注入声明路由的回调函数
package controller

import (
    "github.com/gin-gonic/gin"
    "github.com/gokit/ginboot"
)

type IndexController struct{}

func init() {
    // 向默认的 Gin 中注册路由
    ginboot.InjectRouteByEngine(ginboot.GinDefault, new(IndexController).registerRoutes)
}

func (i *IndexController) registerRoutes(engine *gin.Engine) {
    // 声明路由
    engine.GET("/", i.index)
}

func (IndexController) index(ctx *gin.Context) {
    ctx.String(200, "hello world")
}
  1. 如果需要定义路由群组,可以在 init 函数中,通过 ginboot.InjectGroupXXX() 向 ginboot 注入声明路由群组的回调函数
package controller

import (
    "github.com/gin-gonic/gin"
    "github.com/gokit/ginboot"
)

const GroupUser = "/user"
const GroupAdmin = "/admin"

func init() {
    // 向默认的 Gin 中注册路由群组
    ginboot.InjectGroupByEngine(ginboot.GinDefault, func(engine *gin.Engine) {

        ginboot.RegisterGroup(GroupUser, engine.Group("/user"))
        ginboot.RegisterGroup(GroupAdmin, engine.Group("/admin"))

    })
}
  1. 最后一步,在 main 函数中,通过 “_” 的方式 import 相关包,就大功告成了 ~
// 加载 controller 包下的 init 函数,声明路由和路由群组相关的回调函数
import _ "xxx/xxx/controller"

接口说明

ginboot 包下函数说明

// 注册一个指定 name 的 Gin 实例
func RegisterEngine(name string, engine *gin.Engine)

// 注册一个默认的 Gin 实例,name 为 default,可以使用常量 ginboot.DefaultGin
func RegisterDefaultEngine(engine *gin.Engine)

// 获取默认 Gin 实例
func GetDefaultEngine() *gin.Engine

// 获取指定 name 的 Gin 实例
func GetEngine(name string) *gin.Engine

// 注册一个指定 name 的路由群组
func RegisterGroup(name string, group *gin.RouterGroup)

// 获取指定 name 的路由群组
func GetGroup(name string) *gin.RouterGroup

// 注入路由群组回调函数,可以在回调函数内通过上面的接口获取 GIN 实例并创建路由群组并注册到 ginboot 中
func InjectGroup(callback func())

// 注入路由回调函数,可以在回调函数内通过上面的接口获取 GIN 实例或获取路由群组,然后声明路由
func InjectRoute(callback func())

// 向指定 name 的路由群组中中注入路由回调
func InjectRouteByGroup(groupName string, callback func(group *gin.RouterGroup))

// 向指定的 GIN 实例中注入路由回调
func InjectRouteByEngine(engineName string, callback func(engine *gin.Engine))

// 向指定的 GIN 实例中注入路由群组回调
func InjectGroupByEngine(engineName string, callback func(engine *gin.Engine))

// 在注册 Gin 实例后,启用引导程序引导回调,ginboot 会优先调用路由群组的回调函数,然后再调用路由的回调函数
func Strap()

以上就是我在 golang 中希望的路由管理模式,期待有更好的实现方式来替代这个简单的库 ~

发表评论

发表评论

*

沙发空缺中,还不快抢~