四、分组控制Group
1.分组的目的
在我前一篇文章介绍的 singo
框架中,有这么一段代码
v1 := r.Group("/api/v1")
{
v1.POST("ping", api.Ping)
// 用户注册
v1.POST("user/register", api.UserRegister)
// 用户登录
v1.POST("user/login", api.UserLogin)
// 需要登录保护的
auth := v1.Group("")
auth.Use(middleware.AuthRequired())
{
// User Routing
auth.GET("user/me", api.UserMe)
auth.DELETE("user/logout", api.UserLogout)
}
}
可以看到它给路由进行了一个分组,处于v1这个分组下面的路由都拥有共同的/api/v1
前缀,而在auth组中的路由,则全部需要经过登录才能访问。所以我们得到了分组的目的:(1)统一管理;(2)权限控制。
2.Group对象
分析完分组的目的以后,现在来探讨以下Group
这个对象应该具有的属性。在上面的代码中看出,它首先要有前缀,例如/api/v1
;接着,它可以使用一些例如AuthRequired
的中间件,需要有中间件属性;如果要支持分组嵌套,它还必须知道当前分组的parent是谁;最后,Group对象还需要有访问Router
的能力。因此,我们在/gee/gee.go
中对Group
的定义如下:
type RouterGroup struct {
prefix string
middlewares []HandlerFunc // support middleware
parent *RouterGroup // support nesting
engine *Engine // all groups share a Engine instance
}
接着,将Engine
作为最顶层的分组,其代码修改为如下:
type Engine struct {
*RouterGroup
router *router
groups []*RouterGroup // store all groups
}
然后更改所有和路由有关的函数,统一交给RouterGroup
实现
func New() *Engine {
engine := &Engine{router: newRouter()}
engine.RouterGroup = &RouterGroup{engine: engine}
engine.groups = []*RouterGroup{engine.RouterGroup}
return engine
}
func (group *RouterGroup) Group(prefix string) *RouterGroup {
engine := group.engine
newGroup := &RouterGroup{
prefix: group.prefix + prefix,
parent: group,
engine: engine,
}
engine.groups = append(engine.groups, newGroup)
return newGroup
}
func (group *RouterGroup) addRoute(method string, comp string, handler HandlerFunc) {
pattern := group.prefix + comp
log.Printf("Route %4s - %s", method, pattern)
group.engine.router.addRoute(method, pattern, handler)
}
func (group *RouterGroup) GET(pattern string, handler HandlerFunc) {
group.addRoute("GET", pattern, handler)
}
func (group *RouterGroup) POST(pattern string, handler HandlerFunc) {
group.addRoute("POST", pattern, handler)
}
最后,main.go
!
func main() {
r := gee.New()
r.GET("/index", func(c *gee.Context) {
c.HTML(http.StatusOK, "<h1>Index Page</h1>")
})
v1 := r.Group("/v1")
{
v1.GET("/", func(c *gee.Context) {
c.HTML(http.StatusOK, "<h1>Hello Gee</h1>")
})
v1.GET("/hello", func(c *gee.Context) {
c.String(http.StatusOK, "hello %s, you're at %s\n", c.Query("name"), c.Path)
})
}
v2 := r.Group("/v2")
{
v2.GET("/hello/:name", func(c *gee.Context) {
c.String(http.StatusOK, "hello %s, you're at %s\n", c.Param("name"), c.Path)
})
v2.POST("/login", func(c *gee.Context) {
c.JSON(http.StatusOK, gee.H{
"username": c.PostForm("username"),
"password": c.PostForm("password"),
})
})
}
r.Run(":9999")
}
五、中间件Middleware
1.中间件的设计
参照Gin
框架的设计,中间件的定义与路由映射的Handler 一致,处理的输入是Context
对象。插入点是框架接收到请求初始化Context
对象后,允许用户使用自己定义的中间件做一些额外的处理,例如记录日志等,以及对Context
进行二次加工。另外通过调用(*Context).Next()
函数,中间件可等待用户自己定义的 Handler
处理结束后,再做一些额外的操作,例如计算本次处理所用时间等。
因此我们需要给gee/context.go
添加额外参数,并新增Next()
方法:
type Context struct {
Writer http.ResponseWriter
Req *http.Request
Path string
Method string
StatusCode int
Params map[string]string
// 中间件部分
handlers []HandlerFunc
index int
}
func newContext(w http.ResponseWriter, req *http.Request) *Context {
return &Context{
Writer: w,
Req: req,
Path: req.URL.Path,
Method: req.Method,
index: -1,
}
}
func (c *Context) Next() {
c.index++
s := len(c.handlers)
for ; c.index < s; c.index++ {
c.handlers[c.index](c)
}
}
在这段代码中,index
记录了当前执行到的第几个中间件,在中间件调用Next()
后,权限就交给下一个中间件控制,直到调用到最后一个中间件,然后再从后往前,依次调用每个中间件在Next()
方法后定义的部分。让我们来举例说明一下:
func A(c *Context) {
part1
c.Next()
part2
}
func B(c *Context) {
part3
c.Next()
part4
}
以上代码定义了A、B两个中间件,并假装定义了路由映射的Handler
,此时c.handlers
为[A,B,Handler],c.index
初始化为-1,接下来走代码:
- c.index++变为0,0<len(c.handlers)=3,调用c.handlers[0]即A
- A首先执行part1,接着调用
c.Next()
,此时c.index++变为1,1<3,调用c.handlers[1]即B - B执行part3,接着调用
c.Next()
,此时c.index++变为2,2<3,调用c.handlers[2]即Handler - Handler调用完毕以后,回到B中执行part4,part4执行完毕后,回到A中执行part2,part2执行完毕,结束。
所以执行顺序总结为part1->part3->Handler->part4->part2
。
下面要在gee/gee.go
中定义Use()
函数,将中间件应用到Group
里面:
func (group *RouterGroup) Use(middlewares ...HandlerFunc) {
group.middlewares = append(group.middlewares, middlewares...)
}
func (e *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
var middlewares []HandlerFunc
for _, group := range e.groups {
if strings.HasPrefix(req.URL.Path, group.prefix) {
middlewares = append(middlewares, group.middlewares...)
}
}
c := newContext(w, req)
c.handlers = middlewares
e.router.handle(c)
}
接着修改gee/router.go
:
func (r *router) handle(c *Context) {
n, params := r.getRoute(c.Method, c.Path)
if n != nil {
key := c.Method + "-" + n.pattern
c.Params = params
c.handlers = append(c.handlers, r.handlers[key])
} else {
c.handlers = append(c.handlers, func(c *Context) {
c.String(http.StatusNotFound, "404 NOT FOUND: %s\n", c.Path)
})
}
}
为了测试,在gee/logger.go
编写一个打印日志的中间件:
func Logger() HandlerFunc {
return func(c *Context) {
t := time.Now()
c.Next()
log.Printf("[%d] %s in %v", c.StatusCode, c.Req.RequestURI, time.Since(t))
}
}
最后,开始main.go
测试!
func main() {
r := gee.New()
r.Use(gee.Logger())
r.GET("/", func(c *gee.Context) {
c.HTML(http.StatusOK, "<h1>Hello Gee</h1>")
})
r.Run(":9999")
}
六、模板Template
由于现在开发基本都是前后端分离的模式,采用Go去渲染Template已经不太常用了,所以这一部分跳过。
七、错误恢复
前面几个模块的代码已经是较为完善的Web框架了,但唯有一点没有考虑全,就是对于错误的处理。如果用户输入了一些不正常的参数,导致数组越界、空指针等,服务就会因为错误直接宕机,这显然不是我们想看到的情况。
因此,需要添加一个错误处理机制,在发生错误时,向用户返回Internet Server Error
,并且在日志中打印必要的错误信息。由于已经实现了中间件机制,所以可以把对于错误的处理当作中间件使用。
下面编写gee/recovery.go
文件,实现错误处理:
func trace(message string) string {
var pcs [32]uintptr
n := runtime.Callers(3, pcs[:])
var str strings.Builder
str.WriteString(message + "\nTraceback:")
for _, pc := range pcs[:n] {
fn := runtime.FuncForPC(pc)
file, line := fn.FileLine(pc)
str.WriteString(fmt.Sprintf("\n\t%s:%d", file, line))
}
return str.String()
}
func Recovery() HandlerFunc {
return func(c *Context) {
defer func() {
if err := recover(); err != nil {
message := fmt.Sprintf("%s", err)
log.Printf("%s\n\n", trace(message))
c.Fail(http.StatusInternalServerError, "Internal Server Error")
}
}()
c.Next()
}
}
反正我是看不懂这个trace()
,不妨来看看chatgpt的解释:
这段代码实现了一个 golang web 框架中的异常恢复中间件,也就是 Recovery()
函数。当 web 请求处理函数在执行过程中出现 panic (恐慌)时,该函数会从 panic 中恢复,并记录下导致 panic 的错误信息,以及调用栈中每个函数所在的代码文件和行数,并打印到日志中。最后,函数会将返回的 http 状态码设为500,表示服务器出现异常错误。
函数 trace(message string) string
的作用是返回当前的调用栈信息,以便后续的日志记录使用。该函数会调用 runtime
包和 strings
包实现,首先通过 runtime.Callers
方法获取到当前调用栈中的所有函数地址,然后通过遍历这些地址可以得到每个地址所对应的函数信息,从而获取到函数所在的代码文件名和行号。最后,函数将这些信息拼接成一个字符串返回。
函数 Recovery() HandlerFunc
返回的是一个中间件函数,该函数在 web 请求处理函数执行过程中起到拦截和恢复作用。该函数会在 defer
关键字中启用一个匿名函数,该匿名函数包含了异常恢复的逻辑。具体来说,如果在 web 请求处理函数的执行过程中出现了 panic(恐慌)异常,那么该函数会从 panic 中恢复并调用 log.Printf() 方法打印日志,以记录导致 panic 的错误信息以及调用栈信息。最后,该函数将 http 的响应状态设置为500,表示服务器出现异常错误。
接着,给gee/gee.go
添加一个Default()
方法,默认配置好设置的Logger
和Recovery
中间件:
func Default() *Engine {
engine := New()
engine.Use(Logger(), Recovery())
return engine
}
至此,Gee
框架编写完成,让我们main.go
测试一下!
func main() {
r := gee.Default()
r.GET("/", func(c *gee.Context) {
c.HTML(http.StatusOK, "<h1>Hello Gee</h1>")
})
r.GET("/panic", func(c *gee.Context) {
names := []string{"sanjin"}
c.String(http.StatusOK, names[100])
})
r.Run(":9999")
}
感想
跟着大神的博客一路敲下来,惊讶于实现一个简易的 Web 框架竟然这么简单,满打满算没有多少代码。但是自己确实没有真正吸收多少,基本都是看一行抄一行,这篇博客就当做个笔记吧,我也会经常回头来看看,好好体会这里面的设计思想,以及如何利用代码实现这个框架。