核心服务

为了帮助用户快速构建 Web 应用,Flamego 提供了一些几乎每个 Web 应用都会使用到的核心服务,但是否使用这些核心服务的选择权仍旧在用户手里。Flamego 的设计理念永远是简洁的核心以及按需扩展,不会捆绑给用户不需要的东西。

请求上下文

每个处理器在运行时被调用都会获得一个类型为 flamego.Context核心服务 - 图1在新窗口打开 的请求上下文。除了一些如缓存、数据库连接等有状态的资源之外,每个请求上下文之间的数据和状态并不隐性共享。

因此,flamego.Context 可以在你的处理器中被直接使用:

  1. func main() {
  2. f := flamego.New()
  3. f.Get("/", func(c flamego.Context) string {
  4. ...
  5. })
  6. f.Run()
  7. }

Next

当一个路由被匹配时,Flame 实例会将与路由绑定的中间件和处理器按照注册的顺序生成一个调用栈核心服务 - 图2在新窗口打开

在默认情况下,调用栈中的处理器只有在前一个处理器执行完成并退出后才会调用后续的处理器。但是,你可以通过调用 Next 方法来改变这种默认行为,从而使得当前处理器的逻辑暂停执行,直到后续的所有处理器执行完成后再恢复执行。

  1. package main
  2. import (
  3. "fmt"
  4. "github.com/flamego/flamego"
  5. )
  6. func main() {
  7. f := flamego.New()
  8. f.Get("/",
  9. func(c flamego.Context) {
  10. fmt.Println("starting the first handler")
  11. c.Next()
  12. fmt.Println("exiting the first handler")
  13. },
  14. func() {
  15. fmt.Println("executing the second handler")
  16. },
  17. )
  18. f.Run()
  19. }

运行上面的程序并执行 curl http://localhost:2830/ 后,可以在终端看到如下输出:

  1. [Flamego] Listening on 0.0.0.0:2830 (development)
  2. starting the first handler
  3. executing the second handler
  4. exiting the first handler

路由日志就是利用这个功能实现响应时间和状态码的收集核心服务 - 图3在新窗口打开

客户端地址

Web 应用经常会想要知道客户端的来源地址,RemoteAddr() 则是一个提供获取客户端地址的辅助方法:

  1. func main() {
  2. f := flamego.New()
  3. f.Get("/", func(c flamego.Context) string {
  4. return "The remote address is " + c.RemoteAddr()
  5. })
  6. f.Run()
  7. }

标准库的 http.Request.RemoteAddr 字段仅会记录客户端的最近发起地址,如果 Web 应用存在反向代理的话,这个字段的值就显得毫无意义。RemoteAddr() 方法会按照如下顺序从一些特定的 HTTP 请求头中获取潜在的客户端地址:

  • X-Real-IP
  • X-Forwarded-For
  • http.Request.RemoteAddr

这样一来,就可以配置反向代理提供这些请求头来将客户端地址传递给你的 Web 应用。

注意

目前并没有绝对可靠的方法来获取真实的客户端地址,尤其是在客户端使用 VPN 或者代理访问 Web 应用的情况下。

重定向

Redirect 方法是 http.Redirect 方法的语法糖核心服务 - 图4在新窗口打开,因为它可以直接从 Flame 实例的请求上下文中获取重定向所需的 http.ResponseWriter*http.Request 对象,并使用 http.StatusFound 作为默认的跳转码:

  • 代码
  • 测试
  1. package main
  2. import (
  3. "net/http"
  4. "github.com/flamego/flamego"
  5. )
  6. func main() {
  7. f := flamego.New()
  8. f.Get("/", func(c flamego.Context) {
  9. c.Redirect("/signup")
  10. })
  11. f.Get("/login", func(c flamego.Context) {
  12. c.Redirect("/signin", http.StatusMovedPermanently)
  13. })
  14. f.Run()
  15. }
  1. $ curl -i http://localhost:2830/
  2. HTTP/1.1 302 Found
  3. ...
  4. $ curl -i http://localhost:2830/login
  5. HTTP/1.1 301 Moved Permanently
  6. ...

注意

