title: Go语言动手写Web框架 - Gee第四天 分组控制Group
date: 2019-09-01 15:10:10
description: 7天用 Go语言 从零实现Web框架教程(7 days implement golang web framework from scratch tutorial),用 Go语言/golang 动手写Web框架,从零实现一个Web框架,以 Gin 为原型从零设计一个Web框架。本文介绍了分组控制(Group Control)的意义,以及嵌套分组路由的实现。
tags:

  • Go
    nav: 从零实现
    categories:
  • Web框架 - Gee
    keywords:
  • Go语言
  • 从零实现Web框架
  • 动手写Web框架
  • Group Control
    image: post/gee-day4/group.jpg
    github: https://github.com/geektutu/7days-golang
    book: 七天用Go从零实现系列
    book_title: Day4 分组控制

本文是 7天用Go从零实现Web框架Gee教程系列的第四篇。

  • 实现路由分组控制(Route Group Control),代码约50行

分组的意义

分组控制(Group Control)是 Web 框架应提供的基础功能之一。所谓分组,是指路由的分组。如果没有路由分组,我们需要针对每一个路由进行控制。但是真实的业务场景中,往往某一组路由需要相似的处理。例如:

  • /post开头的路由匿名可访问。
  • /admin开头的路由需要鉴权。
  • /api开头的路由是 RESTful 接口,可以对接第三方平台,需要三方平台鉴权。

大部分情况下的路由分组,是以相同的前缀来区分的。因此,我们今天实现的分组控制也是以前缀来区分,并且支持分组的嵌套。例如/post是一个分组,/post/a/post/b可以是该分组下的子分组。作用在/post分组上的中间件(middleware),也都会作用在子分组,子分组还可以应用自己特有的中间件。

中间件可以给框架提供无限的扩展能力,应用在分组上,可以使得分组控制的收益更为明显,而不是共享相同的路由前缀这么简单。例如/admin的分组,可以应用鉴权中间件;/分组应用日志中间件,/是默认的最顶层的分组,也就意味着给所有的路由,即整个框架增加了记录日志的能力。

提供扩展能力支持中间件的内容,我们将在下一节当中介绍。

分组嵌套

一个 Group 对象需要具备哪些属性呢?首先是前缀(prefix),比如/,或者/api;要支持分组嵌套,那么需要知道当前分组的父亲(parent)是谁;当然了,按照我们一开始的分析,中间件是应用在分组上的,那还需要存储应用在该分组上的中间件(middlewares)。还记得,我们之前调用函数(*Engine).addRoute()来映射所有的路由规则和 Handler 。如果Group对象需要直接映射路由规则的话,比如我们想在使用框架时,这么调用:

  1. r := gee.New()
  2. v1 := r.Group("/v1")
  3. v1.GET("/", func(c *gee.Context) {
  4. c.HTML(http.StatusOK, "<h1>Hello Gee</h1>")
  5. })

那么Group对象,还需要有访问Router的能力,为了方便,我们可以在Group中,保存一个指针,指向Engine,整个框架的所有资源都是由Engine统一协调的,那么就可以通过Engine间接地访问各种接口了。

所以,最后的 Group 的定义是这样的:

day4-group/gee/gee.go

  1. RouterGroup struct {
  2. prefix string
  3. middlewares []HandlerFunc // support middleware
  4. parent *RouterGroup // support nesting
  5. engine *Engine // all groups share a Engine instance
  6. }

我们还可以进一步地抽象,将Engine作为最顶层的分组,也就是说Engine拥有RouterGroup所有的能力。

  1. Engine struct {
  2. *RouterGroup
  3. router *router
  4. groups []*RouterGroup // store all groups
  5. }

那我们就可以将和路由有关的函数,都交给RouterGroup实现了。

  1. // New is the constructor of gee.Engine
  2. func New() *Engine {
  3. engine := &Engine{router: newRouter()}
  4. engine.RouterGroup = &RouterGroup{engine: engine}
  5. engine.groups = []*RouterGroup{engine.RouterGroup}
  6. return engine
  7. }
  8. // Group is defined to create a new RouterGroup
  9. // remember all groups share the same Engine instance
  10. func (group *RouterGroup) Group(prefix string) *RouterGroup {
  11. engine := group.engine
  12. newGroup := &RouterGroup{
  13. prefix: group.prefix + prefix,
  14. parent: group,
  15. engine: engine,
  16. }
  17. engine.groups = append(engine.groups, newGroup)
  18. return newGroup
  19. }
  20. func (group *RouterGroup) addRoute(method string, comp string, handler HandlerFunc) {
  21. pattern := group.prefix + comp
  22. log.Printf("Route %4s - %s", method, pattern)
  23. group.engine.router.addRoute(method, pattern, handler)
  24. }
  25. // GET defines the method to add GET request
  26. func (group *RouterGroup) GET(pattern string, handler HandlerFunc) {
  27. group.addRoute("GET", pattern, handler)
  28. }
  29. // POST defines the method to add POST request
  30. func (group *RouterGroup) POST(pattern string, handler HandlerFunc) {
  31. group.addRoute("POST", pattern, handler)
  32. }

可以仔细观察下addRoute函数,调用了group.engine.router.addRoute来实现了路由的映射。由于Engine从某种意义上继承了RouterGroup的所有属性和方法,因为 (*Engine).engine 是指向自己的。这样实现,我们既可以像原来一样添加路由,也可以通过分组添加路由。

使用 Demo

测试框架的Demo就可以这样写了:

  1. func main() {
  2. r := gee.New()
  3. r.GET("/index", func(c *gee.Context) {
  4. c.HTML(http.StatusOK, "<h1>Index Page</h1>")
  5. })
  6. v1 := r.Group("/v1")
  7. {
  8. v1.GET("/", func(c *gee.Context) {
  9. c.HTML(http.StatusOK, "<h1>Hello Gee</h1>")
  10. })
  11. v1.GET("/hello", func(c *gee.Context) {
  12. // expect /hello?name=geektutu
  13. c.String(http.StatusOK, "hello %s, you're at %s\n", c.Query("name"), c.Path)
  14. })
  15. }
  16. v2 := r.Group("/v2")
  17. {
  18. v2.GET("/hello/:name", func(c *gee.Context) {
  19. // expect /hello/geektutu
  20. c.String(http.StatusOK, "hello %s, you're at %s\n", c.Param("name"), c.Path)
  21. })
  22. v2.POST("/login", func(c *gee.Context) {
  23. c.JSON(http.StatusOK, gee.H{
  24. "username": c.PostForm("username"),
  25. "password": c.PostForm("password"),
  26. })
  27. })
  28. }
  29. r.Run(":9999")
  30. }

通过 curl 简单测试:

  1. $ curl "http://localhost:9999/v1/hello?name=geektutu"
  2. hello geektutu, you're at /v1/hello
  3. $ curl "http://localhost:9999/v2/hello/geektutu"
  4. hello geektutu, you're at /hello/geektutu