Mygin实现分组路由Group

本篇是Mygin第五篇

目的

  • 实现路由分组

为什么要分组

分组控制(Group Control)是 Web 框架应该提供的基础功能之一,对同一模块功能的开发,应该有相同的前缀。或者对一部分第三方接口,统一需要加解密等功能。分组后很方便。例如:

  • 对于任务模块,统一前缀为/task
  • 除去/user/login接口,都需要鉴权
  • 以/openapi 开头的接口,需要对接第三方平台,需要三方平台鉴权

大多数分组都是统一前缀,正确的分组可以实现子分组,无限极往下分组,当然实际情况下也不会有太多层分组。每个分组有不同的中间件(middleware),分组与子分组就像洋葱一样,一层一层往内。要想往内,需要拨开最外层,也就是要执行外层的中间件才能往内。对于分组,也有后续扩展的好处,比如/task分组,现在要统一加上访问日志记录,有了分组后就可以在该分组上添加一个中间件就可以了。

分组嵌套

在实现分组嵌套之前,先看看gin中的分组嵌套是怎么实现的。gin中的分组中含有Engin的指针,其实很好理解,因为分组要有访问Router的能力。Engin中继承了RouterGroup,实例化Engin后就可以使用Group的功能。有了Engin的指针,整个框架的所有资源都是由Engine去统一调度的,因此通过Engine可以间接地拥有整个框架的功能。你中有我我中有你。

  • gin/routergroup.go
Golang 复制代码
package mygin

import (
	"net/http"
	"path"
	"regexp"
)

// IRoutes 定义了路由组的接口
type IRoutes interface {
	BasePath() string
	GET(string, ...HandlerFunc) IRoutes
	POST(string, ...HandlerFunc) IRoutes
	DELETE(string, ...HandlerFunc) IRoutes
	PATCH(string, ...HandlerFunc) IRoutes
	PUT(string, ...HandlerFunc) IRoutes
	OPTIONS(string, ...HandlerFunc) IRoutes
	HEAD(string, ...HandlerFunc) IRoutes
	Match([]string, string, ...HandlerFunc) IRoutes
}

// anyMethods 包含所有 HTTP 方法的字符串表示
var anyMethods = []string{
	http.MethodGet, http.MethodPost, http.MethodPut, http.MethodPatch,
	http.MethodHead, http.MethodOptions, http.MethodDelete, http.MethodConnect,
	http.MethodTrace,
}

// RouterGroup 定义了路由组的结构体
type RouterGroup struct {
	Handlers HandlersChain // 路由组的中间件处理函数链
	basePath string        // 路由组的基础路径
	engine   *Engine       // 路由组所属的引擎
	root     bool          // 是否是根路由组
}

// Group 创建一个新的路由组
func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroup {
	return &RouterGroup{
		Handlers: append(group.Handlers, handlers...),
		basePath: path.Join(group.basePath, relativePath),
		engine:   group.engine,
	}
}

// BasePath 返回路由组的基础路径
func (group *RouterGroup) BasePath() string {
	return group.basePath
}

// handle 处理路由,将路由信息添加到引擎中
func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
	absolutePath := path.Join(group.basePath, relativePath)
    //处理函数,Group组定义的函数先执行,自定义的函数后执行
	handlers = append(group.Handlers, handlers...)
	group.engine.addRoute(httpMethod, absolutePath, handlers)

	if group.root {
		return group.engine
	}
	return group
}

// Handle 校验 HTTP 方法的有效性,并处理路由
func (group *RouterGroup) Handle(httpMethod, relativePath string, handlers ...HandlerFunc) IRoutes {
	// 检查 HTTP 方法的有效性
	if match := regexp.MustCompile("^[A-Z]+$").MatchString(httpMethod); !match {
		panic("http method " + httpMethod + " is not valid")
	}
	// 处理路由
	return group.handle(httpMethod, relativePath, handlers)
}

// GET 注册 GET 方法的路由
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
	return group.handle(http.MethodGet, relativePath, handlers)
}

// POST 注册 POST 方法的路由
func (group *RouterGroup) POST(relativePath string, handlers ...HandlerFunc) IRoutes {
	return group.handle(http.MethodPost, relativePath, handlers)
}

// DELETE 注册 DELETE 方法的路由
func (group *RouterGroup) DELETE(relativePath string, handlers ...HandlerFunc) IRoutes {
	return group.handle(http.MethodDelete, relativePath, handlers)
}

// PATCH 注册 PATCH 方法的路由
func (group *RouterGroup) PATCH(relativePath string, handlers ...HandlerFunc) IRoutes {
	return group.handle(http.MethodPatch, relativePath, handlers)
}

// PUT 注册 PUT 方法的路由
func (group *RouterGroup) PUT(relativePath string, handlers ...HandlerFunc) IRoutes {
	return group.handle(http.MethodPut, relativePath, handlers)
}

// OPTIONS 注册 OPTIONS 方法的路由
func (group *RouterGroup) OPTIONS(relativePath string, handlers ...HandlerFunc) IRoutes {
	return group.handle(http.MethodOptions, relativePath, handlers)
}