Redirect 仅实现了无脑的重定向逻辑,因此可能使你的 Web 应用遭受开放重定向漏洞核心服务 - 图5在新窗口打开的攻击,例如:

  1. c.Redirect("https://www.google.com")

请务必在进行重定向之前检验用户的输入!

URL 参数

URL 参数核心服务 - 图6在新窗口打开,也叫“URL 查询参数”,又叫“URL 查询字符串”,常被用于页面传递参数给后端服务器。

Query 是用于获取 URL 参数的辅助方法,若指定参数不存在则返回空字符串:

  • 代码
  • 测试
  1. package main
  2. import (
  3. "github.com/flamego/flamego"
  4. )
  5. func main() {
  6. f := flamego.New()
  7. f.Get("/", func(c flamego.Context) string {
  8. return "The name is " + c.Query("name")
  9. })
  10. f.Run()
  11. }
  1. $ curl http://localhost:2830?name=joe
  2. The name is joe
  3. $ curl http://localhost:2830
  4. The name is

Flame 实例的请求上下文提供了一系列相关的辅助方法,包括:

  • QueryTrim 去除空格并返回值
  • QueryStrings 返回字符串列表
  • QueryUnescape 返回未经反转义的值
  • QueryBool 返回解析为 bool 类型的值
  • QueryInt 返回解析为 int 类型的值
  • QueryInt64 返回解析为 int64 类型的值
  • QueryFloat64 返回解析为 float64 类型的值

以上方法均接受一个可选参数作为所获取的 URL 参数不存在时的默认值。

提示

如果现有的辅助方法不能满足应用需求,你还可以通过直接操作底层的 url.Values核心服务 - 图7在新窗口打开 来获取:

  1. vals := c.Request().URL.Query()

flamego.Context 是否可以替代 context.Context

不可以。

flamego.Context 是请求上下文的容器,仅适用于路由层,而 context.Context 是通用的上下文容器,应当被用于后续的传递(如传递到数据库层)。

你可以通过如下方法获取请求所对应的 context.Context

  1. f.Get(..., func(c flamego.Context) {
  2. ctx := c.Request().Context()
  3. ...
  4. })
  5. // 或
  6. f.Get(..., func(r *http.Request) {
  7. ctx := r.Context()
  8. ...
  9. })

默认日志器

Charm核心服务 - 图8在新窗口打开 团队开源的 log.Logger核心服务 - 图9在新窗口打开 可以作为所有中间件和处理器的通用结构化日志器(Structured Logging)使用:

  1. package main
  2. import (
  3. "github.com/charmbracelet/log"
  4. "github.com/flamego/flamego"
  5. )
  6. func main() {
  7. f := flamego.New()
  8. f.Get("/", func(r *http.Request, logger log.Logger) {
  9. logger.Info("Hello, Flamego!", "path", r.RequestURI)
  10. })
  11. f.Run()
  12. }

运行上面的程序并执行 curl http://localhost:2830/ 后,可以在终端看到如下输出:

  1. 2023-03-06 20:57:38 🧙 Flamego: Listening on 0.0.0.0:2830 env=development
  2. 2023-03-06 20:57:51 INFO 🧙 Flamego: Hello, Flamego! path=/

路由日志就是使用了这个核心服务实现响应时间和状态码的打印核心服务 - 图10在新窗口打开

提示

1.8.0 之前的版本仅支持标准库的 *log.Logger核心服务 - 图11在新窗口打开 作为日志器。

响应流

请求的响应流是通过 http.ResponseWriter核心服务 - 图12在新窗口打开 类型来表示的,你可以通过处理器参数或调用 flamego.ContextResponseWriter 方法来获取它:

  1. f.Get(..., func(w http.ResponseWriter) {
  2. ...
  3. })
  4. // 或
  5. f.Get(..., func(c flamego.Context) {
  6. w := c.ResponseWriter()
  7. ...
  8. })

💡 小贴士

并不是所有在调用栈中的中间件和处理器都一定会被调用,请求上下文(flamego.Context)会在任一输出状态码的处理器核心服务 - 图13在新窗口打开执行完成之后停止调用剩余的处理器,这个机制类似于短路评估核心服务 - 图14在新窗口打开

请求对象

