:framework

Go语言Web框架Tango中的中间件应用级别

Tango在创建之初就支持了全局中间件的支持,经过最近的改进,同时支持了Group级别和Route级别的中间件。下面我们就一起来看下这三种级别的中间件:

比如我们自定义了一个新的中间件:

func MyMiddleware() tango.HandlerFunc {
    return func(ctx *tango.Context) {
        ctx.Info("I'm a middleware")
        ctx.Next()
    }
}

全局中间件

全局中间件在任何一个请求调用时均会进行调用。用法如下:

t := tango.Classic()
t.Use(MyMiddleware())
t.Get("/", func() string {return "global"})

Group中间件

Group中间件在Group下匹配的路由被调用时会被调用。用法如下:

t := tango.Classic()
t.Group("/group", func(g *tango.Group) {
    g.Use(MyMiddleware())
    g.Get("/", func() string {return "group"})
})

Route中间件

Route中间件在该Route被调用时会被调用。如果有多个,会按照先后顺序调用。用法如下:

t := tango.Classic()
t.Get("/route", func() string {return "route"}, MyMiddleware())

中间件优先级

  • 全局中间件被先调用,Group中间件次之,最后是Route中间件
  • 相同级别的中间件,先加入的被先调用

Go语言Web框架Tango在Session中间件中的设计

Tango在创建之初的目标就是既有Beego的效率,又有martini的灵活中间件。因此Session也是在核心库 tango 之外作为官方中间件存在,地址是 tango-session

Session因为有着灵活的设计,因此可以完成很多我们希望的功能。首先看下Session中间件的选项:

type Options struct {
    MaxAge           time.Duration
    SessionIdName    string
    Store            Store
    Generator        IdGenerator
    Tracker          Tracker
    OnSessionNew     func(*Session)
    OnSessionRelease func(*Session)
}

其中我将要着重讲的是Store,Generator和Tracker。

Store

Store是一个接口,主要作用是存储Session中的内容,其定义如下:

type Store interface {
    Add(id Id) bool
    Exist(id Id) bool
    Clear(id Id) bool

    Get(id Id, key string) interface{}
    Set(id Id, key string, value interface{}) error
    Del(id Id, key string) bool

    SetMaxAge(maxAge time.Duration)
    SetIdMaxAge(id Id, maxAge time.Duration)

    Run() error
}

默认的内核自带了MemoryStore,这将会把所有Session内容保存在内存中。同时官方中间件中也提供了

这几种方式进行Session内容的存储。当然如果你愿意,也可以自己来实现一个Store。

Generator

Generator是一个接口,主要封装了SessionID的生成算法,其定义如下:

type IdGenerator interface {
    Gen(req *http.Request) Id
    IsValid(id Id) bool
}

默认的Generator是Sha1Generator,他是通过req.RemoteAddr,当前时间和随机字符串生成。 当然你也可以自定义更好的算法来生成SessionID。

Tracker

Tracker是一个接口,主要封装了Session的跟踪方式,其定义如下:

type Tracker interface {
    SetMaxAge(maxAge time.Duration)
    Get(req *http.Request) (Id, error)
    Set(req *http.Request, rw http.ResponseWriter, id Id)
    Clear(rw http.ResponseWriter)
}

默认的Tracker实现是CookieTracker,就是我们最常见的,将SessionID保存在cookie中,通过cookie来进行跟踪。Session中间件中也同时提供了HeaderTracker,支持将SessionID保存在自定义的Http Header中。当然你也可以自定义Tracker,比如通过URL参数来进行跟踪等等方式。

最后

看起来似乎很复杂,但是一般情况下都不需要去改变,你只需要

t := tango.Classic()
t.Use(session.New())
t.Run()

Tango v0.4版本发布,带来统一高性能的新路由

起子

自Tango发布之后大概有3个月了,受到了很多人的关注,也提了很多的改进意见。我自己也通过不断 的用他开发,从而发现问题,不断改进。昨天我发布了0.4.0版本,很明显,最近版本号升得比较快。

从0.3到0.4,最重大的改变是路由方面的。路由基本上完全重写,目前路由变得更统一,更灵活,性能上也有不少提升。来看看新的路由如何使用。

新路由

目前支持4种路由,静态路由,命名路由,Catch-All路由,正则路由。下面分别说说4种路由:

静态路由

静态路由是最普遍的,默认的net/http目前只支持静态路由,比如:

tango.Get("/", new(Action))
tango.Get("/public/bootstrap.min.css", new(Action))

命名路由

