七叶笔记 » golang编程 » Golang实例:从零构建一个HTTP路由器

Golang实例:从零构建一个HTTP路由器

HTTP路由器)负责侦听HTTP请求并根据匹配条件(例如HTTP方法或URL)调用适当的处理程序。

Golang提供了一个非常简单的路由器ServeMux。但它太基础简单,所以大家一般都会选择第三方路由模块,比如 go rilla/mux。

今天我们来学习下如何从零自己构建一个HTTP路由。

概述

一个HTTP路由器主要负责以下几件事:

404处理程序:为不匹配的请求提供404响应

匹配:匹配URL路径和HTTP方法并调用路由处理程序

参数:提取动态网址参数,例如/users/(?P<id>\d+)

紧急恢复:赶上紧急情况并回复500

下面是一个代码片段,展示了上述的所有功能:

 r := New Route r()
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。

相关文章