geektutu / blog

极客兔兔的博客,Coding Coding 创建有趣的开源项目。
https://geektutu.com
Apache License 2.0
168 stars 21 forks source link

Go语言动手写Web框架 - Gee第五天 中间件Middleware | 极客兔兔 #45

Open geektutu opened 5 years ago

geektutu commented 5 years ago

https://geektutu.com/post/gee-day5.html

7天用 Go语言 从零实现Web框架教程(7 days implement golang web framework from scratch tutorial),用 Go语言/golang 动手写Web框架,从零实现一个Web框架,以 Gin 为原型从零设计一个Web框架。本文介绍了如何为Web框架添加中间件的功能(middlewares)。

NoahCodeGG commented 2 years ago

@Nick233333 目前的中间件设计无法支持指定路由使用中间件吧

文章中已经说的很明白了---我们上一篇文章分组控制 Group Control中讲到,中间件是应用在RouterGroup上的,应用在最顶层的 Group,相当于作用于全局,所有的请求都会被中间件处理。那为什么不作用在每一条路由规则上呢?作用在某条路由规则,那还不如用户直接在 Handler 中调用直观。只作用在某条路由规则的功能通用性太差,不适合定义为中间件。

NoahCodeGG commented 2 years ago

@ZAKLLL 这个请求url匹配中间件,放在路由做可能更合适,在ServeHTTP 通过前缀匹配,对于那些/:xxx/ 模糊路径的pattern是无法正常匹配上的,但是如果放在route中,先将url从router中获取对应的解析后的route,通过目标router.pattern与 中间件进行匹配更合理一点也能匹配到模糊路径了。

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)
}

//在路由进行中间件匹配处理
func (r *router) handle(c *Context) {
  route, params := r.getRoute(c.Method, c.Path)
  if route != nil {
      //用route 来跟分组前缀匹配
      for _, group := range r.engine.groups {
          if strings.HasPrefix(route.pattern, group.prefix) {
              c.handlers = append(c.handlers, group.middlewares...)
          }

      }

      key := c.Method + "-" + route.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)
      })
  }
  c.Next()
}

这种特殊情况我觉得吧,对于一个框架来说是没必要去考虑的,本身先是匹配的 group 的 prefix,连 group 都模糊了,这要么将事务放到全局中间件去做,要么改变路由单独分组去处理

krau233 commented 1 year ago

请问为什么中间件定义的时候要使用返回匿名函数的形式而不是直接写成不匿名的函数将该函数传递

xuyanshi commented 1 year ago

@krau233 请问为什么中间件定义的时候要使用返回匿名函数的形式而不是直接写成不匿名的函数将该函数传递

多套了一层,可能是为了方便重构或复用?

shyn0121 commented 1 year ago

88

0xWhitePages commented 1 year ago

人晕了

GuYith commented 1 year ago

捉个小虫,本章的代码路径都写成了day4-group(应该是day5-middleware),不过链接都是对的:)

big-dust commented 11 months ago
func (c *Context) Next() {
    c.index++
    s := len(c.handlers)
    for ; c.index < s; c.index++ {
        c.handlers[c.index](c)
    }
}

请问对于Next函数,如果有3个中间价(包括handler),在第1个和第2个中间价中都调用了Next(),第3个handler没有调用Next(),那么第三个handler是不是会被调用两次?

Fencent commented 10 months ago

@big-dust

func (c *Context) Next() {
  c.index++
  s := len(c.handlers)
  for ; c.index < s; c.index++ {
      c.handlers[c.index](c)
  }
}

请问对于Next函数,如果有3个中间价(包括handler),在第1个和第2个中间价中都调用了Next(),第3个handler没有调用Next(),那么第三个handler是不是会被调用两次?

不会的,这是递归进行,当第三个调用完就返回第二个执行Next()后面的去了,第二个执行完会返回第一个执行Next()后面的,然后结束

big-dust commented 10 months ago

@Fencent

@big-dust

func (c *Context) Next() {
    c.index++
    s := len(c.handlers)
    for ; c.index < s; c.index++ {
        c.handlers[c.index](c)
    }
}

请问对于Next函数,如果有3个中间价(包括handler),在第1个和第2个中间价中都调用了Next(),第3个handler没有调用Next(),那么第三个handler是不是会被调用两次?

不会的,这是递归进行,当第三个调用完就返回第二个执行Next()后面的去了,第二个执行完会返回第一个执行Next()后面的,然后结束

明白了,谢谢!

SCUTking commented 9 months ago

@big-dust

@Fencent

@big-dust

func (c *Context) Next() {
  c.index++
  s := len(c.handlers)
  for ; c.index < s; c.index++ {
      c.handlers[c.index](c)
  }
}

请问对于Next函数,如果有3个中间价(包括handler),在第1个和第2个中间价中都调用了Next(),第3个handler没有调用Next(),那么第三个handler是不是会被调用两次?

不会的,这是递归进行,当第三个调用完就返回第二个执行Next()后面的去了,第二个执行完会返回第一个执行Next()后面的,然后结束

明白了,谢谢!

这个index起的作用,因为单线程执行,所以调用到handler时,index已经是2了。再返回到第一个的Next会因为index不在满足index<s而退出循环。

jiechen257 commented 9 months ago

你的 Fail 函数没有定义,我自行写了下:

// Fail 在发生错误时返回 HTTP 响应
func (c *Context) Fail(status int, message string) {
    c.Writer.WriteHeader(status)
    c.Writer.Write([]byte(message))
}
caoyukun0430 commented 3 months ago

