blog icon indicating copy to clipboard operation
blog copied to clipboard

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

Open geektutu opened this issue 5 years ago • 67 comments

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

geektutu avatar Sep 01 '19 13:09 geektutu

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

感谢!!!

hu-xiaokang avatar Oct 29 '19 03:10 hu-xiaokang

@acaibird 不是所有的handler都会调用 Next()。 手工调用 Next(),一般用于在请求前后各实现一些行为。如果中间件只作用于请求前,可以省略调用Next(),算是一种兼容性比较好的写法吧。

geektutu avatar Oct 29 '19 03:10 geektutu

@geektutu @acaibird 不是所有的handler都会调用 Next()。 手工调用 Next(),一般用于在请求前后各实现一些行为。如果中间件只作用于请求前,可以省略调用Next(),算是一种兼容性比较好的写法吧。

嗯嗯,明白了,多谢!

hu-xiaokang avatar Oct 29 '19 05:10 hu-xiaokang

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 avatar Mar 18 '20 10:03 blackher

@blackher Next() 函数一开始调用了 c.index++,下标恰好从 0 开始。

geektutu avatar Mar 18 '20 13:03 geektutu

@geektutu 我的意思是 index 直接初始化为0 Next()就不用c.index++了 我自己尝试初始化为0 去掉c.index++ 一直死循环

blackher avatar Mar 19 '20 09:03 blackher

你把Next() 第一行的 c.index++ 去掉,就会一直卡在第一个中间件上了,必然死循环。每调用一次 Next()c.index 得 +1,不然 c.index 就会一直是 0。

死循环之后,是不会走到 for 语句后面的 c.index++ 的。

geektutu avatar Mar 19 '20 10:03 geektutu

@geektutu 提问一下,这样写不是会凡是用到 onlyForV2的 都会报500的错吗?,应该不是遇到错误报500吧?

ReviveKwan avatar Mar 30 '20 07:03 ReviveKwan

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

ronething-bot avatar Mar 30 '20 08:03 ronething-bot

func A(c *Context) {
    // 中间件上半部
    part1
    c.Next()
    // 中间件下半部
    part2
}

感觉这样解释比较容易理解

SourceLink avatar Apr 20 '20 08:04 SourceLink

首先谢谢博主,文章写得很好,思路很清晰。 然后我有个问题,你这个middleware的实现执行是无序的。所以是不是把middleware放在trie的node里面,然后在search的时候collect所有的middlewares,这样更好?

proverbs avatar Jul 06 '20 22:07 proverbs

@proverbs 首先谢谢博主,文章写得很好,思路很清晰。 然后我有个问题,你这个middleware的实现执行是无序的。所以是不是把middleware放在trie的node里面,然后在search的时候collect所有的middlewares,这样更好?

代码看来,因为routerGroups中所有的RouterGroup,在添加时肯定是按照先父再子的顺序添加的,所以从头到尾遍历routerGroups这种简单的收集方式,也就是达成了实际路径中从左向右的查找过程。 所以顺序是可以保证的。

当然这种收集方式有个前提:分组中所有的前缀必须是静态路径,否则使用实际路由从全部分组做前缀匹配,无法保证收集的完整性。这也给上节的分组控制的使用加了限制。

如果我理解错了,还请指出。

seascheng avatar Aug 13 '20 07:08 seascheng

博主,能否请教一个基础问题,为什么Context中,Writer不用指针,而Req却要用指针呢?

void1104 avatar Dec 01 '20 03:12 void1104

@void1104 这里和 http ServerHTTP 函数原始的两个参数对应 req 和 writer。req 是结构体,用指针可以节省内存,Writer 是一个接口类型,不能用指针。

package http

type Handler interface {
    ServeHTTP(w ResponseWriter, r *Request)
}

geektutu avatar Dec 01 '20 05:12 geektutu

理解了,谢谢大佬

------------------ 原始邮件 ------------------ 发件人: "geektutu/geektutu-blog" <[email protected]>; 发送时间: 2020年12月1日(星期二) 中午1:23 收件人: "geektutu/geektutu-blog"<[email protected]>; 抄送: "彭家鑫"<[email protected]>;"Mention"<[email protected]>; 主题: 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.

void1104 avatar Dec 01 '20 06:12 void1104

@geektutu @acaibird 不是所有的handler都会调用 Next()。 手工调用 Next(),一般用于在请求前后各实现一些行为。如果中间件只作用于请求前,可以省略调用Next(),算是一种兼容性比较好的写法吧。

你好,大佬写的文章很受用。有个问题想问一下,在使用循环调用handler的话,如何实现类似于gin中的c.Abort()这种中间件的退出机制呢?

YoshieraHuang avatar Dec 16 '20 11:12 YoshieraHuang

你好,大佬写的文章很受用。有个问题想问一下,在使用循环调用handler的话,如何实现类似于gin中的c.Abort()这种中间件的退出机制呢?

@YoshieraHuang context 中维护一个状态值,调用 c.Abort() 改变状态,循环时检查状态,发现已经中止停止循环即可。

geektutu avatar Dec 16 '20 15:12 geektutu

你好,大佬写的文章很受用。有个问题想问一下,在使用循环调用handler的话,如何实现类似于gin中的c.Abort()这种中间件的退出机制呢?

@YoshieraHuang context 中维护一个状态值,调用 c.Abort() 改变状态,循环时检查状态,发现已经中止停止循环即可。

理解了,谢谢大佬。

YoshieraHuang avatar Dec 17 '20 03:12 YoshieraHuang

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 avatar Mar 07 '21 10:03 MoneyHappy

@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错误码来表示中间件的起作用了

hyicode avatar Mar 26 '21 15:03 hyicode

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

Nick233333 avatar Apr 03 '21 16:04 Nick233333

这是短路中间件,如果使用 后续的中间件和handler就直接跳过了 c.index = len(c.handlers)

walkmiao avatar Apr 12 '21 13:04 walkmiao

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) }) } 这个{}的用法是什么含义呀

jqb44179 avatar Apr 22 '21 06:04 jqb44179

学习日志: 经过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 avatar Apr 22 '21 08:04 jqb44179

@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) }) } 这个{}的用法是什么含义呀

这里大括号的作用是开创一个新的语法块,或者说作用域,也就是里面定义的同名变量会覆盖外层。在这里的作用我个人觉得更多的是可读性吧,能够清楚知道每个分组的路由设置情况

whitebluepants avatar May 03 '21 15:05 whitebluepants

@geektutu @void1104 这里和 http ServerHTTP 函数原始的两个参数对应 req 和 writer。req 是结构体,用指针可以节省内存,Writer 是一个接口类型,不能用指针。

package http

type Handler interface {
    ServeHTTP(w ResponseWriter, r *Request)
}

我思考和尝试了一下,接口类型确实不能用指针,不然实现这个接口的类型变量就传不进来(或者说类型转换?). 但是在ide上定义一个指针接口倒没有报错,说明确实有指针接口这种东西。想请教下什么时候会用到指针接口呢,网上涉及这个也有点少。

whitebluepants avatar May 03 '21 15:05 whitebluepants

	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或者采用指针判断吗?

oi8io avatar May 07 '21 12:05 oi8io

@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或者采用指针判断吗?

这应该没必要管吧. 使用中间键的权利暴露给用户. 用户要犯傻也没办法呀啊

puck1006 avatar Jun 10 '21 08:06 puck1006

onlyForV2中出现的Context的Fail方法,在前面的教程中好像漏掉了

cuglaiyp avatar Jun 15 '21 11:06 cuglaiyp

@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") 即可

pzx521521 avatar Jul 21 '21 10:07 pzx521521