仿照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复刻一遍路由分组
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函数,中断中间件的运行。

完整github地址

posted @ 2021-02-04 16:39  王小右  阅读(391)  评论(0编辑  收藏  举报