Golang Web入门(2):如何实现一个RESTful风格的路由

摘要

在上一篇文章中,我们聊了聊在Golang中怎么实现一个Http服务器。但是在最后我们可以发现,固然DefaultServeMux可以做路由分发的功能,但是他的功能同样是不完善的。

由DefaultServeMux做路由分发,是不能实现RESTful风格的API的,我们没有办法定义请求所需的方法,也没有办法在API路径中加入query参数。其次,我们也希望可以让路由查找的效率更高。

所以在这篇文章中,我们将分析httprouter这个包,从源码的层面研究他是如何实现我们上面提到的那些功能。并且,对于这个包中最重要的前缀树,本文将以图文结合的方式来解释。

1 使用

我们同样以怎么使用作为开始,自顶向下的去研究httprouter。我们先来看看官方文档中的小例子:

package mainimport ("fmt""net/http""log""github.com/julienschmidt/httprouter"
)func Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {fmt.Fprint(w, "Welcome!\n")
}func Hello(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("/", Index)router.GET("/hello/:name", Hello)log.Fatal(http.ListenAndServe(":8080", router))
}

其实我们可以发现,这里的做法和使用Golang自带的net/http包的做法是差不多的。都是先注册相应的URI和函数,换一句话来说就是将路由和处理器相匹配。

在注册的时候,使用router.XXX方法,来注册相对应的方法,比如GET,POST等等。

注册完之后,使用http.ListenAndServe开始监听。

至于为什么,我们会在后面的章节详细介绍,现在只需要先了解做法即可。

2 创建

我们先来看看第一行代码,我们定义并声明了一个Router。下面来看看这个Router的结构,这里把与本文无关的其他属性省略:

