网站首页 > 技术文章 正文
HTTP路由器)负责侦听HTTP请求并根据匹配条件(例如HTTP方法或URL)调用适当的处理程序。
Golang提供了一个非常简单的路由器ServeMux。但它太基础简单,所以大家一般都会选择第三方路由模块,比如gorilla/mux。
今天我们来学习下如何从零自己构建一个HTTP路由。
概述
一个HTTP路由器主要负责以下几件事:
404处理程序:为不匹配的请求提供404响应
匹配:匹配URL路径和HTTP方法并调用路由处理程序
参数:提取动态网址参数,例如/users/(?P<id>\d+)
紧急恢复:赶上紧急情况并回复500
下面是一个代码片段,展示了上述的所有功能:
r := NewRouter()
r.Route("GET", "/", homeRoute)
r.Route("POST", "/users", createUserRoute)
r.Route("GET", "/users/(?P<ID>\d+)", getUserRoute)
r.Route("GET", "/panic", panicRoute)
http.ListenAndServe("localhost:8000", r)
基本路由
首先,我们构建一个路由,该路由负责响应无效请求,并返回404响应。
路由器处理进入Web服务器的每个HTTP请求,可以通过将其传递到Golang的http.ListenAndServe方法中来完成。ListenAndServe的第二个参数是http.Handler,它负责处理每个传入的请求。为了实现这一点,我们的路由器将需要实现该Handler接口。
Handler只声明一个方法,ServeHTTP所以我们创建一个结构来匹配它。
type Router struct {}
func (sr *Router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
http.NotFound(w, r)
}
这样就有一种可以在任何http.Handler接受的地方使用的路由类型。把加入到可运行的程序中httper.go。
package httper
import "net/http"
func main() {
r := &Router{}
http.ListenAndServe(":8000", r)
}
type Router struct{}
func (sr *Router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
http.NotFound(w, r)
从命令行运行该程序go run httper.go,然后就可以通过Web浏览器中打开127.0.0.1:8000,验证其是否响应"404页面未找到"。
路由匹配
一个总是返回404请求的路由并什么太多用处。我们继续修改路由以便可以匹配的列表。
对于每个传入请求,需要执行以下操作:
从请求中提取HTTP方法和URL路径;
检查是否存在与方法和路径匹配的路由;
匹配时调用它;
如果找不到匹配项,则返回404。
为此,为每条路由需要保存这些信息:路由的HTTP方法,路由的路径以及如果找到匹配项,则调用的处理函数。我们创建一个结构RouteEntry来将存储在他们。
type RouteEntry struct {
Path string
Method string
Handler http.HandlerFunc
}
还需要更新Router以存储的列表RouteEntry。为了改善使用路由的体验,我们添加一个名为helper的辅助功能Route来完成这项工作。路由功能将创建一个新路由RouteEntry并将其添加到路由列表中。
type RouteEntry struct {
Path string
Method string
Handler http.HandlerFunc
}
type Router struct {
routes []RouteEntry
}
func (rtr *Router) Route(method, path string, handlerFunc http.HandlerFunc) {
e := RouteEntry{
Method: method,
Path: path,
HandlerFunc: handlerFunc,
}
rtr.routes = append(rtr.routes, e)
}
最后,编写逻辑以检查传入的请求并找到匹配的路由。
匹配逻辑有两个明显的地方:Router本身还是RouteEntry。这些位置中的任何一个都可以使用,但是使用RouteEntry匹配负责是明智的,因为它存储了要匹配的条件。
我们给RouteEntry结构添加一个Match方法。由于基于请求的信息进行匹配,因此将request作为参数。为了表明匹配成功,将让它返回一个布尔值。
func (re *RouteEntry) Match(r *http.Request) bool {
if r.Method != re.Method {
return false
}
if r.URL.Path != re.Path {
return false
}
return true
}
现在,路由器所需要做的就是遍历所有路由,并检查其中是否有匹配请求。
func (rtr *Router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
for _, e := range rtr.routes {
match := e.Match(r)
if !match {
continue
}
e.HandlerFunc.ServeHTTP(w, r)
return
}
http.NotFound(w, r)
}
为了确保所有操作都能正常进行,新添加一条简单的路由来处理。
r := &Router{}
r.Route("GET", "/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello,Chongchong!"))
})
当加入这些代码,然后go run httper.go。可以通过浏览器访问127.0.0.1:8000/来验证其是否有效。应该看到它以"Hello,Chongchong!"回应。任何其路径会返回404响应。
提取路由参数
现在,有了一个基本实用的HTTP路由器。我们进一步添加功能充实它。常用的系统处理API中都会涉及增删改查(CRUD)的动态参数的定义的路由。例如,URL通过ID获取用户的路由,可能的路径为/users/10 ,其中10为用户ID。在当前的路由器中,如果一个一个的为每个可能的用户ID都定义一个路由显然是冗杂和不必要的。实际上需要的是一种定义带有动态路径的方法/users/?。
为了执行动态匹配,需要使用利器——正则表达式。
访问参数
不过,在深入探讨正则表达式之前,先讨论一下路由处理程序将如何访问提取的参数。一个fetchUserRoute将需要能够从URL中提取ID来获取正确的用户。
幸运的是,Golang提供了一种机制,可以将短暂的数据存储在称为context的请求对象上。用这种机制,路由器可以将参数添加到请求上下文中,以供处理程序在调用时读取。
下面是处理程序如何访问参数的示例。注意,由于访问请求上下文中的内容有点麻烦,因此又创建一个了辅助函数来减少重复。
r.Route("GET", `/hello/(?P<Message>\w+)`, func(w http.ResponseWriter, r *http.Request) {
message := URLParam(r, "Message")
w.Write([]byte("Hello " + message))
})
func URLParam(r *http.Request, name string) string {
ctx := r.Context()
params := ctx.Value("params").(map[string]string)
return params[name]
}
用正则匹配
将把参数存储在中map[string]string,其中映射中的每个键都是参数名称,而值是从URL中提取的值。正则表达式已命名了适合此用例的组。在Golang中,可以使用FindStringSubmatch方法匹配这些命名组。
r := regexp.MustCompile(
`/books/(?P<AuthorID>\d+)/(?P<BookID>\d+)`,
)
match := r.FindStringSubmatch("/books/123/456")
if match == nil {
return
}
fmt.Println(match) // [123, 456]
fmt.Println(r.SubexpNames()) // [AuthorID, BookID]
保存网址参数
知道如何匹配正则表达式组,我们将可以更新RouteEntry结构的匹配逻辑以使用它们。为此,需要将Path属性从字符串更改为Regexp类型。然后,需要更新Match方法逻辑。
type RouteEntry struct {
Path *regexp.Regexp
Method string
HandlerFunc http.HandlerFunc
}
func (ent *RouteEntry) Match(r *http.Request) map[string]string {
match := ent.Path.FindStringSubmatch(r.URL.Path)
if match == nil {
return nil
}
params := make(map[string]string)
groupNames := ent.Path.SubexpNames()
for i, group := range match {
params[groupNames[i]] = group
}
return params
}
注意,上面还更改了的签名Match以返回参数映射,而非布尔值。
最后需要做的一件事是更新路由器逻辑,以在找到匹配项后将参数添加到请求上下文中。
for _, e := range rtr.routes {
params := e.Match(r)
if params == nil {
continue
}
ctx := context.WithValue(r.Context(), "params", params)
e.HandlerFunc.ServeHTTP(w, r.WithContext(ctx))
return
}
我们在程序中添加这些部分,然后测试:
Panic恢复
添加动态URL参数极大地提高了路由器的实用性。现在可以将其在一些项目中使用。为了防止生产中发生坏事,应该增加另外一件事,那就是紧急恢复。
当前,如果路由处理程序之一出现紧急情况,服务器将返回一个空响应,而不是默认页面。将添加以下几行代码来捕获这些紧急情况并返回适当的500(内部服务器错误)状态代码。
func (rtr *Router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
defer func() {
if r := recover(); r != nil {
log.Println("ERROR:", r)
http.Error(w, "发生错误…", http.StatusInternalServerError)
}
}()
// ...
}
为了测试它是否有效,我们添加一条特殊的/panic路由来触发该恢复逻辑。
r.Route("GET", "/panic", func(w http.ResponseWriter, r *http.Request) {
panic("something bad happened!")
})
测试访问 127.0.0.1:8000/panic,就会返回 Uh oh!
总结
本我们实例介绍了如何使用Golang语言的标准库,从头开始构建一个路由器,当然我们构建的路由器仅仅为HTTP路由原理说明、练手和好玩,不建议在生产环境使用!在生产中使用建议使用成熟的类库,比如gorilla/mux。
猜你喜欢
- 2024-11-02 武汉课工场大数据培训:Java正则表达式入坑指南
- 2024-11-02 Go语言进阶之路:并发爬虫,爬取空姐网所有相册图片
- 2024-11-02 golang常用库:gorilla/mux-http路由库使用
- 2024-11-02 golang 使用pprof和go-torch做性能分析
- 2024-11-02 Golang Gin 入门 (一)(golang官方教程)
- 2024-11-02 日志文件转运工具Filebeat笔记(日志转载)
- 2024-11-02 Linux 命令行下搜索工具大盘点,效率提高不止一倍
- 2024-11-02 SlimTrie:战胜Btree单机百亿文件的极致索引-实现篇
- 2024-11-02 Go的安全编程和防御性编程(输入验证和过滤)
- 2024-11-02 清华学神尹成带你学习golang2021(56)(Go语言测试命令)
- 最近发表
- 标签列表
-
- cmd/c (57)
- c++中::是什么意思 (57)
- sqlset (59)
- ps可以打开pdf格式吗 (58)
- phprequire_once (61)
- localstorage.removeitem (74)
- routermode (59)
- vector线程安全吗 (70)
- & (66)
- java (73)
- org.redisson (64)
- log.warn (60)
- cannotinstantiatethetype (62)
- js数组插入 (83)
- resttemplateokhttp (59)
- gormwherein (64)
- linux删除一个文件夹 (65)
- mac安装java (72)
- reader.onload (61)
- outofmemoryerror是什么意思 (64)
- flask文件上传 (63)
- eacces (67)
- 查看mysql是否启动 (70)
- java是值传递还是引用传递 (58)
- 无效的列索引 (74)