使用 Go 处理中间件

帅气猫咪 · 2019-10-18 21:04:19 · 1944 次点击 · 预计阅读时间 5 分钟 · 大约8小时之前 开始浏览    
这是一个创建于 2019-10-18 21:04:19 的文章,其中的信息可能已经有所发展或是发生改变。

简介

开发 web 应用的时候, 很多地方都需要使用中间件来统一处理一些任务, 比如记录日志, 登录校验等.

gin 也提供了中间件功能.

gin 的中间件

在项目创建之初, 就已经导入了一些中间件, 当时没有仔细介绍.

g.Use(gin.Logger())
g.Use(gin.Recovery())
g.Use(middleware.NoCache())
g.Use(middleware.Options())
g.Use(middleware.Secure())
复制代码

前面两个是 gin 自带的中间件, 分别是日志记录和错误恢复. 后面三个是设置一些 header, 具体是阻止缓存响应, 响应 options 请求, 以及浏览器安全设置.

// 阻止缓存响应
func NoCache() gin.HandlerFunc {
	return func(ctx *gin.Context) {
		ctx.Header("Cache-Control", "no-cache, no-store, max-age=0, must-revalidate, value")
		ctx.Header("Expires", "Thu, 01 Jan 1970 00:00:00 GMT")
		ctx.Header("Last-Modified", time.Now().UTC().Format(http.TimeFormat))
		ctx.Next()
	}
}

// 响应 options 请求, 并退出
func Options() gin.HandlerFunc {
	return func(ctx *gin.Context) {
		if ctx.Request.Method != "OPTIONS" {
			ctx.Next()
		} else {
			ctx.Header("Access-Control-Allow-Origin", "*")
			ctx.Header("Access-Control-Allow-Methods", "GET,POST,PUT,PATCH,DELETE,OPTIONS")
			ctx.Header("Access-Control-Allow-Headers", "authorization, origin, content-type, accept")
			ctx.Header("Allow", "HEAD,GET,POST,PUT,PATCH,DELETE,OPTIONS")
			ctx.Header("Content-Type", "application/json")
			ctx.AbortWithStatus(200)
		}
	}
}

// 安全设置
func Secure() gin.HandlerFunc {
	return func(ctx *gin.Context) {
		ctx.Header("Access-Control-Allow-Origin", "*")
		ctx.Header("X-Frame-Options", "DENY")
		ctx.Header("X-Content-Type-Options", "nosniff")
		ctx.Header("X-XSS-Protection", "1; mode=block")
		if ctx.Request.TLS != nil {
			ctx.Header("Strict-Transport-Security", "max-age=31536000")
		}

		// Also consider adding Content-Security-Policy headers
		// ctx.Header("Content-Security-Policy", "script-src 'self' https://cdnjs.cloudflare.com")
	}
}
复制代码

gin 的中间件结构就是一个返回 func(ctx *gin.Context) 的函数, 又叫做 gin.HandlerFunc. 本质上和普通的 handler 没什么不同, gin.HandlerFuncfunc(*Context) 的别名.

中间件可以被定义在三个地方

  • 全局中间件
  • Group 中间件
  • 单个路由中间件

一点需要注意的是在 middleware 和 handler 中使用 goroutine 时, 应该使用 gin.Context 的只读副本, 例如 cCp := context.Copy().

另一点则是注意中间件的顺序.

官方的示例如下:

func Logger() gin.HandlerFunc {
	return func(c *gin.Context) {
		t := time.Now()

		// Set example variable
		c.Set("example", "12345")

		// before request

		c.Next()

		// after request
		latency := time.Since(t)
		log.Print(latency)

		// access the status we are sending
		status := c.Writer.Status()
		log.Println(status)
	}
}
复制代码

创建中间件

介绍了 gin 的中间件知识之后, 就可以根据需求使用中间件了.

实现一个中间件在每个请求中设置 X-Request-Id 头.

// 在请求头中设置 X-Request-Id
func RequestId() gin.HandlerFunc {
	return func(ctx *gin.Context) {
		requestId := ctx.Request.Header.Get("X-Request-Id")

		if requestId == "" {
			requestId = uuid.NewV4().String()
		}

		ctx.Set("X-Request-Id", requestId)

		ctx.Header("X-Request-Id", requestId)
		ctx.Next()
	}
}
复制代码

设置 header 的同时保存在 context 内部, 通过设置唯一的 ID 之后, 就可以追踪一系列的请求了.

再来实现一个日志记录的中间件, 虽然 gin 已经自带了日志记录的中间件, 但自己实现可以更加个性化.

// 定义日志组件, 记录每一个请求
func Logging() gin.HandlerFunc {
	return func(ctx *gin.Context) {
		path := ctx.Request.URL.Path
		method := ctx.Request.Method
		ip := ctx.ClientIP()

		// 只记录特定的路由
		reg := regexp.MustCompile("(/v1/user|/login)")
		if !reg.MatchString(path) {
			return
		}

		var bodyBytes []byte
		if ctx.Request.Body != nil {
			bodyBytes, _ = ioutil.ReadAll(ctx.Request.Body)
		}
		// 读取后写回
		ctx.Request.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes))

		blw := &bodyLogWriter{
			body:           bytes.NewBufferString(""),
			ResponseWriter: ctx.Writer,
		}
		ctx.Writer = blw

		start := time.Now()
		ctx.Next()
		// 计算延迟, 和 gin.Logger 的差距有点大
		// 这是因为 middleware 类似栈, 先进后出, ctx.Next() 是转折点
		// 所以 gin.Logger 放在最前, 记录总时长
		// Logging 放在最后, 记录实际运行的时间, 不包含其他中间件的耗时
		end := time.Now()
		latency := end.Sub(start)

		code, message := -1, ""
		var response handler.Response
		if err := json.Unmarshal(blw.body.Bytes(), &response); err != nil {
			logrus.Errorf(
				"response body 不能被解析为 model.Response struct, body: `%s`, err: `%v`",
				blw.body.Bytes(),
				err,
			)
			code = errno.InternalServerError.Code
			message = err.Error()
		} else {
			code = response.Code
			message = response.Message
		}

		logrus.WithFields(logrus.Fields{
			"latency": fmt.Sprintf("%s", latency),
			"ip":      ip,
			"method":  method,
			"path":    path,
			"code":    code,
			"message": message,
		}).Info("记录请求")
	}
}
复制代码

在注册中间件的时候, 将 Logging 放在全局中间件的最后, 将 gin.Logger() 放在全局中间件的最开始. 通过对比延迟, 你可以发现, 在 handler 处理比较快时, 中间件在总请求耗时中占据了很大的比例.

所以, 中间件虽然非常实用, 但需要控制全局中间件的数量.

总结

中间件是非常实用的, 基本上 web 框架都会实现.

当前部分的代码

作为版本 v0.8.0


有疑问加站长微信联系(非本文作者)

本文来自:掘金

感谢作者:帅气猫咪

查看原文:使用 Go 处理中间件

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

1944 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传