Golang构建HTTP服务(二)--- Handler,ServeMux与中间件
Golang标准库http包提供了基础的http服务,这个服务又基于Handler接口和ServeMux结构的做Mutilpexer。实际上,go的作者设计Handler这样的接口,不仅提供了默认的ServeMux对象,开发者也可以自定义ServeMux对象。
本质上ServeMux只是一个路由管理器,而它本身也实现了Handler接口的ServeHTTP方法。因此围绕Handler接口的方法ServeHTTP,可以轻松的写出go中的中间件。
在go的http路由原理讨论中,追本溯源还是讨论Handler接口和ServeMux结构。下面就基于这两个对象开始更多关于go中http的故事吧。
介绍http库源码的时候,创建http服务的代码很简单,实际上代码隐藏了很多细节,才有了后来的流程介绍。本文的目的主要是把这些细节暴露,从更底层的方式开始,一步步隐藏细节,完成样例代码的一样的逻辑。了解更多http包的原理之后,才能基于此构建中间件。
自定义的Handler
标准库http提供了Handler接口,用于开发者实现自己的handler。只要实现接口的ServeHTTP方法即可。
关于约定名词 handler函数,handler处理器,handler,请参考http原理与源码笔记中的定义。不然对下文的描述将会很困惑。
type textHandler struct {
responseText string
}
func (th *textHandler) ServeHTTP(w http.ResponseWriter, r *http.Request){
fmt.Fprintf(w, th.responseText)
}
type indexHandler struct {}
func (ih *indexHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
html := `<doctype html>
<html>
<head>
<title>Hello World</title>
</head>
<body>
<p>
<a href="/welcome">Welcome</a> | <a href="/message">Message</a>
</p>
</body>
</html>`
fmt.Fprintln(w, html)
}
func main() {
mux := http.NewServeMux()
mux.Handle("/", &indexHandler{})
thWelcome := &textHandler{"TextHandler !"}
mux.Handle("/text",thWelcome)
http.ListenAndServe(":8000", mux)
}
上面自定义了两个handler结构,都实现了ServeHTTP方法。我们知道,NewServeMux可以创建一个ServeMux实例,ServeMux同时也实现了ServeHTTP方法,因此代码中的mux也是一种handler。把它当成参数传给http.ListenAndServe方法,后者会把mux传给Server实例。因为指定了handler,因此整个http服务就不再是DefaultServeMux,而是mux,无论是在注册路由还是提供请求服务的时候。
有一点值得注意,这里并没有使用HandleFunc注册路由,而是直接使用了mux注册路由。当没有指定mux的时候,系统需要创建一个默认的defaultServeMux,此时我们已经有了mux,因此不再需要http.HandleFucn方法了,直接使用mux的Handle方法注册即可。
此外,Handle第二个参数是一个handler(处理器),并不是HandleFunc的一个handler函数,其原因也是因为mux.Handle本质上就需要绑定url的pattern模式和handler(处理器)即可。既然indexHandler是handle(处理器),当然就能作为参数,一切请求的处理过程,都交给器实现的接口方法ServeHTTP就行了。这个过程有点饶,如果不甚了解,建议先阅读http原理与源码笔记了解注册路由的本质。下图
handleFunc-handle
左边的12两步只是为了创建一个ServeMux实例,然后调用实例的Handle方法,右边的直接就调用了mux实例的Handle方法。
创建handler处理器
上面费劲口舌罗嗦,不就是1,2,3与3的差别么,并且1,2的两步操作,封装程度更高,开发者只需要写函数即可,不用再定义结构。代码更简洁,因此,下面将直接创建handler函数,调用go的方法将函数转变成handler(处理器)。
func text(w http.ResponseWriter, r *http.Request){
fmt.Fprintln(w, "hello world")
}
func index(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
html := `<doctype html>
<html>
<head>
<title>Hello World</title>
</head>
<body>
<p>
<a href="/welcome">Welcome</a> | <a href="/message">Message</a>
</p>
</body>
</html>`
fmt.Fprintln(w, html)
}
func main() {
mux := http.NewServeMux()
mux.Handle("/", http.HandlerFunc(index))
mux.HandleFunc("/text", text)
http.ListenAndServe(":8000", mux)
}
代码中使用了http.HandlerFunc
方法直接将一个handler函数转变成实现了handler(处理器)。等价与图中的3的步骤。
而mux.HandleFunc("/text", text)
就更进一步,与图中的2步骤一致,与defaultServemux.HandleFunc(pattern, function)
的用法一样。
使用默认的DefaultServeMux
经过了上面两个过程的转化,隐藏了更多的细节,代码与defaultServeMux的方式越来越像。下面再去掉自定义的ServeMux,只需要修改main函数的逻辑如下:
func main() {
http.Handle("/", http.HandlerFunc(index))
http.HandleFunc("/text", text)
http.ListenAndServe(":8000", nil)
}
上述的代码就和前文的例子一样,当代码中不显示的创建serveMux对象,http包就默认创建一个DefaultServeMux对象用来做路由管理器mutilplexer。
自定义Server
默认的DefaultServeMux创建的判断来自server对象,如果server对象不提供handler,才会使用默认的serveMux对象。既然ServeMux可以自定义,那么Server对象一样可以。
使用http.Server 即可创建自定义的server对象:
func main(){
http.HandleFunc("/", index)
server := &http.Server{
Addr: ":8000",
ReadTimeout: 60 * time.Second,
WriteTimeout: 60 * time.Second,
}
server.ListenAndServe()
}
自定义的serverMux对象也可以传到server对象中。
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", index)
server := &http.Server{
Addr: ":8000",
ReadTimeout: 60 * time.Second,
WriteTimeout: 60 * time.Second,
Handler: mux,
}
server.ListenAndServe()
}
可见go中的路由和处理函数之间关系非常密切,同时又很灵活。通过巧妙的使用Handler接口,可以设计出优雅的中间件程序。
中间件Middleware
所谓中间件,就是连接上下级不同功能的函数或者软件,通常进行一些包裹函数的行为,为被包裹函数提供添加一些功能或行为。前文的HandleFunc就能把签名为 func(w http.ResponseWriter, r *http.Reqeust)
的函数包裹成handler。这个函数也算是中间件。
这里我们以HTTP请求的中间件为例子,提供一个log中间件,能够打印出每一个请求的log。
go的http中间件很简单,只要实现一个函数签名为func(http.Handler) http.Handler
的函数即可。http.Handler是一个接口,接口方法我们熟悉的为serveHTTP。返回也是一个handler。因为go中的函数也可以当成变量传递或者或者返回,因此也可以在中间件函数中传递定义好的函数,只要这个函数是一个handler即可,即实现或者被handlerFunc包裹成为handler处理器。
func middlewareHandler(next http.Handler) http.Handler{
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request){
// 执行handler之前的逻辑
next.ServeHTTP(w, r)
// 执行完毕handler后的逻辑
})
}
这种方式在Elixir的Plug框架中很流行,思想偏向于函数式范式。熟悉python的朋友一定也想到了装饰器。闲话少说,来看看go是如何实现的吧:
func loggingHandler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
log.Printf("Started %s %s", r.Method, r.URL.Path)
next.ServeHTTP(w, r)
log.Printf("Comleted %s in %v", r.URL.Path, time.Since(start))
})
}
func main() {
http.Handle("/", loggingHandler(http.HandlerFunc(index)))
http.ListenAndServe(":8000", nil)
}
loggingHandler即是一个中间件函数,将请求的和完成的时间处理。可以看见请求或go的输出:
2016/12/04 21:18:13 Started GET /
2016/12/04 21:18:13 Comleted / in 13.365µs
2016/12/04 21:18:20 Started GET /
2016/12/04 21:18:20 Comleted / in 17.541µs
既然中间件是一种函数,并且签名都是一样,那么很容易就联想到函数一层包一层的中间件。再添加一个函数,然后修改main函数:
func hook(next http.Handler) http.Handler{
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Println("before hook")
next.ServeHTTP(w, r)
log.Println("after hook")
})
}
func main() {
http.Handle("/", hook(loggingHandler(http.HandlerFunc(index))))
http.ListenAndServe(":8000", nil)
}
在loggingHandler再包了一层hook,可以看到输出为:
2016/12/04 21:26:30 before hook
2016/12/04 21:26:30 Started GET /
2016/12/04 21:26:30 Comleted / in 14.016µs
2016/12/04 21:26:30 after hook
函数调用形成了一条链,可以是在这条链上做很多事情。当然go的写法上,比起elixir的|>
的符号,优雅性略差。
总结
通过对http包的源码学习,我们了解了Handler接口和ServeMux结构。并且知道如何配合他们实现go的中间件函数。当然,对于几个约定名词,handler函数,handler处理器和handler对象的理解,是掌握它们关系的关键因素,而handler处理器和handler对象的关系,恰恰又是go接口使用的经典例子,让go具有一些动态类型的特性。
了解了http服务如何构建之后,处理请求和返回响应就是下一个故事。而实现处理逻辑恰恰在我们一直在强调的ServeHTTP接口方法中。
接下来将会更详细的讨论请求和响应相关的函数对象。
关于作者
作者: 人世间 来源: 简书
- 【Go 语言社区】并发性
- GoldenGate数据迁移的问题总结(一)(r10笔记第84天)
- Elasticsearch大文件检索性能提升20倍实践(干货)
- Elasticsearch聚合优化 | 聚合速度提升5倍!
- Elasticsearch聚合后分页深入详解
- 可扩展机器学习——线性回归(linear Regression)
- 简单易学的机器学习算法——Label Propagation
- 利用Theano理解深度学习——Convolutional Neural Networks
- 持续精进——我的2017年终总结
- 实战 | Elasticsearch打造知识库检索系统
- Elasticsearch实战 | 必要的时候,还得空间换时间!
- 转--以io.Writer为例看go中的interface{}
- Go支持https协议的简单例子
- Elasticsearch索引增量统计及定时邮件实现
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- 极端情况下收缩 Go 进程的线程数
- C++核心准则T.60:最小化模板对上下文的依赖
- C++核心准则T.61:不要过度参数化成员(SCARY)
- C++核心准则T.62:将非依赖类模板成员放入非模板基类中
- C++核心准则T.64:使用特化提供类模板的不同实现
- C++核心准则T.68:在模板中使用{}代替()以避免歧义
- C++核心准则T.69:在模板内部,不要进行不受限制的非成员函数调用
- C++核心准则T.80:不要天真地模板化类继承
- 贪心算法求解:王者荣耀购买点券最优策略
- 面试老被问LinkedList源码?(深度剖析)
- 汽水瓶问题(非常interesting)
- 八种方法(实现两个数互换),绝了绝了!
- 当你触摸屏幕时手机都干了什么?你必须知道的Android事件传递
- 栈与队列:来看看栈和队列不为人知的一面
- 栈与队列:我用栈来实现队列怎么样?