在我们开发定义路由的时候,可能会遇到很多部分重复的路由:

/admin/users
/admin/manager
/admin/photo
/admin/Gin

分组路由

类似以上示例,就是分好组的路由,分组的原因有很多
(1)比如基于模块化,把同样模块的放在一起
(2)比如基于版本,把相同版本的API放一起,便于使用

在有的框架中,分组路由也被称之为命名空间。

Gin
func main() {
    r := gin.Default()

    //V1版本的API
    v1Group := r.Group("/v1")
    v1Group.GET("/users", func(c *gin.Context) {
        c.String(200, "/v1/users")
    })
    v1Group.GET("/products", func(c *gin.Context) {
        c.String(200, "/v1/products")
    })

    //V2版本的API
    v2Group := r.Group("/v2")
    v2Group.GET("/users", func(c *gin.Context) {
        c.String(200, "/v2/users")
    })
    v2Group.GET("/products", func(c *gin.Context) {
        c.String(200, "/v2/products")
    })

    r.Run(":8080")
}
Groupr{}
 v1Group := r.Group("/v1")
    {
        v1Group.GET("/users", func(c *gin.Context) {
            c.String(200, "/v1/users")
        })
        v1Group.GET("/products", func(c *gin.Context) {
            c.String(200, "/v1/products")
        })
    }

    v2Group := r.Group("/v2")
    {
        v2Group.GET("/users", func(c *gin.Context) {
            c.String(200, "/v2/users")
        })
        v2Group.GET("/products", func(c *gin.Context) {
            c.String(200, "/v2/products")
        })
    }

路由中间件

Group
func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroup
...HandlerFunc
/admin
v1Group := r.Group("/v1", func(c *gin.Context) {
        fmt.Println("/v1中间件")
    })
/v1/users/v1/products/v1中间件

分组路由嵌套

我们不光可以定义一个分组路由,还可以在这个分组路由中再添加一个分组路由,达到分组路由嵌套的目的,这种业务场景也不少,比如:

/v1/admin/users
/v1/admin/manager
/v1/admin/photo
adminGin
    v1AdminGroup := v1Group.Group("/admin")
    {
        v1AdminGroup.GET("/users", func(c *gin.Context) {
            c.String(200, "/v1/admin/users")
        })
        v1AdminGroup.GET("/manager", func(c *gin.Context) {
            c.String(200, "/v1/admin/manager")
        })
        v1AdminGroup.GET("/photo", func(c *gin.Context) {
            c.String(200, "/v1/admin/photo")
        })
    }
Group

原理解析

那么以前这种分组路由这么方便,实现会不会很复杂呢?我们来看看源代码,分析一下它的实现方式。

GET
// GET is a shortcut for router.Handle("GET", path, handle).
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
	return group.handle(http.MethodGet, relativePath, handlers)
}
relativePathGin
calculateAbsolutePathGroup
// Group creates a new router group. You should add all the routes that have common middlewares or the same path prefix.
// For example, all the routes that use a common middleware for authorization could be grouped.
func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroup {
	return &RouterGroup{
		Handlers: group.combineHandlers(handlers),
		basePath: group.calculateAbsolutePath(relativePath),
		engine:   group.engine,
	}
}
gin.Default()gin.EngineRouterGroupRouterGroup
gin.Default()
// Default returns an Engine instance with the Logger and Recovery middleware already attached.
func Default() *Engine {
	debugPrintWARNINGDefault()
	engine := New()
	engine.Use(Logger(), Recovery())
	return engine
}
engineengine := New()New()
// New returns a new blank Engine instance without any middleware attached.
// By default, the configuration is:
// - RedirectTrailingSlash:  true
// - RedirectFixedPath:      false
// - HandleMethodNotAllowed: false
// - ForwardedByClientIP:    true
// - UseRawPath:             false
// - UnescapePathValues:     true
func New() *Engine {
	debugPrintWARNINGNew()
	engine := &Engine{
		RouterGroup: RouterGroup{
			Handlers: nil,
			basePath: "/",
			root:     true,
		},
		FuncMap:                template.FuncMap{},
		RedirectTrailingSlash:  true,
		RedirectFixedPath:      false,
		HandleMethodNotAllowed: false,
		ForwardedByClientIP:    true,
		RemoteIPHeaders:        []string{"X-Forwarded-For", "X-Real-IP"},
		TrustedPlatform:        defaultPlatform,
		UseRawPath:             false,
		RemoveExtraSlash:       false,
		UnescapePathValues:     true,
		MaxMultipartMemory:     defaultMultipartMemory,
		trees:                  make(methodTrees, 0, 9),
		delims:                 render.Delims{Left: "{{", Right: "}}"},
		secureJSONPrefix:       "while(1);",
		trustedProxies:         []string{"0.0.0.0/0", "::/0"},
		trustedCIDRs:           defaultTrustedCIDRs,
	}
	engine.RouterGroup.engine = engine
	engine.pool.New = func() any {
		return engine.allocateContext()
	}
	return engine
}
engine := &Engine{
		RouterGroup: RouterGroup{
			Handlers: nil,
			basePath: "/",
			root:     true,
		},
RouterGroup
Group*RouterGroupbasePathgroup.calculateAbsolutePath(relativePath)
func (group *RouterGroup) calculateAbsolutePath(relativePath string) string {
	return joinPaths(group.basePath, relativePath)
}
RouterGroupbasePathGroupRouterGroupbasePath
gin.Default()gin.EnginebasePath/

这是一种非常棒的代码实现方式,简单的代码,是强大的功能。

小结

分组路由的功能非常强大, 可以帮助我们进行版本的升级,模块的切分,而且它的代码实现又非常简单,这就是优秀的代码。通过分析,我们可以学到很多,也能提升很多,让自己的能力不知不觉的进步。

下面是完整的测试代码,看不懂文章的小伙伴可以一步一步debug。

package main

import (
	"fmt"
	"github.com/gin-gonic/gin"
)

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

	//V1版本的API
	v1Group := r.Group("/v1", func(c *gin.Context) {
		fmt.Println("/v1中间件") // Goland 控制台打印
		c.String(300, "/v1中间件 \n")
	})
	v1Group.GET("/users", func(c *gin.Context) {
		c.String(200, "/v1/users")
	})
	v1Group.GET("/products", func(c *gin.Context) {
		c.String(200, "/v1/products")
	})

	//V2版本的API
	v2Group := r.Group("/v2")
	v2Group.GET("/users", func(c *gin.Context) {
		c.String(200, "/v2/users")
	})
	v2Group.GET("/products", func(c *gin.Context) {
		c.String(200, "/v2/products")
	})

	r.Run(":8080")

}