路由模块

在 Macaron 中, 路由是一个 HTTP 方法配对一个 URL 匹配模型. 每一个路由可以对应一个或多个处理器方法:

  1. m.Get("/", func() {
  2. // show something
  3. })
  4. m.Patch("/", func() {
  5. // update something
  6. })
  7. m.Post("/", func() {
  8. // create something
  9. })
  10. m.Put("/", func() {
  11. // replace something
  12. })
  13. m.Delete("/", func() {
  14. // destroy something
  15. })
  16. m.Options("/", func() {
  17. // http options
  18. })
  19. m.Any("/", func() {
  20. // do anything
  21. })
  22. m.Route("/", "GET,POST", func() {
  23. // combine something
  24. })
  25. m.Combo("/").
  26. Get(func() string { return "GET" }).
  27. Patch(func() string { return "PATCH" }).
  28. Post(func() string { return "POST" }).
  29. Put(func() string { return "PUT" }).
  30. Delete(func() string { return "DELETE" }).
  31. Options(func() string { return "OPTIONS" }).
  32. Head(func() string { return "HEAD" })
  33. m.NotFound(func() {
  34. // 自定义 404 处理逻辑
  35. })

几点说明:

  • 路由匹配的顺序是按照他们被定义的顺序执行的,
  • …但是,匹配范围较小的路由优先级比匹配范围大的优先级高(详见 匹配优先级)。
  • 最先被定义的路由将会首先被用户请求匹配并调用。

在一些时候,每当 GET 方法被注册的时候,都会需要注册一个一模一样的 HEAD 方法。为了达到减少代码的目的,您可以使用一个名为 SetAutoHead 的方法来协助您自动注册:

  1. m := New()
  2. m.SetAutoHead(true)
  3. m.Get("/", func() string {
  4. return "GET"
  5. }) // 路径 "/" 的 HEAD 也已经被自动注册

如果您想要使用子路径但让路由代码保持简洁,可以调用 m.SetURLPrefix(suburl)

命名参数

路由模型可能包含参数列表, 可以通过 *Context.Params 来获取:

占位符

使用一个特定的名称来代表路由的某个部分:

  1. m.Get("/hello/:name", func(ctx *macaron.Context) string {
  2. return "Hello " + ctx.Params(":name")
  3. })
  4. m.Get("/date/:year/:month/:day", func(ctx *macaron.Context) string {
  5. return fmt.Sprintf("Date: %s/%s/%s", ctx.Params(":year"), ctx.Params(":month"), ctx.Params(":day"))
  6. })

当然,想要偷懒的时候可以将 : 前缀去掉:

  1. m.Get("/hello/:name", func(ctx *macaron.Context) string {
  2. return "Hello " + ctx.Params("name")
  3. })
  4. m.Get("/date/:year/:month/:day", func(ctx *macaron.Context) string {
  5. return fmt.Sprintf("Date: %s/%s/%s", ctx.Params("year"), ctx.Params("month"), ctx.Params("day"))
  6. })

全局匹配

路由匹配可以通过全局匹配的形式:

  1. m.Get("/hello/*", func(ctx *macaron.Context) string {
  2. return "Hello " + ctx.Params("*")
  3. })

那么,如果将 * 放在路由中间会发生什么呢?

  1. m.Get("/date/*/*/*/events", func(ctx *macaron.Context) string {
  2. return fmt.Sprintf("Date: %s/%s/%s", ctx.Params("*0"), ctx.Params("*1"), ctx.Params("*2"))
  3. })

正则表达式

您还可以使用正则表达式来书写路由规则:

  • 常规匹配:

    1. m.Get("/user/:username([\\w]+)", func(ctx *macaron.Context) string {
    2. return fmt.Sprintf("Hello %s", ctx.Params(":username"))
    3. })
    4. m.Get("/user/:id([0-9]+)", func(ctx *macaron.Context) string {
    5. return fmt.Sprintf("User ID: %s", ctx.Params(":id"))
    6. })
    7. m.Get("/user/*.*", func(ctx *macaron.Context) string {
    8. return fmt.Sprintf("Last part is: %s, Ext: %s", ctx.Params(":path"), ctx.Params(":ext"))
    9. })
  • 混合匹配:

    1. m.Get("/cms_:id([0-9]+).html", func(ctx *macaron.Context) string {
    2. return fmt.Sprintf("The ID is %s", ctx.Params(":id"))
    3. })
  • 可选匹配:

    • /user/?:id 可同时匹配 /user//user/123
  • 简写:
    • /user/:id:int:int([0-9]+) 正则的简写。
    • /user/:name:string:string([\w]+) 正则的简写。

匹配优先级

