作为一名web开发者,笔者使用Golang作为后端语言在开发Web服务的时候遇到过很多问题,
- Golang作为静态语言,在业务逻辑开发速度方面,相较于Ruby等动态语言并不占有太大优势。使用Golang开发Web系统难免会使得代码可读性,代码整洁不如Ruby等语言来的直观,简洁。但是当我们面对海量用户请求的时候,相同的硬件配置下,使用golang会让我们开发的服务的性能更加好。
- 相较于Ruby语言中的Rails框架,Golang生态里面的Web开发框架并没有集大成者,把Routing,ORM等帮我们处理好。也就没有Rails生态圈中best practice.很多时候,使用Golang开发,意味着我们需要自己写一些粘合代码,让Golang生态中的优秀第三方框架可以各司其职,协同工作。
- 相较于Rails生态圈,Golang提供的单元测试工具真可谓简陋至极。
grafana 作为一个典型的Web系统,为我们使用Golang开发web系统提供了良好的范例。本文旨在通过解读grafana 在Http service 方面的代码,帮助Web开发者“写一些粘合代码”。
首先,我们先看看granfana HTTP service 所使用的第三方开源框架。
- https://github.com/go-macaron/macaron https://go-macaron.com/
- https://github.com/go-macaron/binding
- github.com/go-xorm/xorm http://xorm.io/
- github.com/go-xorm/builder
- https://github.com/hashicorp/go-hclog
- github.com/inconshreveable/log15
- github.com/opentracing/opentracing-go
- github.com/patrickmn/go-cache
- github.com/smartystreets/goconvey
- github.com/smartystreets/assertions
1. 启动流程
http server 包含http service所有需要的基础组件。
granfana 启动的时候,会通过grafana 的registry机制自动注册http server. HTTPServer实现了registry中定义的Service interface,所以启动granfana的时候,会调用 HTTPServer 的Init 方法。Init方法主要初始化HTTPServer struct定义的log以及cache组件。 HTTPServer 实现了registry中定义的BackgroundService方法,所以启动granfana的时候,会调用HTTPServer 的Run方法。Run方法会创建Macaron实例,注册路由,监听http端口,并且处理shutdown逻辑。至此,http server启动完毕。
2. log
grafana 采用了github.com/inconshreveable/log15第三方log组件写log,并且做了简单的封装。主要针对config文件中logging相关的配置写了一些逻辑代码。有一点是和大部分系统不一样的, 在grafana中,不同的组件拥有不同的log对象,比如HTTPServer有自己的log对象,xorm(grafana 采用的ORM框架)也有自己的log对象。这些log对象产生的log会合成一个log,输出到stdout, log file。
3. grafana采用Macaron 作为Go Web 框架
作为一款具有高生产力和模块化设计的web框架, Macaron提供了功能丰富的中间件模块,能满足基本Web服务的需求。需要Macaron解决的第一个问题是web系统的routing问题,grafana针对Macaron的routing做了适当的封装。
首先pkg/api/route_register.go 文件中定义了注册路由的逻辑。grafana把处理request分成namedmiddleware, subfixHandlers,和handler三部分。其中namedmiddleware是一种特殊的middleware,在grafana中, 通过github.com/facebookgo/inject设置了两个namedmiddleware, 分别为ReqeustMetrics 和 RequestTracing.
subfixHandler则是应用于Group类型路由的一类handler。 最后的handler(s)才是处理request核心逻辑的函数。route_register.go 定义了注册路由的逻辑, 而pkg/api/api.go则主要定义了具体的路由。我们以api.go中的admin api为例解释这套逻辑。 reqGrafanaAdmin函数是说凡是访问/api/admin/*的request都必须要具有admin role。AdminGetSetting 是处理核心业务的函数,它接收一个ReqContext对象作为参数, ReqContext是grafana基于macaron.Context定义的request对象。 其中bind(dtos.AdminCreateUserFrom{})以及wrap(…)等也是handler,只是使用了一些bind 以及wrap方法做了一些处理。通过这种分门别类的路由, granfana减少了重复代码的产生,而且代码更简洁,易懂。
4. bus机制
在pkg/bus/bus.go中,定义了bus机制。bus主要利用供refect解决了接口依赖问题。在web系统中,我们往往把系统进行分层, 在grafana中, pkg/api package 属于接入层, pkg/api层会调用service层提供的业务逻辑代码。在api layer,我们为了能够调用service提供的函数并且方便些Unit test,传统的做法是在service提供的业务逻辑函数抽象出interface,这种做法虽然可行,但是很啰嗦。而且在写unit test的时候,会更加的啰嗦。 grafana中,提供bus机制来解决这个问题。首先,系统启动或者运行时, 可以通过bus.AddHandler函数注册HandlerFunc处理函数到globalBus对象, 然后在api层调用bus.Dispatch的时候,我们会提供一个strcut 对象作为msg,Dispatch会调用注册的HandlerFunc函数处理strcut对象,并且将处理结果写入到strcut对象中。 我们以api/admin/stats 为例,在AdminGetStats handler函数中,首先生成GetAdminStatsQuery struct 实例,然后利用bus.Dispatch 方法将query dispatch出去。最后由init函数注册的GetAdminStats函数处理,并且更新GetAdminStatsQuery strcut的Result的值。最后,AdminGetStats 会把statsQuery.Result以json形式返回给客户端。 这种机制在我们写unit test的时候mock service 函数非常实用,用起来也非常方便。
dispatch机制中,每个msg由一个HandlerFunc处理。 除了dispatch 模式外, bus还提供了Publish机制, 每个msg可以由多个handlerFunc处理。实现机制和dispatch类似,只是可以注册多个handlerFunc处理对应的event。(publish机制中,把msg成为event)。我们以signup 为例解释publish机制。在用户signup成功时, pkg/api/signup.go 会调用bus.Publish方法产生SignUpCompleted event。在用户注册完成后,我们往往会发送注册成功邮件,在grafana中,发送注册成功邮件是由NotificationService完成的。NotificationService 会监听SignUpCompleted event, 如果有SignUpCompleted event产生,NotificationService就会执行发送邮件的逻辑。
需要注意的是,不论dispatch还是publish机制, handlerFunc 都是同步完成的,dispatch函数会调用对应的handlerFunc, 然后执行handlerFunc, handlerFunc处理过程中,产生错误的话,错误会作为dispatch的返回值返回。在publish机制中, 一个event会被多个HandlerFunc处理。假设有三个HandlerFunc,分别为HandlerFunc1, HandlerFunc2, HandlerFunc3. bus.Publish会按照AddEventListener的顺序执行三个HandlerFunc, 假设HandlerFunc1执行成功, HandlerFunc2由于种种原因执行失败了,这个时候Publish就会返回,HandlerFunc3就不会被执行。 这一点和MQ产品中的异步机制是完全不同的。
5. Unit test
在golang的世界里面,相对于ruby等语言来说,写单元测试的确是一个繁琐无趣的活。一般来说,我们会通过golang的interface来mock一些方法。这种mock方法又衍生出两种写法。 比如,我们有一个待测方法。 func1, 里面有一个已经被测试过无需写单元测试的方法func2, 那么我们有两种方式来写单元测试。 方法1, 利用“type func”为func2抽象出一个接口interface_for_func2, 然后这个接口作为func1的一个参数。在运行的代码中,把func2传入func1作为参数。 在单元测试中,则用一个实现了interface_for_func2的mock方法传入func1.这样就实现了mock方法的目的。 方法2, 原理上和方法1是类似的,只是写法上有一些不同。 我们不在使用type func 抽象接口,而是为func2专门定义一个接口, 然后用一个struct来实现这些接口,在运行的代码中,使用实现了接口的struct来调用func2, 而在单元测试中,则mock一个实现了接口的mock的struct来调用mock的func2. 这两种方法在https://github.com/DATA-DOG/go-sqlmock和https://github.com/vektra/mockery都有体现。sqlmock的缺点是太过于负责,对鞋单元测试来说,需要构造大量的sql返回值。 mockery则需要生成大量mock的函数。
然而在grafana中, 使用了bus机制来解耦不通层面的代码。 以达到方便些单元测试的目的。 以下图里面的一个case为例,我们只需要使用bus.AddHandler就可以构造mock函数了。 不需要生成代码,简单方便。
本文仅仅解析了grafana中的http相关的部分的代码,还有很多代码没有touch到。以后再予以解析。