你的 Fail 函数没有定义,我自行写了下:

// Fail 在发生错误时返回 HTTP 响应
func (c *Context) Fail(status int, message string) {
  c.Writer.WriteHeader(status)
  c.Writer.Write([]byte(message))
}

Fail看这里 https://github.com/geektutu/7days-golang/blob/master/gee-web/day5-middleware/gee/context.go#L44

lv997 commented 3 months ago

@big-dust

func (c *Context) Next() {
  c.index++
  s := len(c.handlers)
  for ; c.index < s; c.index++ {
      c.handlers[c.index](c)
  }
}

请问对于Next函数,如果有3个中间价(包括handler),在第1个和第2个中间价中都调用了Next(),第3个handler没有调用Next(),那么第三个handler是不是会被调用两次?

这个context.Next()方法设计的非常巧妙。通过将middlewares“请求本身需要处理的handler" 按照顺序存放在context.handlers中。然后根据context.index 依次取出里面的handler执行。因为你前面两个都调用了next()此时index已经来到了"-1 + 1 + 1 + 1 = 2",在调用handler3的时候指针又++了,变成了3, 此时跳出循环,没有更多的context.handlers执行了。 顺序表+递归的方式非常好的模拟了洋葱模型。试着在你主动调用next()的方法后面加入更多代码用于模拟后置中间键,非常好用。

coderZoe commented 2 months ago

在本章中,对于中间件的串行执行,我们使用了一个Next方法,这里有必要对这个方法进行详细解释:

func (c *Context) Next() {
    c.index++
    s := len(c.middleware)
    for ; c.index < s; c.index++ {
        c.middleware[c.index](c)
    }
}

如上,如果我们假设中间件的写法都是:

func(ctx *gee.Context) {
    //do something
    ctx.Next()
    //do something
}

也即每个中间件都调用ctx.Next(),那我们其实可以将上述Next改为

func (c *Context) Next() {
    c.index++
    if(c.index < len(c.middleware)){
        c.middleware[c.index](c)
    }
}

如上不需要循环,因为每个middleWare里都调用了ctx.Next(),这个链式调用会一直走下去 当走到最后一个节点,又会由于函数调用入栈的原因,调用结束后会出栈反向执行

但很明显,我们得考虑中间件中无ctx.Next()的情况,如果中间件中无ctx.Next(),上述版本就会出现调用断掉 因此我们得加for,手动遍历循环这些middleware,避免middleware中断掉的情况

这里存在一个容易疑惑的点是: 假设T1 开始执行Next()index变为0,然后进入for,for内看着似乎会遍历所有的middleware,假设执行到T2 某个middleware内也包含Next()调用,此时再次进入Next(),又开启了一次for循环,那会不会导致部分middleware重复执行呢?

我们做个假设,现在有4个middleware A、B、C、D,那加上原本的路由处理函数h,我们就得到了5个handler。 从最极端的假设开始,A、B、C、D内部都调用了Next(),它们的结构如下:

func(ctx *gee.Context) {
    before()
    ctx.Next()
    after()
}

首先路由请求,进入Next(),此时index++变为0,然后开始for循环第一次迭代,for循环先执行middleware[0]也即A,A内调用Next()index++更新为1,然后开启第二个for循环第一次迭代,由于此时index == 1,因此执行B,重复如此,直到D,D内调用Next()的时候,index被更新为4,因此本次for执行的是middleware[4]也即路由处理函数h,

//index为4 s为5
for ; c.index < s; c.index++ {
    //c.middleware[c.index]是h
    c.middleware[c.index](c)
}

h执行完成后,for循环迭代c.index++,此时index被更新为5,不满足for循环退出,然后回到它的调用点,也即回到D的Next()执行后,D执行after(),执行完后,for循环迭代c.index++,此时index被更新为6,同样不满足迭代返回到调用点,此时到C的after(),重复迭代一直到A的after(),最后整个middleware执行完毕。

所以上面这种情况,虽然会多次进入for循环,但每个for其实只会迭代一次,不会重复执行middleware

再考虑另一个情况,假设B和C内都无Next()调用,我们再分析会发生什么:

首先路由请求,进入Next(),此时index++变为0,然后开始for循环第一次迭代,for循环先执行middleware[0]也即A,A内调用Next()index++更新为1,然后开启第二个for循环第一次迭代,由于此时index == 1,因此执行B,B内无Next()因此B执行完成后控制权回到for,for循环迭代c.index++,此时index被更新为2,然后满足for继续迭代,此时执行C,同样C内无Next(),执行完后,for循环迭代c.index++index被更新为3,然后满足for继续迭代,开始执行D, D内包含Next()调用Next()的时候index被更新为4,执行f,f执行完成,index被更新为5,不满足继续迭代,控制权回到D的after(),D的after()执行完成,index被更新为6, 不满足继续迭代,控制权此时回到A,A执行after(),index被更新为6,不满足继续迭代,整个流程执行完成。

可以看到,我们总结下就是,对于中间件中不包含Next()调用的,是由for循环的迭代c.index++来实现调用下一个middleware的,而中间件如果包含Next(),则是通过进入Next一开始的那行c.index++实现调用下一middleware的

ToTrooo commented 3 weeks ago

为什么用log.Printf会在每个路由都打印东西啊 image

zhengyunkun commented 1 week ago

github源码中第五天的middleware正常跑的时候中间件Logger()并没有被调用,curl http://localhost:9999/ 的时候只输出了html,似乎没有执行Logger(),github代码是有bug吗?