一,背景
实现一个go web框架,是我从java转go的第一个项目,当时团队人手紧缺,业务线原有的java项目重构需求非常紧急,对于为这些项目提供支撑的web框架,给的开发时间更是少得可怜,更让人抓狂的是,这么紧急的框架开发任务,人员配置就我一个,哈哈哈哈。 当时,看了下httprouter的源码,还没来得及学习下gin beego等知名web框架,就直接上手干了。
现在回想起来,虽然该有的功能都有了,但是从细节设计和代码结构上看,还有不少可以优化的地方,并且当时很多涉及到的知识点还没有来得及由点到面的展开,也就没有来得及应用到项目中去。所以决定从零开始,再写一个web框架,同时把一些比较值得记录的地方,以文字的形式做一个输出和总结,希望能够对看到这篇文章的你起到一定的帮助。
1,当时为什么不用现有的gin
、beego
、kit
、echo
和gorilla
等web
框架,而是要重复造轮子?
- 第一,也是最重要的原因–练手。了解一个领域/知识点,最好的办法就是实现它,当时刚从java转go,急需一个项目练手。
- 第二,拿web框架作为练手项目,除了需要扎实的基础之外,难度方面,不高不低,非常合适。
- 第三,团队内部关于现有轮子的争论无法达成一致,最终决定自己开发满足定制化需求的轻量级框架。
- 第四,有plan B,也就是兜底方案。如果开发失败,就直接上现有的轮子,对于业务线,几乎没有切换成本。
2,哪个框架更好?
开源框架很多,不知道应该学习选择什么框架。相信大多数gopher都有遇到过这种困扰。 我的建议是不要被框架绑架,搞清楚事情的本质,才是解决问题的王道。 现在技术更新是日新月异的,框架更是如此,隔一段时间换一个,不同的公司很大可能采用不同的框架,所以你换新工作之后,一般都得重新学习一门新框架,如果新公司用的框架你不会,那如何做到快速上手将会是一个比学习框架更大的挑战。
其实这些表面现象的本质只有一个,那就是:框架只是一个工具而已!不止框架是工具,我们的编程语言也可以是工具。所以框架可以一个个的换,编程语言也可以,现在很多服务已经使用 Go 语言替代 Java、Python 了。
那么基于这个本质,我该怎么做呢?
- 打牢基础。编程语言本身的功能、概念、语法、模式等,它是编程的地基,不会随着框架的改变而改变。
- 精通原理。就拿web框架来说,主要负责的就是三件事:接收请求、找到用户处理逻辑调用一下、再把响应写回去。简单点就是:接受请求、分发请求、写回响应。同一类框架,底层原理都是差不多的,区别主要表现在实现方式、用户体验(开发者是用户)、周边生态等方面。
- 多实践。实践是检验真理的唯一标准,积极参与公司新项目,多向大佬学习。
3,名字less
的由来?
less is more
4,理解less
,需要了解哪些前置知识?
4.1,认真读懂net/http
标准库,其中大致的逻辑如下:
- 标准库创建 HTTP 服务是通过创建一个 Server 数据结构完成的
- Server 数据结构在 for 循环中不断监听每一个连接
- 每个连接默认开启一个 Goroutine 为其服务
- serverHandler 结构代表请求对应的处理逻辑,并且通过这个结构进行具体业务逻辑处理
- Server 数据结构如果没有设置处理函数 Handler,默认使用DefaultServerMux 处理请求
- DefaultServerMux 是使用 map 结构来存储和查找路由规则
很显然,聪明而又熟悉
RESTful
的你肯定已经发现,最后一步”DefaultServerMux 是使用 map 结构来存储和查找路由规则”是一个可优化的点,这也是众多go web框架需要自己实现的一个地方,当然less
也不例外。
4.2,理解Context
的设计思路,在less
中,我们将设计一个实现Context
接口的custom context
,并将被应用于以下方面:
- 日常开发中我们大概率会遇到超时控制的场景,比如一个批量耗时任务、网络请求等;一个良好的超时控制可以有效的避免一些问题(比如 goroutine 泄露、资源不释放等)
- 一个请求,从接受到返回过程中的信息传递和共享
- 超时控制groutine和业务逻辑groutine并发写
ResponseWriter
问题的处理 - 超时控制groutine,触发超时,写入
ResponseWriter
超时响应之后,业务逻辑groutine重复写ResponseWriter
的问题处理
Context设计思想及使用实践,可以参考我的这篇Context设计思想及使用实践
4.3,熟悉http
协议,推荐阅读《HTTP权威指南》,如果时间充裕还可以读一下《TCP/IP详解》系列。
4.4,熟悉http.ResponseWriter
和*http.Request
这两大信息载体的结构和方法,不然的话,连从哪里读取query values
和 form data
以及往哪里写response
都不知道,那就尴尬了。
4.5,熟悉设计模式。
巧妙而又优雅,几乎是所有架构设计的基本准则,less
也不例外,如果你来不及认真研读23种设计模式,可以认真阅读我的这篇面向对象设计六大原则,如果能够做到融会贯通,那么由此衍化而来的设计模式,你也可以做到信手拈来。
4.6,one more thing
为了降低less框架的阅读难度,建议先看懂这个示例级别的小框架, 源码在此。
为什么要强调上面链接里的框架是示例级别?原因在于,这个示例框架只能用于个人开发,不能用于生产,而less的目标是打造成一个生产级别的框架。
那么生产级别和示例级别有哪些明显的差异呢?
答:细节和生态
具体体现在一下几点:
- 核心模块。服务启动方式、路由分发机制、context封装性和中间件机制等的设计
- 功能完备性。日志模块、命令行工具和缓存机制等功能是否提供
- 框架扩展性。是否支持扩展,扩展方式是否友好
- 框架性能。qps等性能
- 文档/社区。
很显然,示例级别的框架,并不满足以上几点。除此之外,一个人完成完美契合以上几点的生产级别的框架也是不可能的,所以需要学会站在巨人的肩膀上思考,向巨人借力,因此,less在关键代码的设计上,会借鉴已经经过千锤百炼、反复迭代升级过的gin的做法,只有这样,才能做出功能完备、性能优良的生产级框架。
4.7,Last but not least – 了解包括但不限于以下开源库,这些开源库将会被引用到less
- cobra
- cast (这两个库的作者是google的大佬Steve Francia)
- survey
- go-daemon
- go-git
- httprouter
- gin-gonic/contrib
- uuid
- go-github
- swag
- gspt
- mapstructure
二,重要结构(web框架三大件:context,router,middleware)
框架中的某个模块,比如说路由,实现的方法不止一种,每一个模块要实现的功能也各有不同,所以用哪一种方法来实现,以及要实现哪些功能,都是一种选择。 而每种选择的背后,其实都是方向问题,因为这些选择共同构成了一个框架的倾向性,也就是设计感。设计感非常考验开发者的综合技能和项目经验,也是考核框架质量的一个重要维度。
核心模块
一个框架最核心的模块:context,router,middleware,它们的设计感会影响到整个框架的性能和表现,也最能直接体现出框架作者的开发水平。
1.context
关于Context设计思想及使用实践,我在这片文章里已经讲得很详细了。在less框架里,我们主要要了解的是context的应用和自定义context的最佳实践。less框架里的自定义context结构如下:
1 |
|
2.router
首选,对于一个web框架来说,router必须要满足以下需求:
- http method 匹配
- 静态路由匹配
- 路由批量通用前缀
- 动态路由匹配
所以在路由存储结构的设计选型上,需要花些时间去思考的,毕竟该结构除了要满足上述需求外,还要有比较高的查找效率。常见的数据结构:
数据结构 | 查找时间复杂度 | 是否满足路由匹配4点要求 |
---|---|---|
数据组 | O(n) | 满足,匹配都需要遍历,时间复杂度O(n) |
哈希表 | O(1) | 满足,动态路由匹配需要遍历,时间复杂度O(n) |
队列 | O(n) | 满足,匹配都需要遍历,时间复杂度O(n) |
栈 | O(n) | 满足,匹配都需要遍历,时间复杂度O(n) |
链表 | O(n) | 满足,匹配都需要遍历,时间复杂度O(n) |
堆 | O(n) | 满足,匹配都需要遍历,时间复杂度O(n) |
图 | O(n^2) | 满足,匹配都需要遍历,时间复杂度O(n) |
树 | O(logn) | 满足,匹配不需要遍历 |
所以,从对比可以得出我们的选型结果,那就是–树。了解树这个数据结构的人都应该知道,树的查找时间复杂度和树高是负相关的,树高越高,查找的效率越低,当树的左右子树极度不平衡的时候,就会退化为链表,时间复杂度为O(n)。所以,还需要对树的结构和节点上存储的数据做一些设计和优化,来提高查找性能。我们可以参考下gin所采取的方案–httprouter。我的这篇博客httprouter源码解析分析了httprouter的源码,可以移步查阅,在此就不做重复叙述了。
我们的less也会采用httprouter作为路由管理的解决方案。
1 | type methodTrees []methodTree |
设计思想
1,一切皆服务
理解这种设计,需要对面向对象、面向接口有一定的了解,同时,如果真正应用的业务开发中,又需要你对DDD能够做到融会贯通。
在less框架里面,我们几乎所有的模块(配置、缓存、日志和web服务等)都会被设计为一种服务,包括使用方,在做业务开发的时候,我们也建议按功能把业务划分为不同的服务来设计,然后把这些服务注册到我们框架定义的服务容器中就可以。 就像堆积积木,只要想好了一个服务的接口,我们逐步实现服务之后,这一个服务就是一块积木,之后可以用相同的思路实现各种服务的积木块,用它们来拼出我们需要的业务逻辑。这就是“一切皆服务”思想带来的便利。
框架方面:
1 | // NewInstance 定义了如何创建一个新实例,所有服务容器的创建服务 |
日志provider,实现provider接口提供各种log service:
1 | // LogServiceProvider 服务提供者 |
业务方面,实现provider接口提供各种业务service:
1 | type DemoProvider struct { |
2,服务容器
设计详见注释
1 | // Container 是一个服务容器,提供绑定服务和获取服务的功能 |
3,命令工具
less引入著名的cobra库来实现扩展命令行功能,Cron、CronSpecs和container使我们新加入的定制化功能。
1 | type Command struct { |
得益于一切皆服务的设计,在less中,结合命令工具,你可以通过命令操作一切服务,比如:启动、停止、查询和重启web。
4,自动化
don’t repeat yourself,把一切重复性的劳动自动化。
4.1,调试模式
调试模式下,监控文件修改,自动编译,自动运行。
4.2,兼容vue,前后端一体化
对于web框架来说,这一点并不是刚需,但是对于做偏运维/运营侧产品开发的同学来说,非常得必要。
4.3,发布自动化
这一点也不是刚需。有很多方式可以将一个服务进行自动化部署,比如现在比较流行的 Docker 化或者 CI/CD 流程。 但是一些比较个人比较小的项目,比如一个博客、一个官网网站,这些部署流程往往都太庞大了,更需要一个服务,能快速将在开发机器上写好、调试好的程序上传到目标服务器,并且更新应用程序。
4.4,其他一系列自动化工具
- 自动化创建服务工具
- 自动化创建命令行工具
- 自动化gin中间件迁移工具
- 自动化less脚手架工具
代码
talk is cheap,直接看代码吧,注释非常友好
quick start
1 | package main |
end
参考