在0.4之前的版本中,命名路由的形式一般是:name,在0.4版本兼容以前的模式,并且新增了(:name)的形式。这个新的设计将允许使用字符或者数字作为分隔符。比如,新旧版本都支持这种形式:

tango.Get("/:name", new(Action))
tango.Get("/:name1-:name2", new(Action))
tango.Get("/:name1/:name2", new(Action))

而新版本增加了对下列形式的支持:

tango.Get("/(:name1)abc(:name2)", new(Action))
tango.Get("/(:name1)123(:name2)", new(Action))

Catch-All路由

Catch-All路由是0.4新增的一种路由形式。Catch-All路由和命名路由很相似,他们的的唯一区别在于 命名路由不能匹配/,而Catch-All路由可以匹配/。比如:

tango.Get("/*name", new(Action))
tango.Get("/*name/123", new(Action))

比如当访问 /name1/name2/123 时,第二个路由会匹配出 *name = name1/name2。当访问/name1/name2时,则第一个路由会匹配出 *name = name1/name2

获取Catch-All路由请通过 Params.Get("*name")

正则路由

注意:0.4版本的正则路由和老版本的正则路由不兼容

为了统一表达形式,0.4版本抛弃了以前版本中完全使用Go的正则的方式来进行匹配的方式,如:

0.3版本以前:

tango.Get("/(.*)", new(Action))

而获取参数通过 Params.Get(”:0”)来获取。

0.3版本到0.4版本之间:

tango.Get("/(?P<name>.*)", new(Action))

而获取参数通过 Params.Get(”:name”)来获取,同时也可以使用Params.Get(":0")来获取。

0.4版本不兼容前面的语法,再次提醒注意,而采用如下方法,匿名的正则已不被允许,Params.Get(":0")也不再受支持:

tango.Get("/(:name.*)", new(Action))
tango.Get("/(:name[0-9]+)", new(Action))

而获取参数通过 Params.Get(”:name”)来获取,也可以通过Params[0].Value来获取,Params目前变成了一个Slice。

路由优先级

  1. 静态路由和其它路由都匹配时,静态路由优先,跟添加的顺序无关;
  2. 其它路由之间根据添加的顺序,先添加的优先。

例如:

t := tango.Classic()
t.Get("/:name", new(Others))
t.Get("/admin", new(Admin))
t.Run()

以上代码,当请求 /admin 时, AdminGet 方法将被执行。

t := tango.Classic()
t.Get("/:name", new(Admin))
t.Get("/*name", new(Others))
t.Run()

以上代码,当请求 /admin 时, AdminGet 方法将被执行;当请求 /admin/ui, OthersGet 方法将被执行。

t := tango.Classic()
t.Get("/*name", new(Admin))
t.Get("/:name", new(Others))
t.Run()

以上代码, OthersGet 方法将永远不会被执行, 因为所有匹配的请求均会调用 AdminGet 方法。

注意事项

关于新路由也有几点需要注意的事项:

  • 命名和正则等可以混合使用,如:/:name1-(:name2[0-9]+),但是要注意,必须要有分隔符,没有分隔符,在新增路由时会panic,比如如下形式就不受支持:/:name1:name2

  • 命名理论上是可以相同的,并且程序不会报错,但是不建议这样使用。通过Params.Get(”:name”)只会返回第一个,可通过自己遍历Params来获取多个相同的名字。比如:/:name-:name,这种规则是允许的,但是不推荐。

收尾

新的路由形式上更加统一,相信可以满足大家的绝大部分需求。希望大家多提宝贵意见,可加QQ群:369240307,或者在 github.com/lunny/tango 以及 git.oschina.net/lunny/tango 中提出issue。

Tango,微内核可扩展的Go语言Web框架

简介

Golang的web框架基本上处于一个井喷期,那么为什么要再造一个轮子。这是因为,目前可扩展性比较强的都是基于函数作为可执行体的,而以结构体作为执行体的框架目前可扩展性都不够强,包括我原先写的框架xweb也是如此。因此,一个全新的框架出来了,先上地址:https://github.com/lunny/tango

初看Tango框架,感觉和Martini及Macaron非常的像。比如这段代码:

package main

import "github.com/lunny/tango"

func main() {
    t := tango.Classic()
    t.Get("/", func() string {
        return "Hello tango!"
    })
    t.Run()
}

这种其实大家都支持的。再看这个吧:

package main

import "github.com/lunny/tango"

type Action struct {}
func (Action) Get() string {
    return "Hello tango!"
}

func main() {
    t := tango.Classic()
    t.Get("/", new(Action))
    t.Run()
}

