Go语言经典库使用分析——高性能可扩展 HTTP 路由 httprouter(转载)
转载自 飞雪无情的博客
Go语言(golang)的一个很大的优势,就是很容易的开发出网络后台服务,而且性能快,效率高。在开发后端HTTP网络应用服务的时候,我们需要处理很多HTTP的请求访问,比如常见的API服务,我们就要处理很多HTTP请求,然后把处理的信息返回给使用者。对于这类需求,Golang提供了内置的net/http
包帮我们来处理这些HTTP请求,让我们可以比较方便的开发一个HTTP服务。
net/http
func main() {
http.HandleFunc("/",Index)
log.Fatal(http.ListenAndServe(":8080", nil))
}
func Index(w http.ResponseWriter, r *http.Request){
fmt.Fprint(w,"Blog:www.flysnow.org\nwechat:flysnow_org")
}
这是net/http
包中一个经典的HTTP服务实现,我们运行后打开http://localhost:8080
,就可以看到如下信息:
Blog:www.flysnow.org
wechat:flysnow_org
显示的关键就是我们http.HandleFunc
函数,我们通过该函数注册了对路径/
的处理函数Index
,所有才会看到上面的显示信息。那么这个http.HandleFunc
他是如何注册Index
函数的呢?下面看看这个函数的源代码。
// DefaultServeMux is the default ServeMux used by Serve.
var DefaultServeMux = &defaultServeMux
var defaultServeMux ServeMux
type ServeMux struct {
mu sync.RWMutex
m map[string]muxEntry
hosts bool // whether any patterns contain hostnames
}
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
DefaultServeMux.HandleFunc(pattern, handler)
}
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
if handler == nil {
panic("http: nil handler")
}
mux.Handle(pattern, HandlerFunc(handler))
}
看以上的源代码,是存在一个默认的DefaultServeMux
路由的,这个DefaultServeMux
类型是ServeMux
,我们注册的路径函数信息都被存入ServeMux
的m
字段中,以便处理HTTP请求的时候使用。
DefaultServeMux.HandleFunc
函数最终会调用ServeMux.Handle
函数。
type muxEntry struct {
h Handler
pattern string
}
func (mux *ServeMux) Handle(pattern string, handler Handler) {
//省略加锁和判断代码
if mux.m == nil {
mux.m = make(map[string]muxEntry)
}
//把我们注册的路径和相应的处理函数存入了m字段中
mux.m[pattern] = muxEntry{h: handler, pattern: pattern}
if pattern[0] != '/' {
mux.hosts = true
}
}
这下应该明白了,注册的路径和相应的处理函数都存入了m
字段中。
既然注册存入了相应的信息,那么在处理HTTP请求的时候,就可以使用了。Go语言的net/http
更底层细节就不详细分析了,我们只要知道处理HTTP请求的时候,会调用Handler
接口的ServeHTTP
方法,而ServeMux
正好实现了Handler
。
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
//省略一些无关代码
h, _ := mux.Handler(r)
h.ServeHTTP(w, r)
}
上面代码中的mux.Handler
会获取到我们注册的Index
函数,然后执行它,具体mux.Handler
的详细实现不再分析了,大家可以自己看下源代码。
通过一个muxEntry
结构保路劲以及处理函数信息。注意区分Handle和Handler函数、handler函数。
现在我们可以总结下net/http
包对HTTP
请求的处理。
HTTP请求->ServeHTTP函数->ServeMux的Handler方法->Index函数
这就是整个一条请求处理链,现在我们明白了net/http
里对HTTP请求的原理。
net/http 的不足
我们自己在使用内置的net/http
的默认路径处理HTTP请求的时候,会发现很多不足,比如:
1. 不能单独的对请求方法(POST,GET等)注册特定的处理函数
2. 不支持Path变量参数
3. 不能自动对Path进行校准
4. 性能一般
5. 扩展性不足
6. ……
那么如何解决以上问题呢?一个办法就是自己写一个处理HTTP请求的路由,因为从上面的源代码我们知道,net/http
用的是默认的路径。
//这个是我们启动HTTP服务的函数,最后一个handler参数是nil
http.ListenAndServe(":8080", nil)
func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
handler := sh.srv.Handler
//这个判断成立,因为我们传递的是nil
if handler == nil {
handler = DefaultServeMux
}
//省略了一些代码
handler.ServeHTTP(rw, req)
}
通过以上的代码分析,我们自己在通过http.ListenAndServe
函数启动一个HTTP服务的时候,最后一个handler
的值是nil,所以上面的nil判断成立,使用的就是默认的路由DefaultServeMux
。
现在我们就知道如何使用自己定义的路由了,那就是给http.ListenAndServe
的最后一个参数handler
传一个自定义的路由,比如:
type CustomMux struct {
}
func (cm *CustomMux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w,"Blog:www.flysnow.org\nwechat:flysnow_org")
}
func main() {
log.Fatal(http.ListenAndServe(":8080", &CustomMux{}))
}
这个自定义的CustomMux
就是我们的路由,它显示了和使用net/http
演示的例子一样的功能。
现在我们改变下代码,只有GET
方法才会显示以上信息。
func (cm *CustomMux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.Method == "GET" {
fmt.Fprint(w,"Blog:www.flysnow.org\nwechat:flysnow_org")
} else {
fmt.Fprint(w,"bad http method request")
}
}
只需要改变下ServeHTTP
方法的处理逻辑即可,现在我们可以换不同的请求方法试试,就会显示不同的内容。
这个就是自定义,我们可以通过扩展ServeHTTP
方法的实现来添加我们想要的任何功能,包括上面章节列出来的net/http
的不足都可以解决,不过我们无需这么麻烦,因为开源大牛已经帮我们做了这些事情,它就是 github
httprouter
httprouter 是一个高性能、可扩展的HTTP路由,上面我们列举的net/http
默认路由的不足,都被httprouter 实现,我们先用一个例子,认识下 httprouter 这个强大的 HTTP 路由。
package main
import (
"fmt"
"github.com/julienschmidt/httprouter"
"net/http"
"log"
)
func Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
fmt.Fprintf(w, "Blog:%s \nWechat:%s","www.flysnow.org","flysnow_org")
}
func main() {
router := httprouter.New()
router.GET("/", Index)
log.Fatal(http.ListenAndServe(":8080", router))
}
这个例子,实现了在GET
请求/
路径时,会显示如下信息:
Blog:www.flysnow.org
wechat:flysnow_org
在这个例子中,首先通过httprouter.New()
生成了一个*Router
路由指针,然后使用GET
方法注册一个适配/
路径的Index
函数,最后*Router
作为参数传给ListenAndServe
函数启动HTTP服务即可。
其实不止是GET
方法,httprouter 为所有的HTTP Method 提供了快捷的使用方式,只需要调用对应的方法即可。
func (r *Router) GET(path string, handle Handle) {
r.Handle("GET", path, handle)
}
func (r *Router) HEAD(path string, handle Handle) {
r.Handle("HEAD", path, handle)
}
func (r *Router) OPTIONS(path string, handle Handle) {
r.Handle("OPTIONS", path, handle)
}
func (r *Router) POST(path string, handle Handle) {
r.Handle("POST", path, handle)
}
func (r *Router) PUT(path string, handle Handle) {
r.Handle("PUT", path, handle)
}
func (r *Router) PATCH(path string, handle Handle) {
r.Handle("PATCH", path, handle)
}
func (r *Router) DELETE(path string, handle Handle) {
r.Handle("DELETE", path, handle)
}
以上这些方法都是 httprouter 支持的,我们可以非常灵活的根据需要,使用对应的方法,这样就解决了net/http
默认路由的问题。
httprouter 命名参数
现代的API,基本上都是Restful API,httprouter提供的命名参数的支持,可以很方便的帮助我们开发Restful API。比如我们设计的API/user/flysnow
,这这样一个URL,可以查看flysnow
这个用户的信息,如果要查看其他用户的,比如zhangsan
,我们只需要访问API/user/zhangsan
即可。
现在我们可以发现,其实这是一种URL匹配模式,我们可以把它总结为/user/:name
,这是一个通配符,看个例子。
func UserInfo(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
fmt.Fprintf(w, "hello, %s!\n", ps.ByName("name"))
}
func main() {
router := httprouter.New()
router.GET("/user/:name",UserInfo)
log.Fatal(http.ListenAndServe(":8080", router))
}
当我们运行,在浏览器里输入http://localhost:8080/user/flysnow
时,就会显示hello, flysnow!
.
通过上面的代码示例,可以看到,路径的参数是以:
开头的,后面紧跟着变量名,比如:name
,然后在UserInfo
这个处理函数中,通过httprouter.Params
的ByName
获取对应的值。
:name
这种匹配模式,是精准匹配的,同时只能匹配一个,比如:
Pattern: /user/:name
/user/gordon 匹配
/user/you 匹配
/user/gordon/profile 不匹配
/user/ 不匹配
因为httprouter这个路由就是单一匹配的,所以当我们使用命名参数的时候,一定要注意,是否有其他注册的路由和命名参数的路由,匹配同一个路径,比如/user/new
这个路由和/user/:name
就是冲突的,不能同时注册。
这里稍微提下httprouter的另外一种通配符模式,就是把:
换成*
,也就是*name
,这是一种匹配所有的模式,不常用,比如:
Pattern: /user/*name
/user/gordon 匹配
/user/you 匹配
/user/gordon/profile 匹配
/user/ 匹配
因为是匹配所有的*
模式,所以只要*
前面的路径匹配,就是匹配的,不管路径多长,有几层,都匹配。
httprouter兼容http.Handler
通过上面的例子,我们应该已经发现,GET
方法的handle,并不是我们熟悉的http.Handler
,它是httprouter自定义的,相比http.Handler
多了一个通配符参数的支持。
type Handle func(http.ResponseWriter, *http.Request, Params)
自定义的Handle,唯一的目的就是支持通配符参数,如果你的HTTP服务里,有些路由没有用到通配符参数,那么可以使用原生的http.Handler
,httprouter是兼容支持的,这也为我们从net/http
的方式,升级为httprouter路由提供了方便,会高效很多。
func (r *Router) Handler(method, path string, handler http.Handler) {
r.Handle(method, path,
func(w http.ResponseWriter, req *http.Request, _ Params) {
handler.ServeHTTP(w, req)
},
)
}
func (r *Router) HandlerFunc(method, path string, handler http.HandlerFunc) {
r.Handler(method, path, handler)
}
httprouter通过Handler
和HandlerFunc
两个函数,提供了兼容http.Handle
r和http.HandlerFunc
的完美支持。从以上源代码中,我们可以看出,实现的方式也比较简单,就是做了一个http.Handler
到httprouter.Handle
的转换,舍弃了通配符参数的支持。
Handler处理链
得益于http.Handler
的模式,我们可以把不同的http.Handler
组成一个处理链,httprouter.Router
也是实现了http.Handler
的,所以它也可以作为http.Handler
处理链的一部分,比如和Negroni
、 Gorilla handlers
这两个库配合使用,关于这两个库的介绍,可以参考我以前写的文章。
这里使用一个官方的例子,作为Handler处理链的演示。
比如对多个不同的二级域名,进行不同的路由处理。
//一个新类型,用于存储域名对应的路由
type HostSwitch map[string]http.Handler
//实现http.Handler接口,进行不同域名的路由分发
func (hs HostSwitch) ServeHTTP(w http.ResponseWriter, r *http.Request) {
//根据域名获取对应的Handler路由,然后调用处理(分发机制)
if handler := hs[r.Host]; handler != nil {
handler.ServeHTTP(w, r)
} else {
http.Error(w, "Forbidden", 403)
}
}
func main() {
//声明两个路由
playRouter := httprouter.New()
playRouter.GET("/", PlayIndex)
toolRouter := httprouter.New()
toolRouter.GET("/", ToolIndex)
//分别用于处理不同的二级域名
hs := make(HostSwitch)
hs["play.flysnow.org:12345"] = playRouter
hs["tool.flysnow.org:12345"] = toolRouter
//HostSwitch实现了http.Handler,所以可以直接用
log.Fatal(http.ListenAndServe(":12345", hs))
}
以上就是一个简单的,针对不同域名,使用不同路由的例子,代码中的注释比较详细了,这里就不一一解释了。这个例子中,HostSwitch
和httprouter.Router
这两个http.Handler
就组成了一个http.Handler
处理链。
httprouter 静态文件服务
httprouter提供了很方便的静态文件服务,可以把一个目录托管在服务器上,以供访问。
router.ServeFiles("/static/*filepath",http.Dir("./"))
只需要这一句核心代码即可,这个就是把当前目录托管在服务器上,以供访问,访问路径是/static
。
使用ServeFiles
需要注意的是,第一个参数路径,必须要以/*filepath
,因为要获取我们要访问的路径信息。
func (r *Router) ServeFiles(path string, root http.FileSystem) {
if len(path) < 10 || path[len(path)-10:] != "/*filepath" {
panic("path must end with /*filepath in path '" + path + "'")
}
fileServer := http.FileServer(root)
r.GET(path, func(w http.ResponseWriter, req *http.Request, ps Params) {
req.URL.Path = ps.ByName("filepath")
fileServer.ServeHTTP(w, req)
})
}
这是源代码实现,我们发现,最后还是一个GET
请求服务,通过http.FileServer
把filepath
的路径的内容显示出来(如果路径是个目录则列出目录文件;如果路径是文件,则显示内容)。
通过上面的源代码,我们也可以知道,*filepath
这个通配符是为了获取要放问的文件路径,所以要符合预定,不然就会panic。
httprouter 异常捕获
很少有路由支持这个功能的,httprouter允许使用者,设置PanicHandler
用于处理HTTP请求中发生的panic。
func Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
panic("故意抛出的异常")
}
func main() {
router := httprouter.New()
router.GET("/", Index)
router.PanicHandler = func(w http.ResponseWriter, r *http.Request, v interface{}) {
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprintf(w, "error:%s",v)
}
log.Fatal(http.ListenAndServe(":8080", router))
}
演示例子中,我们通过设置router.PanicHandler
来处理发生的panic,处理办法是打印出来异常信息。然后故意在Index
函数中抛出一个painc,然后我们运行测试,会看到异常信息。
这是一种非常好的方式,可以让我们对painc进行统一处理,不至于因为漏掉的panic影响用户使用。
小结
httprouter还有不少有用的小功能,比如对404进行处理,我们通过设置Router.NotFound
来实现,我们看看Router
这个结构体的配置,可以发现更多有用的功能。
type Router struct {
//是否通过重定向,给路径自定加斜杠
RedirectTrailingSlash bool
//是否通过重定向,自动修复路径,比如双斜杠等自动修复为单斜杠
RedirectFixedPath bool
//是否检测当前请求的方法被允许
HandleMethodNotAllowed bool
//是否自定答复OPTION请求
HandleOPTIONS bool
//404默认处理
NotFound http.Handler
//不被允许的方法默认处理
MethodNotAllowed http.Handler
//异常统一处理
PanicHandler func(http.ResponseWriter, *http.Request, interface{})
}
这些字段都是导出的(export),我们可以直接设置,来达到我们的目的。
httprouter是一个高性能,低内存占用的路由,它使用radix tree实现存储和匹配查找,所以效率非常高,内存占用也很低。关于radix tree大家可以查看相关的资料。
httprouter因为实现了http.Handler
,所以可扩展性非常好,可以和其他库、中间件结合使用,gin这个web框架就是使用的自定义的httprouter。
Go语言经典库使用分析——高性能可扩展 HTTP 路由 httprouter(转载)的更多相关文章
- 猴子吃桃问题之《C语言经典案例分析》
猴子吃桃问题之<C语言经典案例分析>一.[什么是猴子吃桃] 猴子吃桃问题:猴子第一天摘下若干个桃子,当即吃了一半,还不过瘾,又多吃了一个.第二天早上又将第一天剩下的桃子吃掉一半 ...
- JAVA语言之怎样写出高性能的Java代码?
本文主要向大家介绍了JAVA语言之怎样写出高性能的 Java 代码?通过具体的内容向大家展示,希望对大家学习JAVA语言有所帮助. 在这篇文章中,我们将讨论几个有助于提升Java应用程序性能的方法.我 ...
- 淘淘商城项目_同步索引库问题分析 + ActiveMQ介绍/安装/使用 + ActiveMQ整合spring + 使用ActiveMQ实现添加商品后同步索引库_匠心笔记
文章目录 1.同步索引库问题分析 2.ActiveM的介绍 2.1.什么是ActiveMQ 2.2.ActiveMQ的消息形式 3.ActiveMQ的安装 3.1.安装环境 3.2.安装步骤 4.Ac ...
- C语言经典例题100
C语言经典例题100 来源 http://www.fishc.com 适合初学者 ----------------------------------------------------------- ...
- 转载~kxcfzyk:Linux C语言多线程库Pthread中条件变量的的正确用法逐步详解
Linux C语言多线程库Pthread中条件变量的的正确用法逐步详解 多线程c语言linuxsemaphore条件变量 (本文的读者定位是了解Pthread常用多线程API和Pthread互斥锁 ...
- STM32固件库文件分析
STM32固件库文件分析 1.汇编编写的启动文件 startup/stm32f10x.hd.s:设置堆栈指针,设置pc指针,初始化中断向量,配置系统时钟,对用c库函数_main最后去c语言世界里. 2 ...
- R语言︱词典型情感分析文本操作技巧汇总(打标签、词典与数据匹配等)
每每以为攀得众山小,可.每每又切实来到起点,大牛们,缓缓脚步来俺笔记葩分享一下吧,please~ --------------------------- 笔者寄语:情感分析中对文本处理的数据的小技巧要 ...
- C语言函数库
C语言函数库 分类函数目录函数进程函数诊断函数接口子程序输入输出 str字符串操作函数mem操作存贮数组 数学函数 时间日期函数 转换函数 分类函数,所在函数库为ctype.h[top] int is ...
- C语言经典100例(51-100)
[程序51] 题目:学习使用按位与 & . 分析:0&0=0; 0&1=0; 1&0=0; 1&1=1 #include "stdio.h" ...
随机推荐
- 微信h5游戏如何在微信中做好域名防封 防屏蔽的 工作
最近微信开始大封杀,不知道原因是什么,可能是因为违规网站太多了吧,很多网站都被错杀了,下面我们聊一下怎样才能避免域名被封杀呢. 在各种不同的域名当中,能够做出了更适合的选择,这些对于大家域名防封_域名 ...
- php设计模式之简单工厂模式代码实例
<?php header("Content-type:text/html;charset=utf-8"); /** * 共同接口 */ interface db { func ...
- Lose it!
You are given an array aa consisting of nn integers. Each aiai is one of the six following numbers: ...
- centos7也支持service命令启动服务吗,对于centos7 中的systemctl和旧的service命令的区别和联系
一.centos7也支持service命令启动服务吗 CentOS 7.0中一个最主要的改变,就是切换到了systemd.它用于替代红帽企业版Linux前任版本中的SysV和Upstart,对系统和服 ...
- Codeforces 1207C Gas Pipeline (dp)
题目链接:http://codeforces.com/problemset/problem/1207/C 题目大意是给一条道路修管道,相隔一个单位的管道有两个柱子支撑,管道柱子高度可以是1可以是2,道 ...
- android .9背景图作为TextView背景时文字无法居中问题
问题产生原因: .9图黑色边框绘制伸缩区域有问题,重叠的最大区域是TextView文字所能显示的区域 如下图所示,横向和纵向最大重叠部分就是文字可显示部分,这个图作为背景后文字整体偏下,无法上下居中对 ...
- 计算机网络 - TCP_NODELAY 和 TCP_CORK, TCP_NOPUSH
参考 https://www.cnblogs.com/biyeymyhjob/p/4670502.html https://stackoverflow.com/questions/3761276/wh ...
- jquery点击添加样式,再次点击移除样式
$("#divSetting").on("click", function () { $(this).toggleClass("open") ...
- Java入门学习路线目录索引
原创 Java入门学习路线目录索引 版权声明:本文为博主原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明. 本文链接:https://blog.csdn.net/One_ ...
- DP_1d1d诗人小G
显然:f[i]=min{f[j]+(s[i]-s[j]+i-j-1-l)^p} 此题可以基于决策单调优化 证明,反正我现在不打算学 实际上就是双向队列 不停弹出队头的元素,直到当前位置在队头元素最优的 ...