从hello-world开始go iris
iris框架是少数支持MVC的go web框架。在简单业务逻辑测试中,其性能超过了约大多数的go web框架。底层还是调用net/http包handleRequest。
iris_examples.mvc.hello-world代码剖析
基本功能实现:
- 使用了recover中间件和logger中间件。
- 添加了一个mvc控制器,处理"/" "/ping" "/hello",请求方法为GET
先上基本示例的代码:
package main
import (
"github.com/kataras/iris"
"github.com/kataras/iris/mvc"
"github.com/kataras/iris/middleware/logger"
"github.com/kataras/iris/middleware/recover"
)
func newApp() *iris.Application {
app := iris.New()
app.Use(recover.New())
app.Use(logger.New())
mvc.New(app).Handle(new(ExampleController))
return app
}
func main() {
app := newApp()
app.Run(iris.Addr(":8080"))
}
type ExampleController struct{}
func (c *ExampleController) Get() mvc.Result {
return mvc.Response{
ContentType: "text/html",
Text: "<h1>Welcome</h1>",
}
}
func (c *ExampleController) GetPing() string {
return "pong"
}
func (c *ExampleController) GetHello() interface{} {
return map[string]string{"message": "Hello Iris!"}
}
func (c *ExampleController) BeforeActivation(b mvc.BeforeActivation) {
anyMiddlewareHere := func(ctx iris.Context) {
ctx.Application().Logger().Warnf("Inside /custom_path")
ctx.Next()
}
b.Handle("GET", "/custom_path", "CustomHandlerWithoutFollowingTheNamingGuide", anyMiddlewareHere)
}
func (c *ExampleController) CustomHandlerWithoutFollowingTheNamingGuide() string {
return "hello from the custom handler without following the naming guide"
}
go iris处理请求的流程大致如下:
- 先初始化一个app实例。
- 加载中间件。
- 由中间件构建mvc实例。
- mvc实例Handle一个包含多个处理rest方法的controller。相当于处理java的一个Component。
- 调用app.Run()方法,构建app实例,启动服务端,阻塞等待客户端的连接。
app实例的初始化,包括了加载配置,路由的初始化,并且完成app.ContextPool的初始化。这个池底层由同步包*sync.Pool实现。
handlers处理流
iris里所说的中间件,类似于netty中所说的handlers。最基本形式上的不同,netty中的handler是包装在SimpleChannelInboundHandler类中,通过读写通道来完成请求的处理。而由于go在语言上支持闭包,iris中间件是一个,输入是一个请求上下文ctx的闭包。
一个最基本的服务端处理,无论使用何种框架,都至少需要完成:
- 拿到请求,解析请求。
- 逐层地用handler处理请求。
- 生成响应,写入到TCP的写缓存,并能过底层的tcp常连接(请求参数或是服务端设置成keepalive的)将其发送回客户端。
类似于不同的切面将拦截的请求信息,处理后转给下一层。iris每一层handler处理后转给下一个handler。作为服务端,所有的handler都保存在app实例的路由数据结构中。在接到待处理请求之后,会根据请求地址,装配成一套handlers序列,供handlerRequest()方法调用。handlers由
- 系统必须的handlers
- 每个路径请求对应的控制器中的控制方法。以及调用Handle(请求)时还可以再指定一个handler闭包。这里注意区别Handle(控制器),Handle(请求路径)是两个不同的方法,只是同名而已。
- 以及中间件handler的加载。
handlers处理顺序
这里就是有一个需要解释的问题,这些handlers是如何给定执行顺序的。请求被从标准net/http的(w,*r)寺装成context.Context之后,就会调用路由(h *routerHandler) HandleRequest(ctx context.Context) {}完成解析路径,和逐层调用ctx.Do(handlers)。所以handler的执行顺序,要定位到解析请求的是路由构建,而不是app的全局路由表构建。
此处不展开,后面再解释。回到原处,解释中间件的handlers之间是如何确定顺序,以及相对控制方法的扩行顺序的。中间件通过app.Use()注册到app.*APIBuilder.middleware。是一个[]context.Handler。逻辑很单一,就是将handler闭包,加到切片中。
完成中间件及控制器添加之后,handler顺序是在调用app.Run()内部app.Build()中构建路由时,确定request_path与相应有序handlers的。保存在app.Router.requestHandler.trees。 一个控制器的根路径集合包装成一个Nodes。对比java的spingmvc框架,RequestMpping注解,添加在控制器相当于根路径,添加到方法上,才是请求的子路径。这里iris保存数据结构的实现也体现了根路径、子路径的分类存入。解析时也会取两段路径拼接。
请求ctx.Do(handlers)按顺序执行各handler,每个handler闭包中,都会调用ctx.Next()调用后续handler。在ctx.Next()之前的代码段,相较于此handler后面所有handlers组成的集合,在此之前执行。放在ctx.Next()之后的代码段、以及defer func(),在此之后执行。
如GET /的路由:
java try...catch...exception与panic recover区别
本例加载了两个中间件,日志打印中间件,为似地可以扩展到服务监控中间件。recover中间件,处理请求中发生的异常。go语言中更多地使用err,而较少地使用panic recover机制。而相较于java,try...catch...exception机制随处可见。形式上发生panic之后,只会调用defer func()中的recover。理由是panic调用后,方法终止,并向上抛异常。但在返回之前会先执行defer func()。所以可以利用这个特性,执行恢复,还有就是要在defer中完成资源的关闭操作,如数据库、网络。
go 的panic机制执行完recover之后,将直接返回,并不继续执行panic()之后的逻辑!!!这与java是不同的。
标准库关于panic的解释如下:
The panic built-in function stops normal execution of the current goroutine. When a function F calls panic, normal execution of F stops immediately. Any functions whose execution was deferred by F are run in the usual way, and then F returns to its caller. To the caller G, the invocation of F then behaves like a call to panic, terminating G's execution and running any deferred functions. This continues until all functions in the executing goroutine have stopped, in reverse order. At that point, the program is terminated and the error condition is reported, including the value of the argument to panic. This termination sequence is called panicking and can be controlled by the built-in function recover.
所以具体到hello-world实例中,recover()是在ctx.Next()之后执行的。
日志中间件需要在控制器方法之后收集信息,并打印日志,所以与是在ctx.Next()之后执行。但该打印哪些日志的配置加载,可以在ctx.Next()之前执行。
如果日志中间件也可能panic(),则将recover.go放在logger.go之前。
RequestMapping
控制器中的各方法最终也会被注册到app的路由表中。后面会详细说明控制器的解析过程,是如何区分标准NamingGuide,重载方法Before/AfterActivation,以及自定义控制方法,进行分辨别类地注册的。
java的spingmvc框架,接爱处理一个请求连接。需要指定RequestMapping的请求路径。指定request_method。如果需要请求路径中的参数,要用@PathVariable指定,以及body cookie等。
go iris为简化编程,支持NamingGuide的方法命名,即RequestMethod+RequestMapping的形式,不需要单独为函数起名字,不一致也会降低代码阅读效率。后面会详细说明具体实现。返回值可以是字串,或其它interface{}。
自定义控制方法及加载流程
如果不想使用NamingGuide,就需要自己调用Handle()注册,至少需要指定httpMethod、path、handlerFunc、anyMiddleware闭包。这里handlerFunc只是方法名的字串,由框架反射调用具体函数并加载。而anyMiddleware是传入的可执行闭包。
注册逻辑,以及anyMiddleware加载要写在重写的BeforeActifvation。go语言在语法上是不支持重写的,应该是实现了ControllerActivator的BeforeActivation接口方法。
先上解析的源代码:
func (app *Application) Handle(controller interface{}) *Application {
c := newControllerActivator(app.Router, controller, app.Dependencies)
if before, ok := controller.(interface {
BeforeActivation(BeforeActivation)
}); ok {
before.BeforeActivation(c)
}
c.activate()
if after, okAfter := controller.(interface {
AfterActivation(AfterActivation)
}); okAfter {
after.AfterActivation(c)
}
return app
}
主要逻辑就是先构建ControllerActivator控制器的执行类,如果重写了BeforeActivation,就执行BeforeActivation的逻辑。本例中注册了一个自定议的控制器。之后再执行c.activate()对其它控制器方法作解析,并注册。
c.Handle太复杂了,后面详细说明。简单斜述,如下:
- 解析过程会调用c.Type.MethodByName(funcName)反射调用,加载方法。类似地Java中的Bean也需要通过反射加载。
- 逻辑实现了如何区分BeforeActivation NamingGuide CustomHandler走不同的逻辑流。
- c.handlerOf(m, funcDependencies)生成了handler闭包。
- 两次注册c.router.Handle() c.routes[funcName] = route。第二次注册,用哈希表key的不重复特性,避免同名方法重复注册。
最后整个框架可分为上下两层,上层处理请求封装,路由,cookie,session,认证等。下层是net/http TCP实现。而中间的连接结构,就是app的字段app.(Router),为router.Router类型。包括了锁、路由集合、包裹函数、构建好的context.ContextPool,以及app.(APIBuilder)。在后文中将做继教详述。
router.go中的Route源代码:
type Router struct {
mu sync.Mutex // for Downgrade, WrapRouter & BuildRouter,
// not indeed but we don't to risk its usage by third-parties.
requestHandler RequestHandler // build-accessible, can be changed to define a custom router or proxy, used on RefreshRouter too.
mainHandler http.HandlerFunc // init-accessible
wrapperFunc func(http.ResponseWriter, *http.Request, http.HandlerFunc)
cPool *context.Pool // used on RefreshRouter
routesProvider RoutesProvider
}