V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
lunny
V2EX  ›  程序员

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

  •  1
     
  •   lunny ·
    lunny · 2015-01-27 10:12:08 +08:00 · 3842 次点击
    这是一个创建于 3586 天前的主题,其中的信息可能已经有所发展或是发生改变。

    转自 lunny的博客

    简介

    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种路由规则:

    • 静态路由
      Go
      tg.Get("/", new(Action))

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

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

    • 正则路由 Go 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作为成员方法的结构体作为执行体。
    ```Go
    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

    同时也可以这样:
    Go
    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:
    Go
    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也支持逻辑分组:
    ```Go
    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会被转换为bytes并写入到ResponseWriter,默认状态码为200
      
      Error: language "string```" is not supported
    • 返回[]byte, 则会直接写入ResponseWriter,默认状态码为200
      
      Error: language "[]byte```" is not supported
    • 返回error接口,如果不为nil, 则返回状态码为500,内容为```error.Error()```
      
      Error: language "error```" is not supported
    • 返回tango.AbortError接口,如果不为nil,则返回状态码为```AbortError.Code```,内容为```AbortError.Error()```
      
      Error: language "AbortError```" is not supported

    当然了,你可以撰写你自己的插件来判断更多的返回值类型。返回值结合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

    例如:
    ```Go
    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必须加锁)

    压缩

    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或者你自己的认证中间件,这样就变为了一个私有的文件服务器。
    Go
    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,以方便你将一个函数包装为中间件。比如:
    ```Go
    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()
    ```

    正常的形式也可以是:
    ```Go
    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指针的函数作为中间件,如:
    Go
    tg.Use(func(ctx *tango.Context){
    fmt.Println("before")
    ctx.Next()
    fmt.Println("after")
    })

    为了和标准库兼容,tango通过UseHandler支持http.Handler作为中间件,如:
    ```Go
    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,那么当前请求将会被立即停止,之后的所有代码将不会被执行。

    注入

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

    Request

    type Action struct {
        tango.Req
    }
    

    Response

    type Action struct {
        tango.Resp
    }
    

    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
    }
    

    第三方插件

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

    案例

    • Wego - golanghome.com论坛的修改版
    • ABlog - 一个新型博客
    7 条回复    2015-01-28 17:55:15 +08:00
    Bearox
        1
    Bearox  
       2015-01-27 15:35:17 +08:00
    你好,我是一个大四的计算机相关学生。没有任何项目经验,出于兴趣,想学一下GO语言,有什么建议么?还有Go语言的前景如何,我纯粹是因为看好google,所以才看好GO语言。对此不是很了解。
    jinzhe
        2
    jinzhe  
       2015-01-27 23:25:55 +08:00
    感觉语法不爽,为啥初始化不是tango.Init()而是tango.Classic(),还有o.Get("/", new(CompressExample))其中的new感觉多余
    lunny
        3
    lunny  
    OP
       2015-01-28 10:04:55 +08:00
    @Bearox 个人感觉Go语言前景广阔啊。学一学你就知道了。
    lunny
        4
    lunny  
    OP
       2015-01-28 10:06:48 +08:00
    @jinzhe 用Classic而不用Init是因为,tango.Classic()包含了一些内置中间件,如果你要完全写自己的中间件,那么可以tango.New(),o.Get("/", new(CompressExample)),这个new是Go的新建对象语法。
    lunny
        5
    lunny  
    OP
       2015-01-28 10:07:10 +08:00
    有些Markdown格式化不正确,但是系统不让我修改了。。。
    jinzhe
        6
    jinzhe  
       2015-01-28 11:43:24 +08:00
    @lunny 感觉和马卡龙和martini差不多
    lunny
        7
    lunny  
    OP
       2015-01-28 17:55:15 +08:00
    @jinzhe 不一样,martini和macaron都是以函数作为控制器载体,tango主要是以结构体作为控制器载体。中间件这块形式上和martini和macaron有点像。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2884 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 30ms · UTC 14:46 · PVG 22:46 · LAX 06:46 · JFK 09:46
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.