请求对象是通过 *http.Request核心服务 - 图15在新窗口打开 类型来表示的,你可以通过处理器参数或调用 flamego.ContextRequest().Request 方法来获取它:

  1. f.Get(..., func(r *http.Request) {
  2. ...
  3. })
  4. // 或
  5. f.Get(..., func(c flamego.Context) {
  6. r := c.Request().Request
  7. ...
  8. })

你可能会疑惑上例中的 c.Request() 返回的是什么类型?

这个方法返回的是 *http.Request 类型的一层简单封装 *flamego.Request核心服务 - 图16在新窗口打开,包含了一些用于读取请求体的辅助方法,例如:

  1. f.Get(..., func(c flamego.Context) {
  2. body := c.Request().Body().Bytes()
  3. ...
  4. })

路由日志

提示

该中间件是通过 flamego.Classic核心服务 - 图17在新窗口打开 所创建的 Flame 实例的默认中间件之一。

flamego.Logger核心服务 - 图18在新窗口打开 是用于提供请求路由和状态码记录的中间件:

  1. package main
  2. import (
  3. "github.com/flamego/flamego"
  4. )
  5. func main() {
  6. f := flamego.New()
  7. f.Use(flamego.Logger())
  8. f.Get("/", func() (int, error) {
  9. return http.StatusOK, nil
  10. })
  11. f.Run()
  12. }

运行上面的程序并执行 curl http://localhost:2830/ 后,可以在终端看到如下输出:

  1. 2023-03-06 20:59:58 🧙 Flamego: Listening on 0.0.0.0:2830 env=development
  2. 2023-03-06 21:00:01 🧙 Flamego: Started method=GET path=/ remote=127.0.0.1
  3. 2023-03-06 21:00:01 🧙 Flamego: Completed method=GET path=/ status=0 duration="564.792µs"

Panic 恢复

提示

该中间件是通过 flamego.Classic核心服务 - 图19在新窗口打开 所创建的 Flame 实例的默认中间件之一。

flamego.Recovery核心服务 - 图20在新窗口打开 是用于捕捉 panic 并自动恢复的中间件:

  1. package main
  2. import (
  3. "github.com/flamego/flamego"
  4. )
  5. func main() {
  6. f := flamego.New()
  7. f.Use(flamego.Recovery())
  8. f.Get("/", func() {
  9. panic("I can't breath")
  10. })
  11. f.Run()
  12. }

运行上面的程序并访问 http://localhost:2830/核心服务 - 图21在新窗口打开 可以看到如下内容:

panic recovery

响应静态资源

提示

该中间件是通过 flamego.Classic核心服务 - 图23在新窗口打开 所创建的 Flame 实例的默认中间件之一。

flamego.Static核心服务 - 图24在新窗口打开 是用于向客户端提供静态资源响应的中间件,并可以通过 flamego.StaticOptions核心服务 - 图25在新窗口打开 进行配置:

  1. func main() {
  2. f := flamego.New()
  3. f.Use(flamego.Static(
  4. flamego.StaticOptions{
  5. Directory: "public",
  6. },
  7. ))
  8. f.Run()
  9. }

你也可以直接使用默认配置:

  1. func main() {
  2. f := flamego.New()
  3. f.Use(flamego.Static())
  4. f.Run()
  5. }

示例:响应源文件

在本例中,我们会将源文件本身作为静态资源响应给客户端:

  1. package main
  2. import (
  3. "github.com/flamego/flamego"
  4. )
  5. func main() {
  6. f := flamego.New()
  7. f.Use(flamego.Static(
  8. flamego.StaticOptions{
  9. Directory: "./",
  10. Index: "main.go",
  11. },
  12. ))
  13. f.Run()
  14. }

在示例的第 11 行,我们将 Directory 的值设置为工作目录("./")而非默认值 "public"

在示例的第 12 行,我们将索引文件设置为 main.go 而非默认值 "index.html"

将上面的程序保存至 main.go 并且运行它,然后执行 curl http://localhost:2830/curl http://localhost:2830/main.go 都可以得到 main.go 的文件内容本身。

示例:响应多个目录

