百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 热门文章 > 正文

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

bigegpt 2024-08-24 23:10 2 浏览

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。

相关推荐

AI「自我复制」能力曝光,RepliBench警示:大模型正在学会伪造身份

科幻中AI自我复制失控场景,正成为现实世界严肃的研究课题。英国AISI推出RepliBench基准,分解并评估AI自主复制所需的四大核心能力。测试显示,当前AI尚不具备完全自主复制能力,但在获取资源...

【Python第三方库安装】介绍8种情况,这里最全看这里就够了!

**本图文作品主要解决CMD或pycharm终端下载安装第三方库可能出错的问题**本作品介绍了8种安装方法,这里最全的python第三方库安装教程,简单易上手,满满干货!希望大家能愉快地写代码,而不要...

pyvips,一个神奇的 Python 库!(pythonvip视频)

大家好,今天为大家分享一个神奇的Python库-pyvips。在图像处理领域,高效和快速的图像处理工具对于开发者来说至关重要。pyvips是一个强大的Python库,基于libvips...

mac 安装tesseract、pytesseract以及简单使用

一.tesseract-OCR的介绍1.tesseract-OCR是一个开源的OCR引擎,能识别100多种语言,专门用于对图片文字进行识别,并获取文本。但是它的缺点是对手写的识别能力比较差。2.用te...

实测o3/o4-mini:3分钟解决欧拉问题,OpenAI最强模型名副其实!

号称“OpenAI迄今为止最强模型”,o3/o4-mini真实能力究竟如何?就在发布后的几小时内,网友们的第一波实测已新鲜出炉。最强推理模型o3,即使遇上首位全职提示词工程师RileyGoodsid...

使用Python将图片转换为字符画并保存到文件

字符画(ASCIIArt)是将图片转换为由字符组成的艺术作品。利用Python,我们可以轻松实现图片转字符画的功能。本教程将带你一步步实现这个功能,并详细解释每一步的代码和实现原理。环境准备首先,你...

5分钟-python包管理器pip安装(python pip安装包)

pip是一个现代的,通用、普遍的Python包管理工具。提供了对Python包的查找、下载、安装、卸载的功能,是Python开发的基础。第一步:PC端打开网址:选择gz后缀的文件下载第二步:...

网络问题快速排查,你也能当好自己家的网络攻城狮

前面写了一篇关于网络基础和常见故障排查的,只列举了工具。没具体排查方式。这篇重点把几个常用工具的组合讲解一下。先有请今天的主角:nslookup及dig,traceroute,httping,teln...

终于把TCP/IP 协议讲的明明白白了,再也不怕被问三次握手了

文:涤生_Woo下周就开始和大家成体系的讲hadoop了,里面的每一个模块的技术细节我都会涉及到,希望大家会喜欢。当然了你也可以评论或者留言自己喜欢的技术,还是那句话,希望咱们一起进步。今天周五,讲讲...

记一次工控触摸屏故障的处理(工控触摸屏维修)

先说明一下,虽然我是自动化专业毕业,但已经很多年不从事现场一线的工控工作了。但自己在单位做的工作也牵涉到信息化与自动化的整合,所以平时也略有关注。上一周一个朋友接到一个活,一家光伏企业用于启动机组的触...

19、90秒快速“读懂”路由、交换命令行基础

命令行视图VRP分层的命令结构定义了很多命令行视图,每条命令只能在特定的视图中执行。本例介绍了常见的命令行视图。每个命令都注册在一个或多个命令视图下,用户只有先进入这个命令所在的视图,才能运行相应的命...

摄像头没图像的几个检查方法(摄像头没图像怎么修复)

背景描述:安防监控项目上,用户的摄像头运行了一段时间有部分摄像头不能进行预览,需要针对不能预览的摄像头进行排查,下面列出几个常见的排查方法。问题解决:一般情况为网络、供电、设备配置等情况。一,网络检查...

小谈:必需脂肪酸(必需脂肪酸主要包括)

必需脂肪酸是指机体生命活动必不可少,但机体自身又不能合成,必需由食物供给的多不饱和脂肪酸(PUFA)。必需脂肪酸主要包括两种,一种是ω-3系列的α-亚麻酸(18:3),一种是ω-6系列的亚油酸(18:...

期刊推荐:15本sci四区易发表的机械类期刊

  虽然,Sci四区期刊相比收录在sci一区、二区、三区的期刊来说要求不是那么高,投稿起来也相对容易一些。但,sci四区所收录的期刊中每本期刊的投稿难易程度也是不一样的。为方便大家投稿,本文给大家推荐...

be sick of 用法考察(be in lack of的用法)

besick表示病了,做谓语.本身是形容词,有多种意思.最通常的是:生病,恶心,呕吐,不适,晕,厌烦,无法忍受asickchild生病的孩子Hermother'sverysi...