简介
上一篇文章中,咱们介绍了 gorilla web 开发工具包中的路由治理库gorilla/mux
,在文章最初咱们介绍了如何应用中间件解决通用的逻辑。在日常 Go Web 开发中,开发者遇到了很多雷同的中间件需要,gorilla/handlers(后文简称为handlers
)收集了一些比拟罕用的中间件。一起来看看吧~
对于中间件,后面几篇文章曾经介绍的很多了。这里就不赘述了。handlers
库提供的中间件可用于规范库 net/http
和所有反对 http.Handler
接口的框架。因为 gorilla/mux
也反对 http.Handler
接口,所以也能够与 handlers
库联合应用。这就是兼容规范的益处。
我的项目初始化 & 装置
本文代码应用 Go Modules。
创立目录并初始化:
$ mkdir gorilla/handlers && cd gorilla/handlers
$ go mod init github.com/darjun/go-daily-lib/gorilla/handlers
装置 gorilla/handlers
库:
$ go get -u github.com/gorilla/handlers
上面顺次介绍各个中间件和相应的源码。
日志
handlers
提供了两个日志中间件:
LoggingHandler
:以 Apache 的Common Log Format
日志格局记录 HTTP 申请日志;CombinedLoggingHandler
:以 Apache 的Combined Log Format
日志格局记录 HTTP 申请日志,Apache 和 Nginx 默认都应用这种日志格局。
两种日志格局差异很小,Common Log Format
格局如下:
%h %l %u %t "%r" %>s %b
各个批示符含意如下:
%h
:客户端的 IP 地址或主机名;%l
:RFC 1413
定义的客户端标识,由客户端机器上的identd
程序生成。如果不存在,则该字段为-
;%u
:已验证的用户名。如果不存在,该字段为-
;-
%t
:工夫,格局为day/month/year:hour:minute:second zone
,其中:day
:2 位数字;month
:月份缩写,3 个字母,如Jan
;year
:4 位数字;hour
:2 位数字;minute
:2 位数字;second
:2 位数字;zone
:+
或-
后跟 4 位数字;- 例如:
21/Jul/2021:06:27:33 +0800
%r
:蕴含 HTTP 申请行信息,例GET /index.html HTTP/1.1
;%>s
:服务器发送给客户端的状态码,例如200
;%b
:响应长度(字节数)。
Combined Log Format
格局如下:
%h %l %u %t "%r" %>s %b "%{Referer}i" "%{User-Agent}i"
可见相比 Common Log Format
只是多了:
%{Referer}i
:HTTP 首部中的Referer
信息;%{User-Agent}i
:HTTP 首部中的User-Agent
信息。
对中间件,咱们能够让它作用于全局,即全副处理器,也能够让它只对某些处理器失效。如果要对所有处理器失效,能够调用 Use()
办法。如果只须要作用于特定的处理器,在注册时用中间件将处理器包装一层:
func index(w http.ResponseWriter, r *http.Request) {fmt.Fprintln(w, "Hello World")
}
type greeting string
func (g greeting) ServeHTTP(w http.ResponseWriter, r *http.Request) {fmt.Fprintf(w, "Welcome, %s", g)
}
func main() {r := mux.NewRouter()
r.Handle("/", handlers.LoggingHandler(os.Stdout, http.HandlerFunc(index)))
r.Handle("/greeting", handlers.CombinedLoggingHandler(os.Stdout, greeting("dj")))
http.Handle("/", r)
log.Fatal(http.ListenAndServe(":8080", nil))
}
下面代码中 LoggingHandler
只作用于处理函数 index
,CombinedLoggingHandler
只作用于处理器greeting("dj")
。
运行代码,通过浏览器拜访 localhost:8080
和localhost:8080/greeting
:
::1 - - [21/Jul/2021:06:39:45 +0800] "GET / HTTP/1.1" 200 12
::1 - - [21/Jul/2021:06:39:54 +0800] "GET /greeting HTTP/1.1" 200 11 """Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.164 Safari/537.36"
对照后面剖析的批示符,很容易看出各个局部。
因为 *mux.Router
的Use()
办法承受类型为 MiddlewareFunc
的中间件:
type MiddlewareFunc func(http.Handler) http.Handler
而 handlers.LoggingHandler/CombinedLoggingHandler
并不满足,所以还须要包装一层能力传给 Use()
办法:
func Logging(handler http.Handler) http.Handler {return handlers.CombinedLoggingHandler(os.Stdout, handler)
}
func main() {r := mux.NewRouter()
r.Use(Logging)
r.HandleFunc("/", index)
r.Handle("/greeting/", greeting("dj"))
http.Handle("/", r)
log.Fatal(http.ListenAndServe(":8080", nil))
}
另外 handlers
还提供了CustomLoggingHandler
,咱们能够利用它定义本人的日志中间件:
func CustomLoggingHandler(out io.Writer, h http.Handler, f LogFormatter) http.Handler
最要害的 LogFormatter
类型定义:
type LogFormatterParams struct {
Request *http.Request
URL url.URL
TimeStamp time.Time
StatusCode int
Size int
}
type LogFormatter func(writer io.Writer, params LogFormatterParams)
咱们实现一个简略的LogFormatter
,记录时间 + 申请行 + 响应码:
func myLogFormatter(writer io.Writer, params handlers.LogFormatterParams) {
var buf bytes.Buffer
buf.WriteString(time.Now().Format("2006-01-02 15:04:05 -0700"))
buf.WriteString(fmt.Sprintf(` "%s %s %s" `, params.Request.Method, params.URL.Path, params.Request.Proto))
buf.WriteString(strconv.Itoa(params.StatusCode))
buf.WriteByte('\n')
writer.Write(buf.Bytes())
}
func Logging(handler http.Handler) http.Handler {return handlers.CustomLoggingHandler(os.Stdout, handler, myLogFormatter)
}
应用:
func main() {r := mux.NewRouter()
r.Use(Logging)
r.HandleFunc("/", index)
r.Handle("/greeting/", greeting("dj"))
http.Handle("/", r)
log.Fatal(http.ListenAndServe(":8080", nil))
}
当初记录的日志是上面这种格局:
2021-07-21 07:03:18 +0800 "GET /greeting/ HTTP/1.1" 200
翻看源码,咱们能够发现 LoggingHandler/CombinedLoggingHandler/CustomLoggingHandler
都是基于底层的 loggingHandler
实现的,不同的是 LoggingHandler
应用了预约义的 writeLog
作为 LogFormatter
,CombinedLoggingHandler
应用了预约义的 writeCombinedLog
作为 LogFormatter
,而CustomLoggingHandler
应用咱们本人定义的LogFormatter
:
func CombinedLoggingHandler(out io.Writer, h http.Handler) http.Handler {return loggingHandler{out, h, writeCombinedLog}
}
func LoggingHandler(out io.Writer, h http.Handler) http.Handler {return loggingHandler{out, h, writeLog}
}
func CustomLoggingHandler(out io.Writer, h http.Handler, f LogFormatter) http.Handler {return loggingHandler{out, h, f}
}
预约义的 writeLog/writeCombinedLog
实现如下:
func writeLog(writer io.Writer, params LogFormatterParams) {buf := buildCommonLogLine(params.Request, params.URL, params.TimeStamp, params.StatusCode, params.Size)
buf = append(buf, '\n')
writer.Write(buf)
}
func writeCombinedLog(writer io.Writer, params LogFormatterParams) {buf := buildCommonLogLine(params.Request, params.URL, params.TimeStamp, params.StatusCode, params.Size)
buf = append(buf, ` "`...)
buf = appendQuoted(buf, params.Request.Referer())
buf = append(buf, `" "`...)
buf = appendQuoted(buf, params.Request.UserAgent())
buf = append(buf, '"','\n')
writer.Write(buf)
}
它们都是基于 buildCommonLogLine
结构根本信息,writeCombinedLog
还别离调用 http.Request.Referer()
和http.Request.UserAgent
获取了 Referer
和User-Agent
信息。
loggingHandler
定义如下:
type loggingHandler struct {
writer io.Writer
handler http.Handler
formatter LogFormatter
}
loggingHandler
实现有一个比拟奇妙的中央:为了记录响应码和响应大小,定义了一个类型 responseLogger
包装原来的http.ResponseWriter
,在写入时记录信息:
type responseLogger struct {
w http.ResponseWriter
status int
size int
}
func (l *responseLogger) Write(b []byte) (int, error) {size, err := l.w.Write(b)
l.size += size
return size, err
}
func (l *responseLogger) WriteHeader(s int) {l.w.WriteHeader(s)
l.status = s
}
func (l *responseLogger) Status() int {return l.status}
func (l *responseLogger) Size() int {return l.size}
loggingHandler
的要害办法ServeHTTP()
:
func (h loggingHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {t := time.Now()
logger, w := makeLogger(w)
url := *req.URL
h.handler.ServeHTTP(w, req)
if req.MultipartForm != nil {req.MultipartForm.RemoveAll()
}
params := LogFormatterParams{
Request: req,
URL: url,
TimeStamp: t,
StatusCode: logger.Status(),
Size: logger.Size(),}
h.formatter(h.writer, params)
}
结构 LogFormatterParams
对象,调用对应的 LogFormatter
函数。
压缩
如果客户端申请中有 Accept-Encoding
首部,服务器能够应用该首部批示的算法将响应压缩,以节俭网络流量。handlers.CompressHandler
中间件启用压缩性能。还有一个 CompressHandlerLevel
能够指定压缩级别。实际上 CompressHandler
就是应用 gzip.DefaultCompression
调用的CompressHandlerLevel
:
func CompressHandler(h http.Handler) http.Handler {return CompressHandlerLevel(h, gzip.DefaultCompression)
}
看代码:
func index(w http.ResponseWriter, r *http.Request) {fmt.Fprintln(w, "Hello World")
}
type greeting string
func (g greeting) ServeHTTP(w http.ResponseWriter, r *http.Request) {fmt.Fprintf(w, "Welcome, %s", g)
}
func main() {r := mux.NewRouter()
r.Use(handlers.CompressHandler)
r.HandleFunc("/", index)
r.Handle("/greeting/", greeting("dj"))
http.Handle("/", r)
log.Fatal(http.ListenAndServe(":8080", nil))
}
运行,申请localhost:8080
,通过 Chrome 开发者工具的 Network 页签能够看到响应采纳了 gzip 压缩:
疏忽一些细节解决,CompressHandlerLevel
函数代码如下:
func CompressHandlerLevel(h http.Handler, level int) http.Handler {
const (
gzipEncoding = "gzip"
flateEncoding = "deflate"
)
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var encoding string
for _, curEnc := range strings.Split(r.Header.Get(acceptEncoding), ",") {curEnc = strings.TrimSpace(curEnc)
if curEnc == gzipEncoding || curEnc == flateEncoding {
encoding = curEnc
break
}
}
if encoding == "" {h.ServeHTTP(w, r)
return
}
if r.Header.Get("Upgrade") != "" {h.ServeHTTP(w, r)
return
}
var encWriter io.WriteCloser
if encoding == gzipEncoding {encWriter, _ = gzip.NewWriterLevel(w, level)
} else if encoding == flateEncoding {encWriter, _ = flate.NewWriter(w, level)
}
defer encWriter.Close()
w.Header().Set("Content-Encoding", encoding)
r.Header.Del(acceptEncoding)
cw := &compressResponseWriter{
w: w,
compressor: encWriter,
}
w = httpsnoop.Wrap(w, httpsnoop.Hooks{Write: func(httpsnoop.WriteFunc) httpsnoop.WriteFunc {return cw.Write},
WriteHeader: func(httpsnoop.WriteHeaderFunc) httpsnoop.WriteHeaderFunc {return cw.WriteHeader},
Flush: func(httpsnoop.FlushFunc) httpsnoop.FlushFunc {return cw.Flush},
ReadFrom: func(rff httpsnoop.ReadFromFunc) httpsnoop.ReadFromFunc {return cw.ReadFrom},
})
h.ServeHTTP(w, r)
})
}
从申请 Accept-Encoding
首部中获取客户端批示的压缩算法。如果客户端未指定,或申请首部中有 Upgrade
,则不压缩。反之,则压缩。依据辨认的压缩算法,创立对应gzip
或flate
的 io.Writer
实现对象。
与后面的日志中间件一样,为了压缩写入的内容,新增类型 compressResponseWriter
封装 http.ResponseWriter
,重写Write()
办法,将写入的字节流传入后面创立的 io.Writer
实现压缩:
type compressResponseWriter struct {
compressor io.Writer
w http.ResponseWriter
}
func (cw *compressResponseWriter) Write(b []byte) (int, error) {h := cw.w.Header()
if h.Get("Content-Type") == "" {h.Set("Content-Type", http.DetectContentType(b))
}
h.Del("Content-Length")
return cw.compressor.Write(b)
}
内容类型
咱们能够通过 handler.ContentTypeHandler
指定申请的 Content-Type
必须在咱们给出的类型中,只对 POST/PUT/PATCH
办法失效。例如咱们限度登录申请必须通过 application/x-www-form-urlencoded
的模式发送:
func main() {r := mux.NewRouter()
r.HandleFunc("/", index)
r.Methods("GET").Path("/login").HandlerFunc(login)
r.Methods("POST").Path("/login").
Handler(handlers.ContentTypeHandler(http.HandlerFunc(dologin), "application/x-www-form-urlencoded"))
http.Handle("/", r)
log.Fatal(http.ListenAndServe(":8080", nil))
}
这样,只有申请 /login
的Content-Type
不是 application/x-www-form-urlencoded
就会返回 415 谬误。咱们能够成心写错,再申请看看体现:
Unsupported content type "application/x-www-form-urlencoded"; expected one of ["application/x-www-from-urlencoded"]
ContentTypeHandler
的实现非常简单:
func ContentTypeHandler(h http.Handler, contentTypes ...string) http.Handler {return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {if !(r.Method == "PUT" || r.Method == "POST" || r.Method == "PATCH") {h.ServeHTTP(w, r)
return
}
for _, ct := range contentTypes {if isContentType(r.Header, ct) {h.ServeHTTP(w, r)
return
}
}
http.Error(w, fmt.Sprintf("Unsupported content type %q; expected one of %q", r.Header.Get("Content-Type"), contentTypes), http.StatusUnsupportedMediaType)
})
}
就是读取 Content-Type
首部,判断是否在咱们指定的类型中。
办法处理器
在下面的例子中,咱们注册门路 /login
的GET
和 POST
办法解决采纳 r.Methods("GET").Path("/login").HandlerFunc(login)
这种简短的写法。handlers.MethodHandler
能够简化这种写法:
func main() {r := mux.NewRouter()
r.HandleFunc("/", index)
r.Handle("/login", handlers.MethodHandler{"GET": http.HandlerFunc(login),
"POST": http.HandlerFunc(dologin),
})
http.Handle("/", r)
log.Fatal(http.ListenAndServe(":8080", nil))
}
MethodHandler
底层是一个 map[string]http.Handler
类型,它的 ServeHTTP()
办法依据申请的 Method 调用不同的解决:
type MethodHandler map[string]http.Handler
func (h MethodHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {if handler, ok := h[req.Method]; ok {handler.ServeHTTP(w, req)
} else {allow := []string{}
for k := range h {allow = append(allow, k)
}
sort.Strings(allow)
w.Header().Set("Allow", strings.Join(allow, ","))
if req.Method == "OPTIONS" {w.WriteHeader(http.StatusOK)
} else {http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
}
}
办法如果未注册,则返回 405 Method Not Allowed
。有一个办法除外,OPTIONS
。该办法通过Allow
首部返回反对哪些办法。
重定向
handlers.CanonicalHost
能够将申请重定向到指定的域名,同时指定重定向响应码。在同一个服务器对应多个域名时比拟有用:
func index(w http.ResponseWriter, r *http.Request) {fmt.Fprintln(w, "hello world")
}
func main() {r := mux.NewRouter()
r.Use(handlers.CanonicalHost("http://www.gorillatoolkit.org", 302))
r.HandleFunc("/", index)
http.Handle("/", r)
log.Fatal(http.ListenAndServe(":8080", nil))
}
下面将所有申请以 302 重定向到http://www.gorillatoolkit.org
。
CanonicalHost
的实现也很简略:
func CanonicalHost(domain string, code int) func(h http.Handler) http.Handler {fn := func(h http.Handler) http.Handler {return canonical{h, domain, code}
}
return fn
}
要害类型canonical
:
type canonical struct {
h http.Handler
domain string
code int
}
外围办法:
func (c canonical) ServeHTTP(w http.ResponseWriter, r *http.Request) {dest, err := url.Parse(c.domain)
if err != nil {c.h.ServeHTTP(w, r)
return
}
if dest.Scheme == ""|| dest.Host =="" {c.h.ServeHTTP(w, r)
return
}
if !strings.EqualFold(cleanHost(r.Host), dest.Host) {
dest := dest.Scheme + "://" + dest.Host + r.URL.Path
if r.URL.RawQuery != "" {dest += "?" + r.URL.RawQuery}
http.Redirect(w, r, dest, c.code)
return
}
c.h.ServeHTTP(w, r)
}
由源码可知,域名不非法或未指定协定(Scheme)或域名(Host)的申请下不转发。
Recovery
之前咱们本人实现了 PanicRecover
中间件,防止申请解决时 panic。handlers
提供了一个 RecoveryHandler
能够间接应用:
func PANIC(w http.ResponseWriter, r *http.Request) {panic(errors.New("unexpected error"))
}
func main() {r := mux.NewRouter()
r.Use(handlers.RecoveryHandler(handlers.PrintRecoveryStack(true)))
r.HandleFunc("/", PANIC)
http.Handle("/", r)
log.Fatal(http.ListenAndServe(":8080", nil))
}
选项 PrintRecoveryStack
示意 panic 时输入堆栈信息。
RecoveryHandler
的实现与之前咱们本人编写的根本一样:
type recoveryHandler struct {
handler http.Handler
logger RecoveryHandlerLogger
printStack bool
}
func (h recoveryHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {defer func() {if err := recover(); err != nil {w.WriteHeader(http.StatusInternalServerError)
h.log(err)
}
}()
h.handler.ServeHTTP(w, req)
}
总结
GitHub 上有很多开源的 Go Web 中间件实现,能够间接拿来应用,防止反复造轮子。handlers
很轻量,容易与规范库 net/http
和 gorilla 路由库 mux
联合应用。
大家如果发现好玩、好用的 Go 语言库,欢送到 Go 每日一库 GitHub 上提交 issue😄
参考
- gorilla/handlers GitHub:github.com/gorilla/handlers
- Go 每日一库 GitHub:https://github.com/darjun/go-daily-lib
我
我的博客:https://darjun.github.io
欢送关注我的微信公众号【GoUpUp】,独特学习,一起提高~