在本例中,我们会将两个不同目录的文件作为静态资源响应给客户端:

  • 文件树
  • css/main.css
  • js/main.js
  • main.go
  • 测试
  1. $ tree .
  2. .
  3. ├── css
  4. └── main.css
  5. ├── go.mod
  6. ├── go.sum
  7. ├── js
  8. └── main.js
  9. └── main.go
  1. html {
  2. color: red;
  3. }
  1. console.log("Hello, Flamego!");
  1. package main
  2. import (
  3. "github.com/flamego/flamego"
  4. )
  5. func main() {
  6. f := flamego.New()
  7. f.Use(flamego.Static(
  8. flamego.StaticOptions{
  9. Directory: "js",
  10. },
  11. ))
  12. f.Use(flamego.Static(
  13. flamego.StaticOptions{
  14. Directory: "css",
  15. },
  16. ))
  17. f.Run()
  18. }
  1. $ curl http://localhost:2830/main.css
  2. html {
  3. color: red;
  4. }
  5. $ curl http://localhost:2830/main.js
  6. console.log("Hello, Flamego!");

你可能注意到客户端不需要将 Directory 的值作为请求路径的一部分,即本例中的 "css""js"。如果你希望客户端带有请求前缀,则可以通过配置 Prefix 实现:

  1. f.Use(flamego.Static(
  2. flamego.StaticOptions{
  3. Directory: "css",
  4. Prefix: "css",
  5. },
  6. ))

💡 小贴士

虽然本例中的 PrefixDirectory 值是相同的,但并不是必须的,它们之间没有直接关联。

示例:响应 embed.FS

在本例中,我们会将自 Go 1.16 起支持核心服务 - 图26在新窗口打开embed.FS核心服务 - 图27在新窗口打开 作为静态资源的数据来源响应给客户端:

  • 文件树
  • css/main.css
  • main.go
  • 测试
  1. tree .
  2. .
  3. ├── css
  4. └── main.css
  5. ├── go.mod
  6. ├── go.sum
  7. └── main.go
  1. html {
  2. color: red;
  3. }
  1. package main
  2. import (
  3. "embed"
  4. "net/http"
  5. "github.com/flamego/flamego"
  6. )
  7. //go:embed css
  8. var css embed.FS
  9. func main() {
  10. f := flamego.New()
  11. f.Use(flamego.Static(
  12. flamego.StaticOptions{
  13. FileSystem: http.FS(css),
  14. },
  15. ))
  16. f.Run()
  17. }
  1. $ curl http://localhost:2830/css/main.css
  2. html {
  3. color: red;
  4. }

注意

由于 Go embed 功能会编码文件的完整路径,客户端必须携带父目录的信息才可以访问到对应的资源,这和直接从本地文件响应有所区别。

下面是错误的客户端请求方式:

  1. $ curl http://localhost:2830/main.css
  2. 404 page not found

渲染内容

flamego.Renderer核心服务 - 图28在新窗口打开 是用于向客户端渲染指定数据格式的中间件,并可以通过 flamego.RenderOptions核心服务 - 图29在新窗口打开 进行配置。

flamego.Render核心服务 - 图30在新窗口打开 会作为渲染服务注入到请求的上下文中,你可以用它渲染 JSON、XML、二进制或纯文本格式的数据:

  • 代码
  • 测试
  1. package main
  2. import (
  3. "net/http"
  4. "github.com/flamego/flamego"
  5. )
  6. func main() {
  7. f := flamego.New()
  8. f.Use(flamego.Renderer(
  9. flamego.RenderOptions{
  10. JSONIndent: " ",
  11. },
  12. ))
  13. f.Get("/", func(r flamego.Render) {
  14. r.JSON(http.StatusOK,
  15. map[string]interface{}{
  16. "id": 1,
  17. "username": "joe",
  18. },
  19. )
  20. })
  21. f.Run()
  22. }
  1. $ curl -i http://localhost:2830/
  2. HTTP/1.1 200 OK
  3. Content-Type: application/json; charset=utf-8
  4. ...
  5. {
  6. "id": 1,
  7. "username": "joe"
  8. }

提示

尝试将第 13 行修改为 JSONIndent: "",,然后重新运行一遍之前的测试,看看会有什么不同。