type Router struct {//这是前缀树,记录了相应的路由trees map[string]*node//记录了参数的最大数目maxParams  uint16}

在创建了这个Router的结构后,我们就使用router.XXX方法来注册路由了。继续看看路由是怎么注册的:

func (r *Router) GET(path string, handle Handle) {r.Handle(http.MethodGet, path, handle)
}func (r *Router) POST(path string, handle Handle) {r.Handle(http.MethodPost, path, handle)
}...

在这里还有一长串的方法,他们都是一样的,调用了

r.Handle(http.MethodPost, path, handle)

这个方法。我们再来看看:

func (r *Router) Handle(method, path string, handle Handle) {...if r.trees == nil {r.trees = make(map[string]*node)}root := r.trees[method]if root == nil {root = new(node)r.trees[method] = rootr.globalAllowed = r.allowed("*", "")}root.addRoute(path, handle)...
}

在这个方法里,同样省略了很多细节。我们只关注一下与本文有关的。我们可以看到,在这个方法中,如果tree还没有初始化,则先初始化这颗前缀树。

然后我们注意到,这颗树是一个map结构。也就是说,一个方法,对应了一颗树。然后,对应这棵树,调用addRoute方法,把URI和对应的Handle保存进去。

3 前缀树

3.1 定义

又称单词查找树,Trie树,是一种树形结构,是一种哈希树的变种。典型应用是用于统计,排序和保存大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。它的优点是:利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较,查询效率比哈希树高。

简单的来讲,就是要查找什么,只要跟着这棵树的某一条路径找,就可以找得到。

比如在搜索引擎中,你输入了一个杨:

他会有这些联想,也可以理解为是一个前缀树。

再举个例子:

在这颗GET方法的前缀树中,包含了以下的路由:

  • /wow/awesome
  • /test
  • /hello/world
  • /hello/china
  • /hello/chinese

说到这里你应该可以理解了,在构建这棵树的过程中,任何两个节点,只要有了相同的前缀,相同的部分就会被合并成一个节点。

3.2 图解构建

上面说的addRoute方法,就是这颗前缀树的插入方法。假设现在数为空,在这里我打算以图解的方式来说明这棵树的构建。

假设我们需要插入的三个路由分别为:

  • /hello/world
  • /hello/china
  • /hello/chinese

(1)插入/hello/world

因为此时树为空,所以可以直接插入:

(2)插入/hello/china

此时,发现/hello/world和/hello/china有相同的前缀/hello/。

那么要先将原来的/hello/world结点,拆分出来,然后将要插入的结点/hello/china,截去相同部分,作为/hello/world的子节点。

(3)插入/hello/chinese

此时,我们需要插入/hello/chinese,但是发现,/hello/chinese和结点/hello/有公共的前缀/hello/,所以我们去查看/hello/这个结点的子节点。

注意,在结点中有一个属性,叫indices。它记录了这个结点的子节点的首字母,便于我们查找。比如这个/hello/结点,他的indices值为wc。而我们要插入的结点是/hello/chinese,除去公共前缀后,chinese的第一个字母也是c,所以我们进入china这个结点。

这时,有没有发现,情况回到了我们一开始插入/hello/china时候的局面。那个时候公共前缀是/hello/,现在的公共前缀是chin。

所以,我们同样把chin截出来,作为一个结点,将a作为这个结点的子节点。并且,同样把ese也作为子节点。

3.3 总结构建算法

到这里,构建就已经结束了。我们来总结一下算法。

具体带注释的代码将在本文最末尾给出,如果想要了解的更深可以自行查看。在这里先理解这个过程:

(1)如果树为空,则直接插入
(2)否则,查找当前的结点是否与要插入的URI有公共前缀 (3)如果没有公共前缀,则直接插入 (4)如果有公共前缀,则判断是否需要分裂当前的结点
(5)如果需要分裂,则将公共部分作为父节点,其余的作为子节点
(6)如果不需要分裂,则寻找有无前缀相同的子节点
(7)如果有前缀相同的,则跳到(4)
(8)如果没有前缀相同的,直接插入
(9)在最后的结点,放入这条路由对应的Handle

但是到了这里,有同学要问了:怎么这里的路由,不带参数的呀?

其实只要你理解了上面的过程,带参数也是一样的。逻辑是这样的:在每次插入之前,会扫描当前要插入的结点的path是否带有参数(即扫描有没有/或者*)。如果带有参数的话,将当前结点的wildChild属性设置为true,然后将参数部分,设置为一个新的子节点。

4 监听

在讲完了路由的注册,我们来聊聊路由的监听。

在上一篇文章的内容中,我们有提到这个:

type serverHandler struct {srv *Server
}func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {handler := sh.srv.Handlerif handler == nil {handler = DefaultServeMux}if req.RequestURI == "*" && req.Method == "OPTIONS" {handler = globalOptionsHandler{}}handler.ServeHTTP(rw, req)
}

当时我们提到,如果我们不传入任何的Handle方法,Golang将使用默认的DefaultServeMux方法来处理请求。而现在我们传入了router,所以将会使用router来处理请求。

因此,router也是实现了ServeHTTP方法的。我们来看看(同样省略了一些步骤):

func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {...path := req.URL.Pathif root := r.trees[req.Method]; root != nil {if handle, ps, tsr := root.getValue(path, r.getParams); handle != nil {if ps != nil {handle(w, req, *ps)r.putParams(ps)} else {handle(w, req, nil)}return} }...// Handle 404if r.NotFound != nil {r.NotFound.ServeHTTP(w, req)} else {http.NotFound(w, req)}
}

在这里,我们选择请求方法所对应的前缀树,调用了getValue方法。

简单解释一下这个方法:在这个方法中会不断的去匹配当前路径与结点中的path,直到找到最后找到这个路由对应的Handle方法。

注意,在这期间,如果路由是RESTful风格的,在路由中含有参数,将会被保存在Param中,这里的Param结构如下:

type Param struct {Key   stringValue string
}

如果未找到相对应的路由,则调用后面的404方法。

5 处理

到了这一步,其实和以前的内容几乎一样了。

在获取了该路由对应的Handle之后,调用这个函数。

唯一和之前使用net/http包中的Handler不一样的是,这里的Handle,封装了从API中获取的参数。

type Handle func(http.ResponseWriter, *http.Request, Params)

Golang Web入门(2):如何实现一个RESTful风格的路由相关推荐

  1. post方法就反回了一个string字符串前台怎么接_Golang Web入门(2):如何实现一个RESTful风格的路由...

    摘要 在上一篇文章中,我们聊了聊在Golang中怎么实现一个Http服务器.但是在最后我们可以发现,固然DefaultServeMux可以做路由分发的功能,但是他的功能同样是不完善的. 由Defaul ...

  2. Golang Web入门(3):如何优雅的设计中间件

    Golang Web入门(3):如何优雅的设计中间件 摘要 我们上篇文章已经可以实现一个性能较高,且支持RESTful风格的路由了.但是,在Web应用的开发中,我们还需要一些可以被扩展的功能. 因此, ...

  3. Golang Web入门(4):如何设计API

    Golang Web入门(4):如何设计API 摘要 在之前的几篇文章中,我们从如何实现最简单的HTTP服务器,到如何对路由进行改进,到如何增加中间件.总的来讲,我们已经把Web服务器相关的内容大概梳 ...

  4. 在 Docker 上运行一个 RESTful 风格的微服务

    tags: Microservice Restful Docker Author: Andy Ai Weibo: NinetyH GitHub: https://github.com/aiyanbo/ ...

  5. 快速搭建一个restful风格的springboot项目

    1.创建一个工程. 2.引入pom.xml依赖,如下 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi ...

  6. nodejs入门学习笔记一——一个完整的http路由服务实现

    开始学习nodejs! 参考书籍:The Node Beginner Book ,所有问题和讨论都围绕本书. 1.学习nodejs需要具备的基础知识: js基本语法,基本上写过前端的都能满足,原生js ...

  7. Web设计主题:创建一个古典风格的网站

    如何进行古典风格的设计?主要针对酒店行业和联邦政府进行Web开发的Ryan Boudreaux针对该话题发表了文章<Web design themes: Create a vintage loo ...

  8. Golang web filter 轻量级实现

    前言 golang web 通过http handle模块进行restful接口与请求处理绑定:既然用了restful每个公司或项目都会制定自己的设计原则和约束条件.在日常开发中通常会根据uri匹配规 ...

  9. 一个REST风格的URI设计方案[Blog Web Services]

    拿Blog Web Services为例,一个REST风格的URI设计,可行乎? /blog get,列表,分页表示/blog?pi={pageindex} 博客分类 /blog/category g ...

最新文章

  1. Vue_(组件通讯)动态组件结合keep-alive
  2. linux检测u盘容量,Ubuntu18.04使用f3probe检测U盘实际容量
  3. C语言实例第3期:在控制台打印出著名的杨辉三角
  4. 帆软日期格式转换_时间转换为年月日
  5. Navicat导出表结构
  6. Oracle 11g中创建实例
  7. AcWing 1303. 斐波那契前 n 项和
  8. 用RAII技术管理资源及其泛型实现
  9. (王道408考研操作系统)第二章进程管理-第四节1:死锁相关概念
  10. java基础将一个int数组转换成一个字符串
  11. js小数运算出现多为小数问题_js小数计算小数点后显示多位小数的实现方法
  12. grub 与grub2
  13. Lua中handler方法的使用(亲测版)
  14. Linux内核子系统---内存管理子系统、进程管理子系统
  15. 【亲测有效】Ubuntu系统开机速度慢解决办法
  16. 浅议化学与社会的关系——兼议绿色化学重要性
  17. python金融量化风险_利用 Python 进行量化投资分析 - 利率及风险资产的超额收益...
  18. 关于软件产品化的几点思考【转】
  19. 调css p 段落间距,CSS段落第一个文字空两格缩进text-indent 和 文字之间间距调整letter-spacing...
  20. 你知道如何使用Java将DWG / DXF CAD文件转换为图像格式吗?

热门文章

  1. Python学习笔记之函数(一)
  2. JAVA大厂高频面试题及答案
  3. java 反射 获取方法列表_Java 反射获取类详细信息的常用方法汇总
  4. Markdown入门指导
  5. React的Element的创建和render
  6. 《跟菜鸟学Cisco UC部署实战》-上线了(线下培训班开班,见百度云)
  7. array数组的若干操作
  8. 炼数成金hadoop视频干货03
  9. syslog-ng记录history日志
  10. 让数据库支持SQL 2005 CLR 的必要条件