Tango同时支持函数和结构体作为执行体,不过主打是结构体,函数只作为兼容而用。下面对Tango的各种功能一一道来。

路由

Tango目前同时支持3种路由规则:

  • 静态路由

    tg.Get("/", new(Action))
    
  • 命名路由

    tg.Get("/:name", new(Action))
    

    命名路由对应的参数内容可通过ctx.Params().Get(":name")来获得

  • 正则路由

tg.Get("/(.*)", new(Action))

正则路由对应的参数内容可通过ctx.Params().Get(":0")来获得

这里要注意命名路由和正则路由不可混用。

执行体

同时支持函数执行体和结构体执行体,支持的函数执行体形式如下,可以有零个或一个返回值,返回值由Return插件提供,后面会再提:

func()
func(http.ResponseWriter, *http.Request)
func(*tango.Context)
func(http.Response.Writer)
func(*http.Request)

同时支持包含Get,Post,Head,Options,Trace,Patch,Delete,Put作为成员方法的结构体作为执行体。

type Action struct {}
func (Action) Get() string {
     return "Get"
}

func (Action) Post() string {
     return "Post"
}

func (Action) Head() string {
     return "Head"
}

func (Action) Options() string {
     return "Options"
}

func (Action) Trace() string {
     return "Trace"
}

func (Action) Patch() string {
     return "Patch"
}

func (Action) Delete() string {
     return "Delete"
}

func (Action) Put() string {
     return "Put"
}

在使用路由时,可以用对应的方法或者Any来加路由:

t := tango.Classic()
t.Get("/1", new(Action))
t.Any("/2", new(Action))

路由分组

Tango提供了Group来进行路由分组,Group的最简单形式如下:

g := tango.NewGroup()
g.Get("/1", func() string {
    return "/1"
})
g.Post("/2", func() string {
    return "/2"
})

t := tango.Classic()
t.Group("/api", g)

这样访问/api/1就会返回/1,访问/api/2就会返回/2

同时也可以这样:

t := tango.Classic()
t.Group("/api", func(g *tango.Group) {
    g.Get("/1", func() string {
       return "/1"
    })
    g.Post("/2", func() string {
       return "/2"
    })
})

Group也支持子Group:

t := tango.Classic()
t.Group("/api", func(g *tango.Group) {
    g.Group("/v1", func(cg *tango.Group) {
        cg.Get("/1", func() string {
        return "/1"
        })
        cg.Post("/2", func() string {
        return "/2"
        })
    })
})

最后,Group也支持逻辑分组:

o := tango.Classic()
o.Group("", func(g *tango.Group) {
    g.Get("/1", func() string {
    return "/1"
    })
})

o.Group("", func(g *tango.Group) {
    g.Post("/2", func() string {
    return "/2"
    })
})

这样,即使路由间只是逻辑上分开,而并没有上级路径分开,也是可以分成不同的组。

返回值

通过前面的例子,我们会发现,所有执行体函数,可以有一个返回值或者没有返回值。Tango的内部插件ReturnHandler来负责根据函数的第一个返回值的类型来自动的生成输出,默认规则如下:

  • string 返回string,则string会被转换为bytes并写入到ResponseWriter,默认状态码为200

  • []byte 返回[]byte, 则会直接写入ResponseWriter,默认状态码为200

  • error 返回error接口,如果不为nil, 则返回状态码为500,内容为error.Error()

  • AbortError 返回tango.AbortError接口,如果不为nil,则返回状态码为AbortError.Code,内容为AbortError.Error()

当然了,你可以撰写你自己的插件来判断更多的返回值类型。返回值结合tango的一些tricker,可以极大的简化代码,比如:

如果Action结构体包含匿名结构体tango.Json或者tango.Xml,则返回值结果如下:

如果包含tango.Json匿名结构体,则返回头的Content-Type会自动设置为:application/json

  • 如果返回值为error,则返回值为{“err”: err.Error()},状态码为200

  • 如果返回值为AbortError,则返回值为{“err”: err.Error()},状态码为err.Code()

  • 如果返回值为string,则返回值为{“content”: content},状态码为200

  • 如果返回值为[]byte,则返回值为{“content”: string(content)},状态码为200

  • 如果返回值为map,slice,结构体或其它可自动Json化的内容,则返回值为map自动json对应的值,状态码为200

例如:

type Action struct {
    tango.Json
}

var i int
func (Action) Get() interface{} {
   if i == 0 {
       i = i + 1
       return map[string]interface{}{"i":i}
   }
   return errors.New("could not visit")
}

