一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第24天,点击查看活动详情。
当用户发起一个接口请求,到达服务的相应的接口处,服务进行请求响应。这一过程中,用户可能想要记录请求日志,限流,权限校验,拦截异常,异常处理等等。
接口请求
首先,从代码入手,我们有一个服务,服务上有三个接口,如下所示:
func main() {
http.HandleFunc("/hello", helloHandler)
http.HandleFunc("/list", listHandler)
http.HandleFunc("/info", infoHandler)
err := http.ListenAndServe(":8008", nil)
if err != nil {
fmt.Print(err)
}
}
func helloHandler(wr http.ResponseWriter, r *http.Request) {
wr.Write([]byte("hello"))
}
func listHandler(wr http.ResponseWriter, r *http.Request) {
wr.Write([]byte("list"))
}
func infoHandler(wr http.ResponseWriter, r *http.Request) {
wr.Write([]byte("info"))
}
服务正常,一切接口也都可以正常调用输出接口。但是来了一个临时需求:需要统计所有接口的处理耗时
于是,开始吭哧吭哧给每个接口都加上时间的统计,并打印出当前请求所消耗的时间。
func helloHandler(wr http.ResponseWriter, r *http.Request) {
timeStart := time.Now()
wr.Write([]byte("hello"))
timeElapsed := time.Since(timeStart)
fmt.Println("hello 接口 耗时" + timeElapsed.String())
}
func listHandler(wr http.ResponseWriter, r *http.Request) {
timeStart := time.Now()
wr.Write([]byte("list"))
timeElapsed := time.Since(timeStart)
fmt.Println("list 接口 耗时" + timeElapsed.String())
}
func infoHandler(wr http.ResponseWriter, r *http.Request) {
timeStart := time.Now()
wr.Write([]byte("info"))
timeElapsed := time.Since(timeStart)
fmt.Println("info 接口 耗时" + timeElapsed.String())
}
现有的接口只有几个可能感觉没事,但是当我们接口很多时,每个都这样操作,那将是一场灾难。也许觉得没关系,人力添加无可厚非的事情。当你又接到新的需求时,接口耗时需要上报到其他系统,这样就又需要改动所有的接口。
是否可以将这一部分无关业务的代码和业务代码分开呢?
http.handler
首先了解下 net/http包的 http.handler。
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
type HandlerFunc func(ResponseWriter, *Request)
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
- Handler 接口只有一个
ServeHTTP(ResponseWriter, *Request)方法,只要实现了ServerHttp 方法就相当于实现了 http.handler 接口。
- HandlerFunc 是一个函数类型,实现了 ServeHTTP 方法,即实现了 http.Handler 接口。可以看到 HandlerFunc 函数的 ServerHttp 方法就是调用函数本身
f(w, r).
请求调用链
http.HandleFunc("/hello", helloHandler)
func helloHandler(wr http.ResponseWriter, r *http.Request) {
wr.Write([]byte("hello"))
}
以 /hello接口为例,helloHandler 实现了 http.handler 接口。当请求过来时就会调用 handler 函数来处理,会调用 HandlerFunc 的 ServeHTTP请求。
h == getHandler() == > h.ServeHTTP() ===> h(w,r)
如果将非业务代码和业务代码分开,那么就需要专门处理时间统计的方法,然后将业务代码作为中间的一个过程。
func reqTimeMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(wr http.ResponseWriter, r *http.Request) {
timeStart := time.Now()
next.ServeHTTP(wr, r)
timeElapsed := time.Since(timeStart)
fmt.Println("接口 耗时" + timeElapsed.String())
})
}
http.Handle("/hello", reqTimeMiddleware(http.HandlerFunc(helloHandler)))
http.Handle("/list", reqTimeMiddleware(http.HandlerFunc(listHandler)))
http.Handle("/info", reqTimeMiddleware(http.HandlerFunc(infoHandler)))
一开始时,使用 http.HandlerFunc(“/hello” ,helloHandler) 是类型转换,把普通函数/方法类型转换成实现了 http.Handler 接口的类型。封装的中间件 reqTimeMiddleware 主要通过包装handler,再返回一个新的handler。
所以可以看到,中间件要做的事情就是通过一个或多个函数对handler进行包装,返回一个包括了各个中间件逻辑的函数链。
优雅中间件使用
在 Gin 框架中,我们可以对中间件有更优雅的写法。
定义一个中间件
func Logger() gin.HandlerFunc {
return func(c *gin.Context) {
t := time.Now()
// 给Context实例设置一个值
c.Set("geektutu", "1111")
// 请求前
c.Next()
// 请求后
latency := time.Since(t)
log.Print(latency)
}
}
使用中间件
r = NewRouter()
r.Use(logger)