Gin中的路由采坑及接口路由定义规范
写在前面
最近使用Gin开发Api接口,在路由注册时遇到了棘手的路由冲突的问题。和公司的少年们讨论了一下,发现Gin的路由其实是一颗基数树(Radix Tree),于是花时间探索了一下,了解这棵树以更好地利用这棵树。
适应人群
入门√——初级——中级——高级;本文适应入门及以上。
Gin路由中的基数树
《算法导论》中的一道题目(二叉查找树-思考题)
给定两个串a = a0a1……ap和b = b0b1……b1,其中每一个ai和每一个bj都属于某个有序字符集,如果下面两条规则之一成立,则说串a按字典序小于串b:
- 存在一个整数j,0<=j<=min(p,q),使得ai=bi,i=0,1,……,j-1,且aj<bj;
- p<q,且ai=bi,对所有的i=0,1,……,p成立。
这与字典中的排序很相似。例如,对于here和hero这两个字符串,根据规则①(设j=3),我们可以得出here排在hero前面;而对于hero和heroine这两个字符串,根据规则②,我们可以得出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/
后面追加hot
与a/b/c
时,均会与:post
这个变量的路由冲突。
由于在RESTFul接口的实现中,路由中存在大量的变量,因此有非常大的概率会造成路由冲突。
Gin路由定义的最佳实践
知道了基数树的局限性,可以总结出在使用Gin定义路由时最佳实践的两个点:
接口确定性的前缀尽可能全
从经验来看,项目伊始就要规划好。推荐前缀中①指明是否为接口路由,②指明版本号,③指明模块或项目(代号)。
比如/api/v1/admin
表示后台管理需要的接口;比如/api/v1/naza
表示开发代号为哪吒其相关的接口。
局部RESTFul接口定义
不否认RESTFul架构的表意清晰的特点,为了方便对接,可以在局部使用RESTFul的接口定义风格。比如
/api/v1/naza/activities/rules
获取活动条款列表/api/v1/naza/activity/:id/members
获取某活动所有成员/api/v1/naza/activity/:id/member/:id
或许某活动中某成员信息
小结
Gin基数树的路由,效率非常高(从数据结构来看),不过其也面临一些局限性。在设计Api时,需要谨慎避免路由冲突。文章最后给出的两个Gin路由定义的最佳实践,其实是框架无关的规则,只要是定义Api便建议去参考的。
使用Gin定义路由,没有规范时,遇到路由冲突心里很慌;理清楚了其中的道理,制定好定义路由的规范,再遇到类似的问题心态上就稳多了 ^_^~