仿照Go web框架gin手写自己的web框架 【下】
最后,就差几个核心功能了,路由分组,中间件,异常恢复。
声明: 三部曲文章主要参考: https://geektutu.com/post/gee.html
直接把这三个核心的点放到整体代码中,不好理解,所以我打算把这三个单独拆出来,单独分析。
路由分组
实现类似gin
框架的这种效果,实例对象可以直击调用GET
, 路由分组的对象也可以调用 GET
方法,并且分组路由是有层级继承关系
package main
import (
"github.com/gin-gonic/gin"
)
func main() {
r := gin.New()
r.GET("/", func(context *gin.Context) {
context.JSON(200, gin.H{
"index": "首页",
})
})
v1 := r.Group("/v1")
v1.GET("/api", func(context *gin.Context) {
context.JSON(200, gin.H{
"/v1/api": "分组路由",
})
})
_ = r.Run("127.0.0.1:7000")
}
简单实现一个核心路由分组的代码 本段代码GitHub地址
package main
import (
"fmt"
"testing"
)
// 路由分组
// https://github.com/gin-gonic/gin/blob/master/routergroup.go#L41
type RouterGroup struct {
prefix string // 前缀
engine *Engine // 内部的Engine始终保证为共享的一个
parent *RouterGroup // 父路由
}
// 核心
type Engine struct {
*RouterGroup // 使Engine 也拥有RouterGroup的方法
}
// Get方法 这里没有实现响应处理函数
func (group *RouterGroup) Get(pattern string) {
fmt.Printf("Get %s%s\n", group.prefix, pattern)
}
// 创建路由分组
func (group *RouterGroup) Group(prefix string) *RouterGroup {
return &RouterGroup{
prefix: group.prefix + prefix, // 前缀为上一个 路由分组前缀 加下一个
parent: group, // 当前路由设置为父路由
}
}
func New() *Engine {
engine := &Engine{}
// RouterGroup里面的 engine属性为 自身的engine 确保所有的engine 为一个
engine.RouterGroup = &RouterGroup{engine: engine}
return engine
}
func TestRouter(t *testing.T) {
// 创建实例
r := New()
r.Get("/index")
// 路由分组
api := r.Group("/api")
api.Get("/123")
// api子路由分组
v1 := api.Group("/v1")
v1.Get("/666")
// 输出
// Get /index
// Get /api/123
// Get /api/v1/666
}
上面gin
框架就设置了 两个路由,为此我们可以实现路由分组最基础的功能来复刻一下。
主要是 RouterGroup
和 Engine
两个结构体,两者之间互相递归属性,可以实现多级路由分组。
我简单用Python复刻一遍路由分组
class RouterGroup(object):
def __init__(self):
self.prefix = "" # 路由前缀
def group(self, *, prefix):
self.prefix = self.prefix + prefix
return self
def http_get(self, pattern: str):
print(f"GET {self.prefix}{pattern}")
class Engine(RouterGroup):
def __init__(self):
super(Engine, self).__init__()
self.RouterGroup = RouterGroup
e = Engine()
e.http_get("/api")
v1_router = e.group(prefix="/v1")
v1_router.http_get("/666")
v1_auth = v1_router.group(prefix="/auth")
v1_auth.http_get("/admin")
# GET /api
# GET /v1/666
# GET /v1/auth/admin
中间件功能核心实现
首先中间件功能呢,形象的比喻就好像拿东西穿洋葱一样,从一边进去,先穿过一层层的外壁,然后经过内芯,最后再穿过一层层外壁出来。
伪代码就好像这样A和B表示两个中间件函数, c表示请求上下文信息。c.Next()
表示交出执行权,给下一个函数执行,执行完了之后再执行这个。
func A(c *Context){
fmt.Println("A--1")
c.Next()
fmt.Println("A--2")
}
func B(c *Context){
fmt.Println("B-------1")
c.Next()
fmt.Println("B-------2")
}
所以上面的函数执行输出路径,我们期望是这样的,和穿洋葱的例子结合起来就非常形象了。
// A--1
// B-------1
// 请求处理函数
// B-------2
// A--2
// 最终执行顺序就是: A1-> B1 -> 处理函数 -> B2 -> A1
所以最终函数就如下所示
package main
import (
"fmt"
"testing"
)
type HandlerFunc func(c *Context)
// HandlersChain defines a HandlerFunc array.
type HandlersChain []HandlerFunc
// 管理中间件的上下文
type Context struct {
// middleware
handlers HandlersChain //存储顺序 [中间件1, 中间件2, 中间件3...., 请求处理函数]
index int
}
func newContext() *Context {
return &Context{
// gin 里面初始化变量
// https://github.com/gin-gonic/gin/blob/master/context.go#L91
index: -1,
}
}
// 下一步调用
func (c *Context) Next() {
// 如果把 Next() c.index++ 去掉,就会一直卡在第一个中间件上了,必然死循环。
// 每调用一次 Next(), c.index 必须得 +1,否则 c.index 就会一直是 0
// 参考gin https://github.com/gin-gonic/gin/blob/master/context.go#L163
c.index++ // 0
s := len(c.handlers) // 3
for ; c.index < s; c.index++ {
c.handlers[c.index](c)
}
}
// 直接中断响应
func (c *Context) Fail(code int, err string) {
// 直接让计数下角标 == 中间件数组长度, 即上Next的 index < 3 不成立
c.index = len(c.handlers)
// 返回响应信息等 操作
fmt.Println(err)
}
// A 中间件
func middlewareA(c *Context) {
fmt.Println("A--1")
//c.Fail(500, "中断操作")
c.Next()
fmt.Println("A--2")
}
// B 中间件
func middlewareB(c *Context) {
fmt.Println("B-------1")
c.Next()
fmt.Println("B-------2")
}
// 请求处理到函数
func handleFunc(c *Context) {
fmt.Println("请求处理函数")
}
func TestMiddleware(t *testing.T) {
n := newContext()
n.handlers = append(n.handlers, middlewareA)
n.handlers = append(n.handlers, middlewareB)
n.handlers = append(n.handlers, handleFunc)
// 启动
n.Next()
}
Python 复刻上面中间件执行顺序
class Context(object):
def __init__(self):
self.handles = []
self.index = -1
def func_next(self):
self.index += 1
s = len(self.handles)
while self.index < s:
self.handles[self.index](self)
self.index += 1
def middleware_a(c: Context):
print("A--1")
c.func_next()
print("A--2")
def middleware_b(c: Context):
print("B-----1")
c.func_next()
print("B-----2")
def func_task(c: Context):
print("请求处理的函数")
ctx = Context()
ctx.handles.append(middleware_a)
ctx.handles.append(middleware_b)
ctx.handles.append(func_task)
ctx.func_next()
# A--1
# B-----1
# 请求处理的函数
# B-----2
# A--2
异常恢复机制
异常恢复这个可以参考gin
框架,是基于中间件 engine.Use(Logger(), Recovery())
见代码 https://github.com/gin-gonic/gin/blob/master/gin.go#L162
所以实现起来也不复杂,明白Go
的异常恢复机制即可
新建一个recovery.go
文件
//Go错误捕获机制参考
// https://stackoverflow.com/questions/35212985/is-it-possible-get-information-about-caller-function-in-golang
package gee
import (
"fmt"
"log"
"net/http"
"runtime"
"strings"
)
// print stack trace for debug
func trace(message string) string {
var pcs [32]uintptr
n := runtime.Callers(3, pcs[:]) // skip first 3 caller
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()
}
}
只用像gin
一样在中间件里面使用这个 异常恢复中间件函数即可。
然后可以把路由分组,和 中间件这两大功能整理到 项目中。
最终代码目录结构如下
- gee/
- context.go // 上下文
- gee.go // gee核心函数
- recovery.go // 处理异常恢复中间件 并且trace错误
- router.go // 路由处理
- trie.go // 前缀树路由 (这一点实现比较复杂,可以参考 https://geektutu.com/post/gee-day3.html)
最终代码过长,但是核心两个方法我都在上面精简出来了,然后就是中间件函数的顺序添加,用了Use
函数封装, 添加一个Fail
函数,中断中间件的运行。