Gin中的路由采坑及接口路由定义规范

写在前面

最近使用Gin开发Api接口,在路由注册时遇到了棘手的路由冲突的问题。和公司的少年们讨论了一下,发现Gin的路由其实是一颗基数树(Radix Tree),于是花时间探索了一下,了解这棵树以更好地利用这棵树。

适应人群

入门√——初级——中级——高级;本文适应入门及以上。

Gin路由中的基数树

《算法导论》中的一道题目(二叉查找树-思考题)

给定两个串a = a0a1……ap和b = b0b1……b1,其中每一个ai和每一个bj都属于某个有序字符集,如果下面两条规则之一成立,则说串a按字典序小于串b:

  1. 存在一个整数j,0<=j<=min(p,q),使得ai=bi,i=0,1,……,j-1,且aj<bj;
  2. p<q,且ai=bi,对所有的i=0,1,……,p成立。

这与字典中的排序很相似。例如,对于herehero这两个字符串,根据规则①(设j=3),我们可以得出here排在hero前面;而对于heroheroine这两个字符串,根据规则②,我们可以得出hero排在heroine前面。

如果a和b是位串(二进制串),则根据规则①,有10100 < 10110;根据规则②,有10100 < 101000

上图示出的是基数树的数据结构,其中存储了位串1011、10、011、100和0。当查找某个关键字a = a0a1……ap时,在深度为i的一个结点处,若ai = 0则向左转;若ai = 1则向右转。设S为一组不同的二进制串构成的集合,各串的长度之和为n。说明如何利用基数树,在O(n)时间内对S按字典序排序。例如,对上图中每个结点的关键字,排序的输出应该是序列0、011、10、100、1011。

关于上面的题目,大家可以参阅参考文献。这里仅提一下,此为二叉查找树的一个应用,难点在于二叉树的创建(即插入元素),创建完成后采用前序遍历把树中的元素读出来即可。

Gin路由中的基数树

从上面的题目可以知道,基数树具有明确的构建规则,且搜索二叉树有非常高的检索效率。Gin路由的本质是一颗基数树,因此其效率有非常大的保障(在数据结构上至少如此)。

Gin的路由是由httprouter这个包实现的,因此这里以httprouter中的例子进行说明。

Priority   Path             Handle
9          \                *<1>
3          ├s               nil
2          |├earch\         *<2>
1          |└upport\        *<3>
2          ├blog\           *<4>
1          |    └:post      nil
1          |         └\     *<5>
2          ├about-us\       *<6>
1          |        └team\  *<7>
1          └contact\        *<8>

上面所展示的树中,Priority表示路由的优先级,Path表示路由路径,Handle表示路由所对应的响应函数,其中*<num>表示函数指针。如果对应到代码,可以认为上面的基数树是由下面的代码生成的:

r.GET("/", f1)
r.GET("/search", f2)
r.GET("/support", f3)
r.GET("/blog", f4)
r.GET("/blog/:post", f5)
r.GET("/about_us", f6)
r.GET("/about_us/team", f7)
r.GET("/contact", f8)

Gin基数树路由的局限性

在编写Api接口时,为了表意清晰方便调用,大都尽量遵循RESTFul的风格。不过很现实的情况是,一个系统在架构(尤其定义接口)时,无法严格按照RESTFul的方式设计,总存在特殊情况,其接口跳出RESTFul的风格而存在。在这种情况下,Gin的路由便表现出其局限性。

比如在已经注册了/blog/:post这条路由的情况下,将无法继续注册/blog/hot/blog/a/b/c这两类路由。因为/blog/:post这条规则,会把/blog/后的第一个元素(以“/”分割)当做:post变量来看待,因此当在/blog/后面追加hota/b/c时,均会与:post这个变量的路由冲突。

由于在RESTFul接口的实现中,路由中存在大量的变量,因此有非常大的概率会造成路由冲突。

Gin路由定义的最佳实践

知道了基数树的局限性,可以总结出在使用Gin定义路由时最佳实践的两个点:

接口确定性的前缀尽可能全

从经验来看,项目伊始就要规划好。推荐前缀中①指明是否为接口路由,②指明版本号,③指明模块或项目(代号)。 比如/api/v1/admin表示后台管理需要的接口;比如/api/v1/naza表示开发代号为哪吒其相关的接口。

局部RESTFul接口定义

不否认RESTFul架构的表意清晰的特点,为了方便对接,可以在局部使用RESTFul的接口定义风格。比如

小结

Gin基数树的路由,效率非常高(从数据结构来看),不过其也面临一些局限性。在设计Api时,需要谨慎避免路由冲突。文章最后给出的两个Gin路由定义的最佳实践,其实是框架无关的规则,只要是定义Api便建议去参考的。

使用Gin定义路由,没有规范时,遇到路由冲突心里很慌;理清楚了其中的道理,制定好定义路由的规范,再遇到类似的问题心态上就稳多了 ^_^~

参考