golang程序设计:Go middleware中间件以及Gin 中间件分析
先从业务开发角度出发,来逐渐引出中间件。
一、刚开始时业务开发
开始业务开发时,业务需求比较少。
- 当我们最开始进行业务开发时,需求不是很多。 第一个需求产是品向大家打声招呼:“hello world”。
接到需求任务,我们就进行代码开发了。
一般都会写下如下的代码,用handlefunc来处理请求的服务
package main
import (
"net/http"
)
func helloHandler(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("hello world"))
}
func main() {
http.HandleFunc("/", helloHandler)
http.ListenAndServe(":8080", nil)
}
- 假如现在业务有变化了,我们要新增一个hello服务的处理耗时,怎么做?
这个需求比较简单,修改代码如下:
package main
import (
"log"
"net/http"
"os"
"time"
)
var logger = log.New(os.Stdout, "", 0)
func helloHandler(w http.ResponseWriter, r *http.Request) {
timeStart := time.Now()
w.Write([]byte("hello world"))
timeElapsed := time.Since(timeStart)
logger.Println(timeElapsed)
}
func main() {
http.HandleFunc("/", helloHandler)
http.ListenAndServe(":8080", nil)
}
这样就可以输出当前hello请求到日志文件了。
- 完成了这个需求后。过了没多久,又有新的需求来了,我们要显示信息,显示Email,
显示好朋友,并且这就一个接口也需要增加耗时记录。
一下子又增加了很多api, 简略示例代码如下:
package main
func helloHandler(wr http.ResponseWriter, r *http.Request) {
// ...
}
func showInfoHandler(wr http.ResponseWriter, r *http.Request) {
// ...
}
func showEmailHandler(wr http.ResponseWriter, r *http.Request) {
// ...
}
func showFriendsHandler(wr http.ResponseWriter, r *http.Request) {
timeStart := time.Now()
wr.Write([]byte("your friends is tom and alex"))
timeElapsed := time.Since(timeStart)
logger.Println(timeElapsed)
}
func main() {
http.HandleFunc("/", helloHandler)
http.HandleFunc("/info/show", showInfoHandler)
http.HandleFunc("/email/show", showEmailHandler)
http.HandleFunc("/friends/show", showFriendsHandler)
// ...
}
每一个handler里面都需要记录运行的时间,每次新加路由都要写同样的代码。都要把业务逻辑代码拷贝过来。
- 业务继续发展,又有了新的需求,增加一个监控系统,需要你上报接口运行时间到监控系统里面,以便监控接口的稳定性。这个监控系统叫metrics。
好了,现在你又要修改代码,通过http post方式把耗时时间发送给metrics系统。
而且你要修改好多个handler,增加metrics上报接口代码。
修改代码:
func helloHandler(wr http.ResponseWriter, r *http.Request) {
timeStart := time.Now()
wr.Write([]byte("hello"))
timeElapsed := time.Since(timeStart)
logger.Println(timeElapsed)
// 新增耗时上报
metrics.Upload("timeHandler", timeElapsed)
}
func showInfoHandler(wr http.ResponseWriter, r *http.Request) {
// ...
// 新增耗时上报
metrics.Upload("timeHandler", timeElapsed)
}
func showEmailHandler(wr http.ResponseWriter, r *http.Request) {
// ...
// 新增耗时上报
metrics.Upload("timeHandler", timeElapsed)
}
func showFriendsHandler(wr http.ResponseWriter, r *http.Request) {
timeStart := time.Now()
wr.Write([]byte("your friends is tom and alex"))
timeElapsed := time.Since(timeStart)
logger.Println(timeElapsed)
// 新增耗时上报
metrics.Upload("timeHandler", timeElapsed)
}
到这里,发现要修改好多的handler函数,才能把接口的耗时时间上报到metrics系统里。
随着新需求越来越多,handler也会越多,那么我们修改的地方也就越多。增加了一个简单的业务统计,就要修改好多个handler函数。
虽然只是增加一个业务统计,我们就要去修改handler,来增加这些和业务无关的代码。
一开始我们并没有做错什么, 但是随着业务的发展,我们逐渐陷入了代码的泥潭。
接下来,我们该怎么办呢?怎么处理这种情况?
二、业务逐渐多了后
随着业务发展,handler越来越多,增加与业务无关的代码所要修改的地方也越来越多。这时候怎么办?有没有办法可以处理这种情况呢?
想一下,java里面有一个Filter的技术,可以拦截请求的处理。我们可不可以利用这个思想来解决我们的问题呢。这种思想当然是可以的。
在go里面就是利用 http.Handler
来把你要处理的函数包起来(实际就是拦截了),然后处理。
在go里面有一个学名叫 middleware(中间件),中间件常见的位置是ServeMux和应用处理程序之间。
http的请求控制流程:
ServeMux => Middleware Handler => Application Handler
针对上面的需求,我们用中间件的方法来修改下:
package main
import (
"log"
"net/http"
"os"
"time"
)
var logger = log.New(os.Stdout, "", 0)
func hello(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("hello world"))
}
func timeMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
timeStart := time.Now()
//next handler
next.ServeHTTP(w, r)
timeElapsed := time.Since(timeStart)
logger.Println(timeElapsed)
})
}
func main() {
http.HandleFunc("/", timeMiddleWare(hello))
http.ListenAndServe(":8080", nil)
}
这样就实现了中间件。
也是把业务代码和非业务代码进行了剥离。
三、怎么实现中间件
上面的中间件是怎么实现的呢?
- 他要满足
http.Handler
接口
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
写一个简单的程序:
func showinfoHandler(info string) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(info)
})
}
上面程序我们把简单的处理程序w.Write放在了一个匿名函数中,然后引用了外部的info构成了一个闭包。接下来我们用 http.HandlerFunc
适配器将此闭包转换为处理程序,然后返回它。
我们可以用相同的方法,将下一个处理程序作为变量来进行传递,然后调用ServeHTTP() 方法将控制转移到下一个处理程序,然后返回它。
func demoMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// out logic
next.ServeHTTP(w, r)
})
}
上面的中间件函数有一个 func(http.Handler) http.Handler
的函数签名。它接收一个处理程序作为参数并返回另外一个处理程序。
一个完整的例子
用一个完整的例子来看看中间件的执行流程:
middlewaredemo.go
package main
import (
"log"
"net/http"
)
func middleOne(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Println("before middleone")
next.ServeHTTP(w, r)
log.Println("after middlewareOne again")
})
}
func middleTwo(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Println("before middletwo")
if r.URL.Path == "/foo" {
return
}
next.ServeHTTP(w, r)
log.Println("after middleTwo again")
})
}
func final(w http.ResponseWriter, r *http.Request) {
log.Println("exec finalHandler")
w.Write([]byte("OK"))
}
func main() {
finalHandler := http.HandlerFunc(final)
http.Handle("/", middleOne(middleTwo(finalHandler)))
http.ListenAndServe(":3030", nil)
}
先执行下面的命令
go run middlewaredemo.go
然后在浏览器上执行:http://localhost:3030/
运行结果:
2020/04/19 23:07:14 before middleone
2020/04/19 23:07:14 before middletwo
2020/04/19 23:07:14 exec finalHandler
2020/04/19 23:07:14 after middleTwo again
2020/04/19 23:07:14 after middlewareOne again
看看执行结果,在 next.ServeHTTP
前,看到处理顺序是依次按照嵌套的顺序出结果, 但是在 next.ServeHTTP
后 的程序,是按照相反的方向出结果。
然后在运行 : http://localhost:3030/foo
运行结果:
2020/04/19 23:12:15 before middleone
2020/04/19 23:12:15 before middletwo
2020/04/19 23:12:15 after middlewareOne again
middleTwo
函数里 return
后面的程序,都没有显示了。
所以,在中间件中,我们可以用 return
来停止在中间件程序的传播。
四、Gin框架的中间件
github.com/gin-gonic/gin,这个web框架使用很广泛,它也有中间件功能。
- 使用方法 一
// 定义中间件
func middlewareOne(c *gin.Context) {
// 中间件逻辑
}
// 使用中间件
r := gin.Default()
r.Use(middlewareOne)
- 使用方法 二
func middlewareTwo() gin.HandlerFunc {
// 自定义逻辑
return func(c *gin.Context) {
// 中间件逻辑
}
}
// 使用中间件
r := gin.Deafult()
r.Use(middlewareTwo()
Gin还有一种像java中的Filter,处理前,处理后的一种方法 Next()
比如:demo1.go
package main
import (
"fmt"
"time"
"github.com/gin-gonic/gin"
)
func middlewareOne() gin.HandlerFunc {
return func(c *gin.Context) {
c.String(200, "before middlewareOne: handler "+time.Now().String()+"\n")
c.Next()
c.String(200, "after middlewareOne : handler "+time.Now().String()+"\n")
}
}
func middlewareTwo() gin.HandlerFunc {
return func(c *gin.Context) {
c.String(200, "before middlewareTwo: handler \n")
c.Next()
c.String(200, "after middlewareTwo : handler \n")
}
}
func main() {
r := gin.Default()
r.Use(middlewareOne(), middlewareTwo())
r.GET("/", func(c *gin.Context) {
fmt.Println("start!")
c.String(200, "GET method \n")
fmt.Println("end!")
})
r.Run(":8080")
}
先在命令行运行:
go run demo1.go
然后在浏览器上输入:http://localhost:8080/
就可以看到输出结果:
before middlewareOne: handler 2020-04-20 01:03:25.4547842 +0800 CST m=+21.318612501
before middlewareTwo: handler
GET method
after middlewareTwo : handler
after middlewareOne : handler 2020-04-20 01:03:25.4547842 +0800 CST m=+21.318612501
其实我们看到gin框架实现的中间件,它书写形式并不是像上面的那种嵌套模式。如果有很多中间件的话,那么这种嵌套模式写出来就让人感觉非常复杂。
而gin中间件这种书写模式,就很清晰,适合人阅读。是一种优雅的实现方式。
它是怎么实现的呢?
五、Gin中间件的实现
gin v1.6.2 版本
gin.go
// HandlerFunc defines the handler used by gin middleware as return value.
type HandlerFunc func(*Context)
// HandlersChain defines a HandlerFunc array.
//定义了Handlers链
type HandlersChain []HandlerFunc
// Last returns the last handler in the chain. ie. the last handler is the main one.
// 从Handlers里取出最后一个Handler,就是main自己
func (c HandlersChain) Last() HandlerFunc {
if length := len(c); length > 0 {
return c[length-1]
}
return nil
}
// RouteInfo represents a request route's specification which contains method and path and its handler.
type RouteInfo struct {
Method string
Path string
Handler string
HandlerFunc HandlerFunc
}
// RoutesInfo defines a RouteInfo array.
type RoutesInfo []RouteInfo
type Engine struct {
RouterGroup
// Enables automatic redirection if the current route can't be matched but a
// handler for the path with (without) the trailing slash exists.
// For example if /foo/ is requested but a route only exists for /foo, the
// client is redirected to /foo with http status code 301 for GET requests
// and 307 for all other request methods.
RedirectTrailingSlash bool
... ...
}
// Default returns an Engine instance with the Logger and Recovery middleware already attached.
// 初始化Engine,里面就用到了2个中间件函数Logger和Recovery
func Default() *Engine {
debugPrintWARNINGDefault()
engine := New()
engine.Use(Logger(), Recovery())
return engine
}
// Use attaches a global middleware to the router. ie. the middleware attached though Use() will be
// included in the handlers chain for every single request. Even 404, 405, static files...
// For example, this is the right place for a logger or error management middleware.
// 增加 middleware -> 实质是到 RouterGroup.Use()
func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
engine.RouterGroup.Use(middleware...) //到了 RouterGroup 里的Use
engine.rebuild404Handlers()
engine.rebuild405Handlers()
return engine
}
context.go
// Context is the most important part of gin. It allows us to pass variables between middleware,
// manage the flow, validate the JSON of a request and render a JSON response for example.
type Context struct {
writermem responseWriter
Request *http.Request
Writer ResponseWriter
Params Params
handlers HandlersChain //这里有一个handlers 链,一个slice
index int8
fullPath string
engine *Engine
// This mutex protect Keys map
KeysMutex *sync.RWMutex
// Keys is a key/value pair exclusively for the context of each request.
Keys map[string]interface{}
... ...
}
// Handler returns the main handler.
// 返回main handler
func (c *Context) Handler() HandlerFunc {
return c.handlers.Last()
}
// Next should be used only inside middleware.
// It executes the pending handlers in the chain inside the calling handler.
// See example in GitHub.
func (c *Context) Next() {
c.index++
for c.index < int8(len(c.handlers)) {
c.handlers[c.index](c)
c.index++
}
}
还有一个routegroup.go里的handler chain
// Use adds middleware to the group, see example code in GitHub.
// 增加middleware
func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
group.Handlers = append(group.Handlers, middleware...)
return group.returnObj()
}
func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {
finalSize := len(group.Handlers) + len(handlers)
if finalSize >= int(abortIndex) {
panic("too many handlers")
}
mergedHandlers := make(HandlersChain, finalSize)
copy(mergedHandlers, group.Handlers)
copy(mergedHandlers[len(group.Handlers):], handlers)
return mergedHandlers
}
六、改进以前框架
写到了这里,想起了以前写的 golang web框架 文章,那只是实现了一个简单的MVC功能,并不具备可扩展性,有了这个中间件技术,就可以把以前的框架进行改进。
改进后的全新 go web 框架 lilac
先实现功能,然后再进行优化改进 - 论开发。
七、参考
- Go高级语言编程-中间件 ,这个写的非常非常非常好,业务部分基本是从这里来的
- gin框架
golang程序设计:Go middleware中间件以及Gin 中间件分析的更多相关文章
- 【Gin-API系列】Gin中间件之日志模块(四)
日志是程序开发中必不可少的模块,同时也是日常运维定位故障的最重要环节之一.一般日志类的操作包括日志采集,日志查询,日志监控.日志统计等等.本文,我们将介绍日志模块在Gin中的使用. Golang如何打 ...
- 【get√】golang新手理解了一点点gin框架的中间件
1.通过use()来使用多个中间件 router := gin.New() router.Use( middleware1, middleware2) 2.中间件的典型实现 func Logger() ...
- 【Gin-API系列】Gin中间件之异常处理(六)
本文我们介绍生产环境上如何通过捕捉异常recovery来完善程序设计和提高用户体验. Golang异常处理 golang 的异常处理比较简单,通常都是在程序遇到异常崩溃panic之后通过defer调用 ...
- gin中间件推荐
中间件推荐 1.1.1. 列表 谷歌翻译欢迎查看原文 https://github.com/gin-gonic/contrib/blob/master/README.md RestGate - RES ...
- express中的中间件(middleware)、自定义中间件、静态文件中间件、路由中间件
express文档地址 什么是中间件呢(middleware)?它是谁的中间件呢? 首先我们需要了解到请求和响应, 请求就是客户端发送请求给服务器, 响应就是,服务器根据客户端的请求返回给客户端的数据 ...
- (28)django的中间件(自定义中间件和防范跨站请求伪造攻击)-重要的概念
Django中间件和中间件不是同一种东西 什么是中间件:中间件是一个很大的概念,只要程序和程序之间还有一层程序,用来处理两个程序的整个交互过程的请求.数据等等就叫中间件 Django中间件:是介于re ...
- django系列8.1--django的中间件01 自定义中间件的5个方法
一.Django中的中间件 Django中间件定义: Middleware is a framework of hooks into Django's request/response process ...
- Django-session中间件源码简单分析
Django-session中间件源码简单分析 settings里有关中间件的配置 MIDDLEWARE = [ 'django.middleware.security.SecurityMiddlew ...
- Django中间件部分源码分析
中间件源码分析 中间件简介 中间件是一个用来处理Django的请求和响应的框架级别的钩子.它是一个轻量.低级别的插件系统,用于在全局范围内改变Django的输入和输出.每个中间件组件都负责做一些特定的 ...
随机推荐
- collate utf8_bin是什么意思
创建数据库时 collate utf_bin是 以二进制值比较,也就是区分大小写,collate是核对的意思 uft-8_general_ci 一般比较,不区分大小写
- LeetCode 每日一题 458. 可怜的小猪
题目描述 有 buckets 桶液体,其中 正好 有一桶含有毒药,其余装的都是水.它们从外观看起来都一样.为了弄清楚哪只水桶含有毒药,你可以喂一些猪喝,通过观察猪是否会死进行判断.不幸的是,你只有 m ...
- uni微信小程序优化,打包后的import vue路径是可删除的
这次的优化我公司项目主包只减小了32kb,但是减小的不仅仅是主包,所有分包均在没有改动任何业务代码的情况下完成了压缩空间的优化. 主包分包压缩空间的优化都要视项目而定,32kb只是我公司的小程序项目. ...
- ApacheCN Pandas 教程集
Pandas 秘籍 零.前言 一.Pandas 基础 二.数据帧基本操作 三.开始数据分析 四.选择数据子集 五.布尔索引 六.索引对齐 七.分组以进行汇总,过滤和转换 八.将数据重组为整齐的表格 九 ...
- Atcoder ARC-104
D 可以发现,从整体上来看这个式子是不好计数的,可以考虑反过来将贡献拆到单个的每个数上: \[\sum\limits_{i = 1} ^ n a_i \times (i - x) = 0 \] 于是每 ...
- RPC和REST的区别
REST定义 REST是一种架构风格,指的是一组架构约束条件和原则.满足这些约束条件和原则的应用程序或设计就是 RESTful.REST规范把所有内容都视为资源,网络上一切皆资源.REST并没有创造新 ...
- bash_profile和bashsrc的区别
感谢大佬:http://unclealan.cn/index.php/system/128.html 描述 在类Linux或者MACOS系统中,家目录(用户目录)中我们会看到,.bash_profil ...
- Java基础复习(七)
一.基本语法 1. java没有sizeof.goto.const这些关键字,但不能用goto.const作为变量名,虽然可以用sizeof,但为啥非得要用这个呢. 2.十六进制数以0x或0X开头: ...
- 简单仿京东"筛选"界面 双导航栏控制器共存 by Nicky.Tsui
大概就是这么一个效果 如图.大概可以看到,"筛选"视图后面有一层视图盖住了后面原来的视图 那么我们可以通过加一个view到导航栏控制器的view里面来实现 //该view作为全局变 ...
- LVS+Keepalived 高可用群集部署
LVS+Keepalived 高可用群集部署 1.LVS+Keepalived 高可用群集概述 2.LVS+Keepalived高可用群集部署 1.LVS+Keepalived 高可用群集概述: LV ...