5. Middleware

Kesa...大约 3 分钟golang

1. 中间件 Middleware

中间件(middleware),是非业务的技术类组件。

Web 框架无法实现所有的业务功能,因此可以提供一个插件,允许用户自定义功能,嵌入到框架之中。

设计一个中间件有两个关键点:

  1. 插入点:若插入点过于底层,中间件逻辑将变得复杂;若插入点离用户接口太近,和用户直接在处理函数中直接调用相比优势不大
  2. 中间件的输入:输入的参数决定了系统的扩展能力,若参数太少,会降低可扩展性

1.1 设计

插入点和输入:

  1. 输入:Context对象
  2. 插入点:Context初始化之后

功能:

  1. 中间件应用于路由分组之上,子分组共享父母分组的中间件,每个分组可以使用独有的中间件
  2. 中间件可以在调用HandlerFunc之前或之后进行某些操作
  3. 多个中间件可以按次序调用

2. 实现

https://github.com/dreamjz/golang-notes/tree/main/books/7-days-golang/Gee/day5-middlewareopen in new window

项目结构如下:

DAY5-MIDDLEWARE
│  go.mod
│  go.work
│  main.go
│
└─gee
        context.go
        gee.go
        go.mod
        logger.go
        router.go
        router_group.go
        router_test.go
        trie.go

2.1 Context

type Context struct {
    Writer http.ResponseWriter
    Req    *http.Request
    // Request info
    Path   string
    Method string
    Params map[string]string // Dynamic route parameters
    // Response info
    StatusCode int
    // Middleware
    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,
	}
}
  • handlers:中间件/处理函数列表
  • index:当前执行的函数,初始值 -1
func (c *Context) Next() {
	c.index++
	n := len(c.handlers)
	for c.index < n {
		c.handlers[c.index](c)
		c.index++
	}
}

func (c *Context) Fail(code int, errMsg string) {
	c.index = len(c.handlers)
	c.JSON(code, H{"message": errMsg})
}
  • Next: 调用下一个处理函数/中间件
  • Fail:修改c.index跳过后序的中间件/处理函数的执行

2.2 gee

func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	var middlewares []HandlerFunc
	for _, group := range engine.groups {
		if strings.HasPrefix(req.URL.Path, group.prefix) {
			middlewares = append(middlewares, group.middlewares...)
		}
	}

	c := newContext(w, req)
	c.handlers = middlewares
	engine.router.handle(c)
}
  • 获取分组的中间件列表
  • 添加至当前的 Context 的handlers
  • 调用engine.router.handle

2.3 router

func (r *router) handle(c *Context) {
    n, params := r.getRoute(c.Method, c.Path)
    var handler HandlerFunc
    if n != nil {
       c.Params = params
       key := c.Method + "-" + n.pattern
       handler = r.handlers[key]
    } else {
       handler = func(c *Context) {
          c.String(http.StatusNotFound, "404 NOT FOUND: %s\n", c.Path)
       }
    }

    c.handlers = append(c.handlers, handler)
    c.Next()
}
  • 获取当前请求对应的处理函数,添加至handlers末尾
  • 调用c.Next(),开始执行handlers函数列表

2.4 logger

package gee

import (
	"log"
	"time"
)

func Logger() HandlerFunc {
	return func(c *Context) {
		start := time.Now()
		c.Next()
		log.Printf("[%d] %s in %vms", c.StatusCode, c.Req.RequestURI, time.Since(start).Milliseconds())
	}
}
  • 调用下一个函数前,获取当前时间
  • 调用c.Next()等待其他的函数执行
  • 统计执行时间

3. Demo

package main

import (
	"gee"
	"log"
	"net/http"
)

func main() {
	r := gee.New()

	r.Use(gee.Logger())

	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>Welcome v1</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)
		})

		m1 := func(c *gee.Context) {
			log.Println("Before v2 middleware#1")
			c.Next()
			log.Println("After v2 middleware#1")
		}
		m2 := func(c *gee.Context) {
			log.Println("Before v2 middleware#2")
			c.Next()
			log.Println("After v2 middleware#2")
		}

		v2.Use(m1, m2)
	}

	r.Run(":8000")
}

  • m1,m2:仅作用在v2分组上,并打印执行信息
$ curl http://localhost:8000/v2/hello/alice
Hello alice, you're at /v2/hello/alice
>>> log:
2023/10/10 11:00:53 Before v2 middleware#1
2023/10/10 11:00:53 Before v2 middleware#2
2023/10/10 11:00:53 After v2 middleware#2
2023/10/10 11:00:53 After v2 middleware#1
2023/10/10 11:00:53 [200] /v2/hello/alice in 0ms

Reference

  1. https://geektutu.com/post/gee-day5.htmlopen in new window
上次编辑于:
评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v2.15.2