Open geektutu opened 4 years ago
func (c *Context) Next() {
c.index++
s := len(c.handlers)
for ; c.index < s; c.index++ {
fmt.Printf("index:%d\n", c.index)
c.handlers[c.index](c)
}
}
您好,想请教下为什么这个Next函数需要遍历c.handlers?因为我看hadlers函数会带c.Next(),如下Logger:
func Logger() HandlerFunc {
return func(c *Context) {
// Start timer
t := time.Now()
// Process request
c.Next()
// Calculate resolution time
log.Printf("[%d] %s in %v", c.StatusCode, c.Req.RequestURI, time.Since(t))
}
}
所以,为什么不直接这样写?
func (c *Context) Next() {
c.index++
c.handlers[c.index](c)
}
感谢!!!
@acaibird 不是所有的handler都会调用 Next()
。
手工调用 Next()
,一般用于在请求前后各实现一些行为。如果中间件只作用于请求前,可以省略调用Next()
,算是一种兼容性比较好的写法吧。
@geektutu @acaibird 不是所有的handler都会调用
Next()
。 手工调用Next()
,一般用于在请求前后各实现一些行为。如果中间件只作用于请求前,可以省略调用Next()
,算是一种兼容性比较好的写法吧。
嗯嗯,明白了,多谢!
func (c *Context) Next() { c.index++ s := len(c.handlers) for ; c.index < s; c.index++ { c.handlers[c.index](c) } }
c.index 为啥要初始化为-1
我初始化为0 去掉c.index++ 一直死循环
@blackher Next()
函数一开始调用了 c.index++
,下标恰好从 0 开始。
@geektutu 我的意思是 index 直接初始化为0 Next()就不用c.index++了 我自己尝试初始化为0 去掉c.index++ 一直死循环
你把Next()
第一行的 c.index++
去掉,就会一直卡在第一个中间件上了,必然死循环。每调用一次 Next()
,c.index
得 +1,不然 c.index
就会一直是 0。
死循环之后,是不会走到 for 语句后面的 c.index++
的。
@geektutu 提问一下,这样写不是会凡是用到 onlyForV2的 都会报500的错吗?,应该不是遇到错误报500吧?
Context Fail 方法
func (c *Context) Fail(code int, err string) {
c.index = len(c.handlers)
c.JSON(code, H{"message": err})
}
c.index = len(c.handlers)
可否置为 c.index = len(c.handlers) - 1
func A(c *Context) {
// 中间件上半部
part1
c.Next()
// 中间件下半部
part2
}
感觉这样解释比较容易理解
首先谢谢博主,文章写得很好,思路很清晰。 然后我有个问题,你这个middleware的实现执行是无序的。所以是不是把middleware放在trie的node里面,然后在search的时候collect所有的middlewares,这样更好?
@proverbs 首先谢谢博主,文章写得很好,思路很清晰。 然后我有个问题,你这个middleware的实现执行是无序的。所以是不是把middleware放在trie的node里面,然后在search的时候collect所有的middlewares,这样更好?
代码看来,因为routerGroups中所有的RouterGroup,在添加时肯定是按照先父再子的顺序添加的,所以从头到尾遍历routerGroups这种简单的收集方式,也就是达成了实际路径中从左向右的查找过程。 所以顺序是可以保证的。
当然这种收集方式有个前提:分组中所有的前缀必须是静态路径,否则使用实际路由从全部分组做前缀匹配,无法保证收集的完整性。这也给上节的分组控制的使用加了限制。
如果我理解错了,还请指出。
博主,能否请教一个基础问题,为什么Context中,Writer不用指针,而Req却要用指针呢?
@void1104 这里和 http ServerHTTP 函数原始的两个参数对应 req 和 writer。req 是结构体,用指针可以节省内存,Writer 是一个接口类型,不能用指针。
package http
type Handler interface {
ServeHTTP(w ResponseWriter, r *Request)
}
理解了,谢谢大佬
------------------ 原始邮件 ------------------ 发件人: "geektutu/geektutu-blog" <notifications@github.com>; 发送时间: 2020年12月1日(星期二) 中午1:23 收件人: "geektutu/geektutu-blog"<geektutu-blog@noreply.github.com>; 抄送: "彭家鑫"<731009209@qq.com>;"Mention"<mention@noreply.github.com>; 主题: Re: [geektutu/geektutu-blog] Go语言动手写Web框架 - Gee第五天 中间件Middleware | 极客兔兔 (#45)
@void1104 这里和 http ServerHTTP 函数原始的两个参数对应 req 和 writer。req 是结构体,用指针可以节省内存,Writer 是一个接口类型,不能用指针。 package http type Handler interface { ServeHTTP(w ResponseWriter, r *Request) }
— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub, or unsubscribe.
@geektutu @acaibird 不是所有的handler都会调用
Next()
。 手工调用Next()
,一般用于在请求前后各实现一些行为。如果中间件只作用于请求前,可以省略调用Next()
,算是一种兼容性比较好的写法吧。
你好,大佬写的文章很受用。有个问题想问一下,在使用循环调用handler的话,如何实现类似于gin中的c.Abort()
这种中间件的退出机制呢?
你好,大佬写的文章很受用。有个问题想问一下,在使用循环调用handler的话,如何实现类似于gin中的
c.Abort()
这种中间件的退出机制呢?
@YoshieraHuang context 中维护一个状态值,调用 c.Abort()
改变状态,循环时检查状态,发现已经中止停止循环即可。
你好,大佬写的文章很受用。有个问题想问一下,在使用循环调用handler的话,如何实现类似于gin中的
c.Abort()
这种中间件的退出机制呢?@YoshieraHuang context 中维护一个状态值,调用
c.Abort()
改变状态,循环时检查状态,发现已经中止停止循环即可。
理解了,谢谢大佬。
func onlyForV2() gee.HandlerFunc {
return func(c *gee.Context) {
// Start timer
t := time.Now()
// if a server error occurred
c.Fail(500, "Internal Server Error")
// Calculate resolution time
log.Printf("[%d] %s in %v for group v2", c.StatusCode, c.Req.RequestURI, time.Since(t))
}
}
这里进来不都进行 c.Fail(500, "Internal Server Error") 这一步报错了么?没太理解,麻烦大佬有空解释下😂 @geektutu
@MoneyHappy
func onlyForV2() gee.HandlerFunc { return func(c *gee.Context) { // Start timer t := time.Now() // if a server error occurred c.Fail(500, "Internal Server Error") // Calculate resolution time log.Printf("[%d] %s in %v for group v2", c.StatusCode, c.Req.RequestURI, time.Since(t)) } }
这里进来不都进行 c.Fail(500, "Internal Server Error") 这一步报错了么?没太理解,麻烦大佬有空解释下😂 @geektutu
这个是中间件功能的示例,这里应该是用发送500错误码来表示中间件的起作用了
目前的中间件设计无法支持指定路由使用中间件吧
这是短路中间件,如果使用 后续的中间件和handler就直接跳过了 c.index = len(c.handlers)
v2.Use(onlyForV2()) // v2 group middleware { v2.GET("/hello/:name", func(c *gee.Context) { // expect /hello/geektutu c.String(http.StatusOK, "hello %s, you're at %s\n", c.Param("name"), c.Path) }) } 这个{}的用法是什么含义呀
学习日志: 经过debug调试,运行后在c.handlers里面会插入需要执行的方法,以/index为例 在c.handers里面就有有以下的内容 1、gee.Logger() 2、main.func 在运行gee.Logger()的时候会显式的调用Next,将会跳转去执行第二步main.func,这时c.index的状态变化如下 1、c.index = -1 // 初始化 2、c.index = 0 // logger 3、c.index = 1 // main 最后将返回logger里面记录的耗时日志
@jqb44179 v2.Use(onlyForV2()) // v2 group middleware { v2.GET("/hello/:name", func(c *gee.Context) { // expect /hello/geektutu c.String(http.StatusOK, "hello %s, you're at %s\n", c.Param("name"), c.Path) }) } 这个{}的用法是什么含义呀
这里大括号的作用是开创一个新的语法块,或者说作用域,也就是里面定义的同名变量会覆盖外层。在这里的作用我个人觉得更多的是可读性吧,能够清楚知道每个分组的路由设置情况
@geektutu @void1104 这里和 http ServerHTTP 函数原始的两个参数对应 req 和 writer。req 是结构体,用指针可以节省内存,Writer 是一个接口类型,不能用指针。
package http type Handler interface { ServeHTTP(w ResponseWriter, r *Request) }
我思考和尝试了一下,接口类型确实不能用指针,不然实现这个接口的类型变量就传不进来(或者说类型转换?). 但是在ide上定义一个指针接口倒没有报错,说明确实有指针接口这种东西。想请教下什么时候会用到指针接口呢,网上涉及这个也有点少。
v1 := engine.Group("/v1")
v1.Use(gee.Logger())
v1.Use(gee.Logger())
{
v1.Get("/hello", hello)
v1.Get("/:lang/say_hello", SayHello)
v1.Get("/:lang/say_bye/*", SayBye)
}
v1Say := v1.Group("/say")
v1Say.Use(gee.Logger())
{
v1Say.Get("/hello", hello)
v1Say.Get("/:lang/say_hello", SayHello)
v1Say.Get("/:lang/say_bye/*", SayBye)
}
博主,如果子group里面使用中间件或者重复使用中间件,这样会导致重复的问题,这个问题是合理还是不合理,因为中间件有什么好办法去重么?用map或者采用指针判断吗?
@oidbio
v1 := engine.Group("/v1") v1.Use(gee.Logger()) v1.Use(gee.Logger()) { v1.Get("/hello", hello) v1.Get("/:lang/say_hello", SayHello) v1.Get("/:lang/say_bye/*", SayBye) } v1Say := v1.Group("/say") v1Say.Use(gee.Logger()) { v1Say.Get("/hello", hello) v1Say.Get("/:lang/say_hello", SayHello) v1Say.Get("/:lang/say_bye/*", SayBye) }
博主,如果子group里面使用中间件或者重复使用中间件,这样会导致重复的问题,这个问题是合理还是不合理,因为中间件有什么好办法去重么?用map或者采用指针判断吗?
这应该没必要管吧. 使用中间键的权利暴露给用户. 用户要犯傻也没办法呀啊
onlyForV2中出现的Context的Fail方法,在前面的教程中好像漏掉了
@MoneyHappy
func onlyForV2() gee.HandlerFunc { return func(c *gee.Context) { // Start timer t := time.Now() // if a server error occurred c.Fail(500, "Internal Server Error") // Calculate resolution time log.Printf("[%d] %s in %v for group v2", c.StatusCode, c.Req.RequestURI, time.Since(t)) } }
这里进来不都进行 c.Fail(500, "Internal Server Error") 这一步报错了么?没太理解,麻烦大佬有空解释下😂 @geektutu
应该是写多了改为 c.String(500, "Internal Server Error") 即可
@hu-xiaokang
func (c *Context) Next() { c.index++ s := len(c.handlers) for ; c.index < s; c.index++ { fmt.Printf("index:%d\n", c.index) c.handlers[c.index](c) } }
您好,想请教下为什么这个Next函数需要遍历c.handlers?因为我看hadlers函数会带c.Next(),如下Logger:
func Logger() HandlerFunc { return func(c *Context) { // Start timer t := time.Now() // Process request c.Next() // Calculate resolution time log.Printf("[%d] %s in %v", c.StatusCode, c.Req.RequestURI, time.Since(t)) } }
所以,为什么不直接这样写?
func (c *Context) Next() { c.index++ c.handlers[c.index](c) }
感谢!!!
在中间件A中调用c.Next()的作用:在执行完当前A中间件之前,先执行它后面的那些中间件,后面的中间件执行完,中间件A继续执行c.Next()后面的语句。
请问一下直接去执行handlers就可以了,为什么还要多维护一个index,
func onlyForV1() gee.HandlerFunc { return func(c gee.Context) { // Start timer fmt.Println(1) } } func onlyForV2() gee.HandlerFunc { return func(c gee.Context) { // Start timer fmt.Println(2) } }
func main() { r := gee.New()
v2 := r.Group("/v1/v2")
v2.Use(onlyForV2()) // v2 group middleware
{
v2.GET("/hello/:name", func(c *gee.Context) {
// expect /hello/geektutu
c.String(http.StatusOK, "hello %s, you're at %s\n", c.Param("name"), c.Path)
})
}
v1 := r.Group("/v1")
v1.Use(onlyForV1()) // v2 group middleware
r.Run(":9999")
} 这样是先打印出了2,然后1.理论上v1/v2 应该先执行1然后2,或许应该对middleware排个序?
@geektutu
你好,大佬写的文章很受用。有个问题想问一下,在使用循环调用handler的话,如何实现类似于gin中的
c.Abort()
这种中间件的退出机制呢?@YoshieraHuang context 中维护一个状态值,调用
c.Abort()
改变状态,循环时检查状态,发现已经中止停止循环即可。
不维护状态值,直接c.index = len(c.handlers)也可以吧
兔兔大佬真棒!这个从c.Next()方法设计真的很独到,看了很久才明白,next方法第一行index++,和for循环的index++同时作用保证了这个嵌套循环内的每个handler方法只执行一次,执行到最内层index已经是最大值了,外层的嵌套循环就直接结束了
请问一下直接去执行handlers就可以了,为什么还要多维护一个index,
@dter 因为在中间件中还可以添加c.Next()调用.如果没有index,单纯的循环,会导致中间件重复
func onlyForV1() gee.HandlerFunc { return func(c gee.Context) { // Start timer fmt.Println(1) } } func onlyForV2() gee.HandlerFunc { return func(c gee.Context) { // Start timer fmt.Println(2) } }
func main() { r := gee.New()
v2 := r.Group("/v1/v2") v2.Use(onlyForV2()) // v2 group middleware { v2.GET("/hello/:name", func(c *gee.Context) { // expect /hello/geektutu c.String(http.StatusOK, "hello %s, you're at %s\n", c.Param("name"), c.Path) }) } v1 := r.Group("/v1") v1.Use(onlyForV1()) // v2 group middleware r.Run(":9999")
} 这样是先打印出了2,然后1.理论上v1/v2 应该先执行1然后2,或许应该对middleware排个序?
@YoshieraHuang yang 讲道理这种使用group的方法,也属于测试开发者了 haha
@junbin-yang
@geektutu
你好,大佬写的文章很受用。有个问题想问一下,在使用循环调用handler的话,如何实现类似于gin中的
c.Abort()
这种中间件的退出机制呢?@YoshieraHuang context 中维护一个状态值,调用
c.Abort()
改变状态,循环时检查状态,发现已经中止停止循环即可。不维护状态值,直接c.index = len(c.handlers)也可以吧
如果这样的话后续所有的HandlerFunc都不会执行了,如果需要执行路由注册进来的的func就不应该将其加入到handler中,应该单独调用,这样的话就要协调其和中间件的调用顺序了?
问下大佬,有个关于技术选型的问题: 1.我在其他框架源码看到中间件的实现是通过 往headers队列头或尾添加执行方法,然后顺序执行,达到中间件的目的 func A(){part1} func B(){part2} 即:【funcA, header, funcB】 执行动作:【part1 header part2】 2.gin则是往headers头顺序添加执行方法,业务方法保证在最后队列,引入next达到中间件的目的 func A(){part1 c.next() part2} func B(){part3 c.next() part4} 即:【funcA,funcB,funC,funcD】 执行动作:【part1 part3 header part4 part2】 3.我想请教一下gin引入了next增加的复杂度,得到的好处超过第一种吗
@liujing-siyang
@junbin-yang
@geektutu
你好,大佬写的文章很受用。有个问题想问一下,在使用循环调用handler的话,如何实现类似于gin中的
c.Abort()
这种中间件的退出机制呢?@YoshieraHuang context 中维护一个状态值,调用
c.Abort()
改变状态,循环时检查状态,发现已经中止停止循环即可。不维护状态值,直接c.index = len(c.handlers)也可以吧
如果这样的话后续所有的HandlerFunc都不会执行了,如果需要执行路由注册进来的的func就不应该将其加入到handler中,应该单独调用,这样的话就要协调其和中间件的调用顺序了?
是的,所以要看情况是否需要继续往后执行其他方法了。按照博主的教程我也撸了个项目github.com/junbin-yang/see可以参考参考,也是把中间件和最后的路由注册方法分开处理。
@dteer
问下大佬,有个关于技术选型的问题: 1.我在其他框架源码看到中间件的实现是通过 往headers队列头或尾添加执行方法,然后顺序执行,达到中间件的目的 即:【funcA,funcB,header,funcC,funcD】 2.gin则是往headers头顺序添加执行方法,业务方法保证在最后队列,引入next达到中间件的目的 3.我想请教一下gin引入了next增加的复杂度,得到的好处超过第一种吗
一般在header处理之后还需要中间件的情况很少吧。不然啥叫中间件。把header和中间件分开处理。可以将header带入路由注册。匹配路由时不需要二次检索。效率更高。【funcA,funcB,funcC,funcD】【header】。 如果说header之后还需要有其他处理的话应该是返回定义好的数据类型,需要一个方法统一格式化输出,如下:情况
type WrapperFuncType func(c gin.Context) ResponseCms func Wrapper(handle WrapperFuncType) gin.HandlerFunc { return func(c gin.Context) { var resp ResponseCms = handle(c) c.PureJSON(http.StatusOK, resp) } } router.POST("/index", Wrapper(Index))
@junbin-yang
@dteer
问下大佬,有个关于技术选型的问题: 1.我在其他框架源码看到中间件的实现是通过 往headers队列头或尾添加执行方法,然后顺序执行,达到中间件的目的 即:【funcA,funcB,header,funcC,funcD】 2.gin则是往headers头顺序添加执行方法,业务方法保证在最后队列,引入next达到中间件的目的 3.我想请教一下gin引入了next增加的复杂度,得到的好处超过第一种吗
一般在header处理之后还需要中间件的情况很少吧。不然啥叫中间件。把header和中间件分开处理。可以将header带入路由注册。匹配路由时不需要二次检索。效率更高。【funcA,funcB,funcC,funcD】【header】。 如果说header之后还需要有其他处理的话应该是返回定义好的数据类型,需要一个方法统一格式化输出,如下:情况
type WrapperFuncType func(c gin.Context) ResponseCms func Wrapper(handle WrapperFuncType) gin.HandlerFunc { return func(c gin.Context) { var resp ResponseCms = handle(c) c.PureJSON(http.StatusOK, resp) } } router.POST("/index", Wrapper(Index))
gin中next的操作不就是实现在header处理前和处理后做对应操作吗
@dteer
@junbin-yang
@dteer
问下大佬,有个关于技术选型的问题: 1.我在其他框架源码看到中间件的实现是通过 往headers队列头或尾添加执行方法,然后顺序执行,达到中间件的目的 即:【funcA,funcB,header,funcC,funcD】 2.gin则是往headers头顺序添加执行方法,业务方法保证在最后队列,引入next达到中间件的目的 3.我想请教一下gin引入了next增加的复杂度,得到的好处超过第一种吗
一般在header处理之后还需要中间件的情况很少吧。不然啥叫中间件。把header和中间件分开处理。可以将header带入路由注册。匹配路由时不需要二次检索。效率更高。【funcA,funcB,funcC,funcD】【header】。 如果说header之后还需要有其他处理的话应该是返回定义好的数据类型,需要一个方法统一格式化输出,如下:情况
type WrapperFuncType func(c gin.Context) ResponseCms func Wrapper(handle WrapperFuncType) gin.HandlerFunc { return func(c gin.Context) { var resp ResponseCms = handle(c) c.PureJSON(http.StatusOK, resp) } } router.POST("/index", Wrapper(Index))
gin中next的操作不就是实现在header处理前和处理后做对应操作吗
“Gee 的中间件的定义与路由映射的 Handler 一致,处理的输入是Context对象。插入点是框架接收到请求初始化Context对象后,允许用户使用自己定义的中间件做一些额外的处理,例如记录日志等,以及对Context进行二次加工。另外通过调用(*Context).Next()函数,中间件可等待用户自己定义的 Handler处理结束后,再做一些额外的操作”,这是作者在中间件设计部分核心的一段话。
自己的一些想法:1.中间件与路由映射的Handler一致,统一由context进行保存管理。 2.采用第二种方式的话,假设现在有3个中间件,即对应3个func(),你可以在其内部自定义next()的位置,控制在何时执行下一个中间件,甚至直接跳过前或后半部分。 3.采用第一种方式,同样3个中间件,可能要写6个func(),而且不利于集中管理和控制。
其实我有些纠结当用户这样使用框架的时候,他/她的期待是什么样的:
g1 := engine.New("/g1").Use(MW1)
g2 := g1.New("/g2").Use(MW2, MW3)
g1.Use(MW4)
我觉得期待的样子应该是g1
这个group有MW1
,MW4
这两个中间件、g2
这个group有MW1
,MW2
,MW3
这三个中间件。
不知道大佬觉得这种use case是否合理? 如果合理的话,上面用append来添加中间件的方式是不是还有提升空间?
func (c *Context) Next() {
c.index++
s := len(c.handlers)
//不是所有的handler都会调用 Next()。
//手工调用 Next(),一般用于在请求前后各实现一些行为。如果中间件只作用于请求前,可以省略调用Next(),此种写法可以兼容 不调用Next的写法
//并且使用c.index<s;c.index++ 也能保证中间件只执行一次
//当中间件不调用 next函数时,通过此循环保证中间件执行顺序
//当中间件调用next 函数时,且当中间件执行完毕,对应c.index也已经到达指定index
//前面存在的for循环因为是通过c.index<s 做循环判断,则不会重复执行已经执行过的中间件
for ; c.index < s; c.index++ {
c.handlers[c.index](c)
}
}
这个next函数设计得真巧妙,太妙了,
顺便建议这个 next前逻辑和next后逻辑可以类比下java的filter设计,让java同学一眼就懂
这个请求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()
}
想问一下这种前后逻辑都在一个函数里的中间件,和spring中的前后逻辑都在不同的函数中的 interceptor有啥区别
var middlewares []HandlerFunc
for _, group := range engine.groups {
if strings.HasPrefix(req.URL.Path, group.prefix) {
middlewares = append(middlewares, group.middlewares...)
}
}
在ServeHTTP()中,每接收到一个请求都需要逐一匹配前缀来添加中间件,太影响性能了。 gin是在前缀树的节点中添加中间件的切片,这样在匹配动态路由并解析参数时,就可以同时获得各分组的中间件。
@FOOLISH06
var middlewares []HandlerFunc for _, group := range engine.groups { if strings.HasPrefix(req.URL.Path, group.prefix) { middlewares = append(middlewares, group.middlewares...) } }
在ServeHTTP()中,每接收到一个请求都需要逐一匹配前缀来添加中间件,太影响性能了。 gin是在前缀树的节点中添加中间件的切片,这样在匹配动态路由并解析参数时,就可以同时获得各分组的中间件。
这样的话,需要在每一个router的node上加入该node属于哪一个组
第二个结果是 [500] /v2/hello/geektutu in 61.467µs for group v2
吧,因为在c.Fail之后c.StatusCode = 500。另外下边那个打印的应该也是500
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)。