// HEAD 注册 HEAD 方法的路由
func (group *RouterGroup) HEAD(relativePath string, handlers ...HandlerFunc) IRoutes {
	return group.handle(http.MethodHead, relativePath, handlers)
}

// Match 注册多个方法的路由
func (group *RouterGroup) Match(methods []string, relativePath string, handlers ...HandlerFunc) IRoutes {
	for _, method := range methods {
		group.handle(method, relativePath, handlers)
	}

	if group.root {
		return group.engine
	}
	return group
}

// Any 注册所有方法的路由
func (group *RouterGroup) Any(relativePath string, handlers ...HandlerFunc) IRoutes {
	for _, method := range anyMethods {
		group.handle(method, relativePath, handlers)
	}

	if group.root {
		return group.engine
	}
	return group
}

可以看到group.engine.addRoute方法,实际调用的仍然是mygin/router.go中的addRoute方法。当group.root为true,也就是为根时,相当于没有调用Group方法,返回group.engine,也就是相当于直接调用的engine,只有当调用Group方法后才会返回group。

接下来看engine的修改

  • mygin/engine.go
Golang 复制代码
package mygin

import (
	"net/http"
)

// HandlerFunc 定义处理函数类型
type HandlerFunc func(*Context)

// HandlersChain 定义处理函数链类型
type HandlersChain []HandlerFunc

// Engine 定义引擎结构,包含路由器
type Engine struct {
	Router
	RouterGroup
}

// ServeHTTP 实现http.Handler接口的方法,用于处理HTTP请求
func (e *Engine) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	// 获取对应HTTP方法的路由树的根节点
	root := e.trees.get(r.Method)
	// 解析请求路径
	parts := root.parseFullPath(r.URL.Path)

	// 查找符合条件的节点
	searchNode := make([]*node, 0)
	root.search(parts, &searchNode)

	// 没有匹配到路由
	if len(searchNode) == 0 {
		w.Write([]byte("404 Not found!\n"))
		return
	}

	// 参数赋值
	params := make([]Param, 0)
	searchPath := root.parseFullPath(searchNode[0].fullPath)
	for i, sp := range searchPath {
		if sp[0] == ':' {
			params = append(params, Param{
				Key:   sp[1:],
				Value: parts[i],
			})
		}
	}

	// 获取处理函数链
	handlers := searchNode[0].handlers
	if handlers == nil {
		w.Write([]byte("404 Not found!\n"))
		return
	}

	// 执行处理函数链
	for _, handler := range handlers {
		handler(&Context{
			Request: r,
			Writer:  w,
			Params:  params,
		})
	}
}

// Default 返回一个默认的引擎实例
func Default() *Engine {
	engine := &Engine{
		Router: Router{
			trees: make(methodTrees, 0, 9),
		},
		RouterGroup: RouterGroup{
			Handlers: nil,
			basePath: "/",
			root:     true,
		},
	}

	// Group 保存 engine 的指针
	engine.RouterGroup.engine = engine

	return engine
}

// Run 启动HTTP服务器的方法
func (e *Engine) Run(addr string) error {
	return http.ListenAndServe(addr, e)
}

最后来测试一下

  • main.go
Golang 复制代码
package main

import (
	"gophp/mygin"
	"path"
)

func main() {
	// 创建一个默认的 mygin 实例
	r := mygin.Default()

	group := r.Group("/api", func(context *mygin.Context) {
		context.String("api Group ....\n")
	})

	group.GET("/hello/:name", func(context *mygin.Context) {
		name := context.Params.ByName("name")
		context.String(path.Join("hello ", name, "!"))
	})

	group.GET("/hello2/:name", func(context *mygin.Context) {
		name := context.Params.ByName("name")
		context.String(path.Join("hello2 ", name, "!\n"))
	})

	// 启动服务器并监听端口
	r.Run(":8088")
}

curl请求测试

shell 复制代码
 curl -i http://localhost:8088/api/hello/scott
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Date: Wed, 24 Jan 2024 05:16:45 GMT
Content-Length: 29

api Group ....
hello /scott/!
~ curl -i http://localhost:8088/api/hello2/scott
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Date: Wed, 24 Jan 2024 05:16:54 GMT
Content-Length: 31

api Group ....
hello2 /scott/!

可以看到两个路由都安装预定的返回了,即先返回Group定义的,再返回路由定义的。

相关推荐
xiaocaibao77712 小时前
编程语言的软件工程
开发语言·后端·golang
xiaocaibao77715 小时前
Java语言的网络编程
开发语言·后端·golang
程序猿-瑞瑞19 小时前
24 go语言(golang) - gorm框架安装及使用案例详解
开发语言·后端·golang·gorm
慕城南风1 天前
Go语言中的defer,panic,recover 与错误处理
golang·go
LeonNo111 天前
golang , chan学习
开发语言·学习·golang
龙门吹雪1 天前
GO语言基础面试题
golang·面试题·map·channel·
zyh_0305212 天前
GIN中间件
后端·golang·gin
007php0072 天前
Go语言zero项目部署后启动失败问题分析与解决
java·服务器·网络·python·golang·php·ai编程
MClink3 天前
Go怎么做性能优化工具篇之pprof
开发语言·性能优化·golang
m0_748254663 天前
go官方日志库带色彩格式化
android·开发语言·golang