仿照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框架就设置了 两个路由,为此我们可以实现路由分组最基础的功能来复刻一下。 主要是 RouterGroupEngine 两个结构体,两者之间互相递归属性,可以实现多级路由分组。

我简单用Python复刻一遍路由分组 ```py 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 复刻上面中间件执行顺序 ```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函数,中断中间件的运行。

完整github地址


文章作者: 王小右
版权声明: 咳咳想白嫖文章?本文章著作权归作者所有,任何形式的转载都请注明出处。 https://www.charmcode.cn !
还能输入 100 字
  目录