以下为从高到低的不同模式的匹配优先级:

  • 静态路由:
    • /
    • /home
  • 正则表达式路由:
    • /(.+).html
    • /([0-9]+).css
  • 路径-后缀路由:
    • /*.*
  • 占位符路由:
    • /:id
    • /:name
  • 全局匹配路由:
    • /*

其它说明:

  • 相同模式的匹配优先级是根据添加的先后顺序决定的。
  • 层级相对明确的模式匹配优先级要高于相对模糊的模式:
    • /*/*/events > /*

构建 URL 路径

您可以通过 *Route.Name 方法配合命名参数来构建 URL 路径,不过首先需要为路由命名:

  1. // ...
  2. m.Get("/users/:id([0-9]+)/:name:string.profile", handler).Name("user_profile")
  3. m.Combo("/api/:user/:repo").Get(handler).Post(handler).Name("user_repo")
  4. // ...

然后通过 *Router.URLFor 方法来为指定名称的路由构建 URL 路径:

  1. // ...
  2. func handler(ctx *macaron.Context) {
  3. // /users/12/unknwon.profile
  4. userProfile := ctx.URLFor("user_profile", ":id", "12", ":name", "unknwon")
  5. // /api/unknwon/macaron
  6. userRepo := ctx.URLFor("user_repo", ":user", "unknwon", ":repo", "macaron")
  7. }
  8. // ...

配合 Go 模板引擎使用

  1. // ...
  2. m.Use(macaron.Renderer(macaron.RenderOptions{
  3. Funcs: []template.FuncMap{map[string]interface{}{
  4. "URLFor": m.URLFor,
  5. }},
  6. }))
  7. // ...

配合 Pongo2 模板引擎使用

  1. // ...
  2. ctx.Data["URLFor"] = ctx.URLFor
  3. ctx.HTML(200, "home")
  4. // ...

高级路由定义

路由处理器可以被相互叠加使用, 例如很有用的地方可以是在验证和授权的时候:

  1. m.Get("/secret", authorize, func() {
  2. // this will execute as long as authorize doesn't write a response
  3. })

让我们来看一个比较极端的例子:

  1. package main
  2. import (
  3. "fmt"
  4. "gopkg.in/macaron.v1"
  5. )
  6. func main() {
  7. m := macaron.Classic()
  8. m.Get("/",
  9. func(ctx *macaron.Context) {
  10. ctx.Data["Count"] = 1
  11. },
  12. func(ctx *macaron.Context) {
  13. ctx.Data["Count"] = ctx.Data["Count"].(int) + 1
  14. },
  15. func(ctx *macaron.Context) {
  16. ctx.Data["Count"] = ctx.Data["Count"].(int) + 1
  17. },
  18. func(ctx *macaron.Context) {
  19. ctx.Data["Count"] = ctx.Data["Count"].(int) + 1
  20. },
  21. func(ctx *macaron.Context) {
  22. ctx.Data["Count"] = ctx.Data["Count"].(int) + 1
  23. },
  24. func(ctx *macaron.Context) string {
  25. return fmt.Sprintf("There are %d handlers before this", ctx.Data["Count"])
  26. },
  27. )
  28. m.Run()
  29. }

先意淫下结果?没错,输出结果会是 There are 5 handlers before this。Macaron 并没有对您可以使用多少个处理器有一个硬性的限制。不过,Macaron 又是怎么知道什么时候停止调用下一个处理器的呢?

想要回答这个问题,我们先来看下下一个例子:

  1. package main
  2. import (
  3. "fmt"
  4. "gopkg.in/macaron.v1"
  5. )
  6. func main() {
  7. m := macaron.Classic()
  8. m.Get("/",
  9. func(ctx *macaron.Context) {
  10. ctx.Data["Count"] = 1
  11. },
  12. func(ctx *macaron.Context) {
  13. ctx.Data["Count"] = ctx.Data["Count"].(int) + 1
  14. },
  15. func(ctx *macaron.Context) {
  16. ctx.Data["Count"] = ctx.Data["Count"].(int) + 1
  17. },
  18. func(ctx *macaron.Context) {
  19. ctx.Data["Count"] = ctx.Data["Count"].(int) + 1
  20. },
  21. func(ctx *macaron.Context) string {
  22. return fmt.Sprintf("There are %d handlers before this", ctx.Data["Count"])
  23. },
  24. func(ctx *macaron.Context) string {
  25. return fmt.Sprintf("There are %d handlers before this", ctx.Data["Count"])
  26. },
  27. )
  28. m.Run()
  29. }

在这个例子中,输出结果将会变成 There are 4 handlers before this,而最后一个处理器永远也不会被调用。这是为什么呢?因为我们已经在第 5 个处理器中向响应流写入了内容。所以说,一旦任一处理器向响应流写入任何内容,Macaron 将不会再调用下一个处理器。

组路由

路由还可以通过 macaron.Group 来注册组路由:

  1. m.Group("/books", func() {
  2. m.Get("/:id", GetBooks)
  3. m.Post("/new", NewBook)
  4. m.Put("/update/:id", UpdateBook)
  5. m.Delete("/delete/:id", DeleteBook)
  6. m.Group("/chapters", func() {
  7. m.Get("/:id", GetBooks)
  8. m.Post("/new", NewBook)
  9. m.Put("/update/:id", UpdateBook)
  10. m.Delete("/delete/:id", DeleteBook)
  11. })
  12. })

同样的,您可以为某一组路由设置集体的中间件:

  1. m.Group("/books", func() {
  2. m.Get("/:id", GetBooks)
  3. m.Post("/new", NewBook)
  4. m.Put("/update/:id", UpdateBook)
  5. m.Delete("/delete/:id", DeleteBook)
  6. m.Group("/chapters", func() {
  7. m.Get("/:id", GetBooks)
  8. m.Post("/new", NewBook)
  9. m.Put("/update/:id", UpdateBook)
  10. m.Delete("/delete/:id", DeleteBook)
  11. }, MyMiddleware3, MyMiddleware4)
  12. }, MyMiddleware1, MyMiddleware2)

同样的,Macaron 不在乎您使用多少层嵌套的组路由,或者多少个组级别处理器(中间件)。