func main() {
    t := tango.Classic()
    t.Any("/", new(Action))
    t.Run()
}

以上例子,访问时会始终返回json,第一次访问会返回map,第二次返回error。(注:这里只是演示代码,实际执行i必须加锁)

  • Compress

Tango拥有一个默认的压缩中间件,可以按照扩展名来进行文件的压缩。同时,你也可以要求某个Action自动或强制使用某种压缩。比如:

type CompressExample struct {
    tango.Compress // 添加这个匿名结构体,要求这个结构体的方法进行自动检测压缩
}

func (CompressExample) Get() string {
    return fmt.Sprintf("This is a auto compress text")
}

o := tango.Classic()
o.Get("/", new(CompressExample))
o.Run()

以上代码默认会检测浏览器是否支持压缩,如果支持,则看是否支持gzip,如果支持gzip,则使用gzip压缩,如果支持deflate,则使用deflate压缩。

type GZipExample struct {
    tango.GZip // add this for ask compress to GZip, if accept-encoding has no gzip, then not compress
}

func (GZipExample) Get() string {
    return fmt.Sprintf("This is a gzip compress text")
}

o := tango.Classic()
o.Get("/", new(GZipExample))
o.Run()

以上代码默认会检测浏览器是否支持gzip压缩,如果支持gzip,则使用gzip压缩,否则不压缩。

type DeflateExample struct {
    tango.Deflate // add this for ask compress to Deflate, if not support then not compress
}

func (DeflateExample) Get() string {
    return fmt.Sprintf("This is a deflate compress text")
}

o := tango.Classic()
o.Get("/", new(DeflateExample))
o.Run()

以上代码默认会检测浏览器是否支持deflate压缩,如果支持deflate,则使用deflate压缩,否则不压缩。

Static

Static 让你用一行代码可以完成一个静态服务器。

func main() {
    t := tango.New(tango.Static())
    t.Run()
}

然后,将你的文件放到 ./public 目录下,你就可以通过浏览器放问到他们。比如:

http://localhost/images/logo.png  --> ./public/images/logo.png

当然,你也可以加入basicauth或者你自己的认证中间件,这样就变为了一个私有的文件服务器。

func main() {
    t := tango.New()
    t.Use(AuthHandler)
    t.Use(tango.Static())
    t.Run()
}

Handler

Handler 是tango的中间件。在tango中,几乎所有的事情都由中间件来完成。撰写一个你自己的中间件非常简单,并且我们鼓励您只加载需要的中间件。

tango的中间件只需要符合以下接口即可。

type Handler interface {
    Handle(*tango.Context)
}

同时,tango也提供了tango.HandlerFunc,以方便你将一个函数包装为中间件。比如:

func MyHandler() tango.HandlerFunc {
    return func(ctx *tango.Context) {
        fmt.Println("this is my first tango handler")
        ctx.Next()
    }
}

t := tango.Classic()
t.Use(MyHandler())
t.Run()

正常的形式也可以是:

type HelloHandler struct {}
func (HelloHandler) Handle(ctx *tango.Context) {
    fmt.Println("before")
    ctx.Next()
    fmt.Println("after")
}

t := tango.Classic()
t.Use(new(HelloHandler))
t.Run()

当然,你可以直接将一个包含tango.Context指针的函数作为中间件,如:

tg.Use(func(ctx *tango.Context){
    fmt.Println("before")
    ctx.Next()
    fmt.Println("after")
})

为了和标准库兼容,tango通过UseHandler支持http.Handler作为中间件,如:

tg.UseHandler(http.Handler(func(resp http.ResponseWriter, req *http.Request) {

}))

老的中间件会被action被匹配之前进行调用。

Call stack

以下是中间件的调用顺序图:

tango.ServeHttp
|--Handler1
      |--Handler2
            |-- ...HandlerN
                      |---Action(If matched)
                ...HandlerN--|
         Handler2 ----|
   Handler1--|
(end)--|

在中间件中,您的中间件代码可以在Next()被调用之前或之后执行,Next表示执行下一个中间件或Action被执行(如果url匹配的话)。如果不调用Next,那么当前请求将会被立即停止,之后的所有代码将不会被执行。

注入

更多的注入方式参见以下示例代码:

Context

type Action struct {
    tango.Ctx
}

Logger

type Action struct {
    tango.Log
}

Params

type Action struct {
    tango.Params
}

Json

type Action struct {
    tango.Json
}

Xml

type Action struct {
    tango.Xml
}

第三方插件

目前已经有了一批第三方插件,更多的插件正在陆续开发中,欢迎大家进行贡献:

案例