mirror of
https://github.com/infinite-iroha/touka.git
synced 2026-02-02 16:31:11 +08:00
update about
This commit is contained in:
parent
1f0724af94
commit
3e76566917
2 changed files with 609 additions and 154 deletions
186
README.md
186
README.md
|
|
@ -1,34 +1,10 @@
|
|||
# Touka(灯花)框架
|
||||
|
||||
Touka(灯花) 是一个基于 Go 语言构建的多层次、高性能 Web 框架。其设计目标是为开发者提供**更直接的控制、有效的扩展能力,以及针对特定场景的行为优化**
|
||||
Touka(灯花) 是一个基于 Go 语言构建的多层次、高性能 Web 框架。其设计目标是为开发者提供**更直接的控制、有效的扩展能力,以及针对特定场景的行为优化**。
|
||||
|
||||
## Touka 的设计特点
|
||||
**想深入了解 Touka 吗?请阅读我们的 -> [深度指南 (about-touka.md)](about-touka.md)**
|
||||
|
||||
Touka 在一些特定方面进行了细致的设计与实现,旨在提供便利的工具与更清晰的控制:
|
||||
|
||||
* **统一且可定制的错误处理**
|
||||
Touka 提供了灵活的错误处理机制,允许开发者通过 `Engine.SetErrorHandler` 设置统一的错误响应逻辑。此机制不仅适用于框架内部产生的错误,更特别之处在于它能够**捕获由 `http.FileServer` 等标准库处理器返回的 404 Not Found、403 Forbidden 等错误状态码**。
|
||||
* **设计考量:** 默认情况下,`http.FileServer` 在文件未找到或权限不足时会直接返回标准错误响应。Touka 的设计能够拦截这些由 `http.FileServer` 发出的错误信号,并将其转发给框架统一的 `ErrorHandler`。这使得开发者可以为文件服务中的异常情况提供**与应用其他部分风格一致的自定义错误响应**,从而提升整体的用户体验和错误管理效率。
|
||||
|
||||
* **客户端 IP 来源的透明解析**
|
||||
Touka 提供了可配置的客户端 IP 获取机制。开发者可以通过 `Engine.SetRemoteIPHeaders` 指定框架优先从哪些 HTTP 头部(如 `X-Forwarded-For`、`X-Real-IP`)解析客户端真实 IP,并通过 `Engine.SetForwardByClientIP` 控制此机制的启用。
|
||||
* **实现细节:** `Context.RequestIP()` 方法会根据这些配置,从 `http.Request.Header` 中解析并返回第一个有效的 IP 地址。如果未配置或头部中未找到有效 IP,则回退到 `http.Request.RemoteAddr`,并对 IP 格式进行验证。这有助于在存在多层代理的环境中获取准确的源 IP。
|
||||
|
||||
* **内置日志与出站 HTTP 客户端的 Context 绑定**
|
||||
Touka 的核心 `Context` 对象直接包含了对 `reco.Logger`(一个异步、结构化日志库)和 `httpc.Client`(一个功能增强的 HTTP 客户端)的引用。开发者可以直接通过 `c.GetLogger()` 和 `c.Client()` 在请求处理函数中访问这些工具。
|
||||
* **设计考量:** 这种集成方式旨在提供这些核心工具在**特定请求生命周期内的统一访问点**。所有日志记录和出站 HTTP 请求操作都与当前请求上下文绑定,并能利用框架层面的全局配置,有助于简化复杂请求处理场景下的代码组织。
|
||||
|
||||
* **强健的 Panic 恢复与连接状态感知**
|
||||
Touka 提供的 `Recovery` 中间件能够捕获处理链中的 `panic`。它会记录详细的堆栈信息和请求快照。此外,它能**识别由客户端意外断开连接**引起的网络错误(如 `broken pipe` 或 `connection reset by peer`),在这些情况下,框架会避免尝试向已失效的连接写入响应。
|
||||
* **设计考量:** 这有助于防止因底层网络问题或客户端行为导致的二次 `panic`,避免在关闭的连接上进行无效写入,从而提升服务的稳定性。
|
||||
|
||||
* **HTTP 协议版本与服务器行为的细致控制**
|
||||
Touka 允许开发者通过 `Engine.SetProtocols` 方法,精确定义服务器支持的 HTTP 协议版本(HTTP/1.1、HTTP/2、H2C)。框架也提供了对重定向行为、未匹配路由处理和文件服务行为的配置选项。
|
||||
* **设计考量:** 这种协议和行为的细致化控制,为开发者提供了在特定部署环境(如 gRPC-Web 对 HTTP/2 的要求)中对服务器通信栈进行调整的能力。
|
||||
|
||||
* **Context 对象的高效复用**
|
||||
Touka 对其核心 `Context` 对象进行了池化管理。每个请求处理结束后,`Context` 对象会被重置并返回到对象池中,以便后续请求复用。
|
||||
* **设计考量:** 这种机制旨在减少每次请求的内存分配和垃圾回收(GC)压力,尤其在高并发场景下,有助于提供更平滑和可预测的性能表现。
|
||||
这份深度指南包含了对框架设计哲学、核心功能(路由、上下文、中间件、错误处理等)的全面剖析,并提供了大量可直接使用的代码示例,帮助您快速上手并精通 Touka。
|
||||
|
||||
### 快速上手
|
||||
|
||||
|
|
@ -48,166 +24,68 @@ import (
|
|||
)
|
||||
|
||||
func main() {
|
||||
r := touka.New()
|
||||
r := touka.Default() // 使用带 Recovery 中间件的默认引擎
|
||||
|
||||
// 配置日志记录器 (可选,不设置则使用默认配置)
|
||||
// 配置日志记录器 (可选)
|
||||
logConfig := reco.Config{
|
||||
Level: reco.LevelDebug,
|
||||
Mode: reco.ModeText, // 或 reco.ModeJSON
|
||||
Mode: reco.ModeText,
|
||||
Output: os.Stdout,
|
||||
Async: true,
|
||||
BufferSize: 4096,
|
||||
}
|
||||
r.SetLogger(logConfig)
|
||||
r.SetLoggerCfg(logConfig)
|
||||
|
||||
// 配置统一错误处理器
|
||||
// Touka 允许您为 404, 500 等错误定义统一的响应。
|
||||
// 特别地,它能捕获 http.FileServer 产生的 404/403 错误并统一处理。
|
||||
r.SetErrorHandler(func(c *touka.Context, code int) {
|
||||
// 这里可以根据 code 返回 JSON, HTML, 或其他自定义错误页面
|
||||
r.SetErrorHandler(func(c *touka.Context, code int, err error) {
|
||||
c.JSON(code, touka.H{"error_code": code, "message": http.StatusText(code)})
|
||||
c.GetLogger().Errorf("发生HTTP错误: %d, 路径: %s", code, c.Request.URL.Path) // 记录错误
|
||||
c.GetLogger().Errorf("发生HTTP错误: %d, 路径: %s, 错误: %v", code, c.Request.URL.Path, err)
|
||||
})
|
||||
|
||||
// 注册基本路由
|
||||
r.GET("/hello", func(c *touka.Context) {
|
||||
// 设置响应头部
|
||||
c.SetHeader("X-Framework", "Touka") // 设置一个头部
|
||||
c.AddHeader("X-Custom-Info", "Hello") // 添加一个头部 (如果已有则追加)
|
||||
c.AddHeader("X-Custom-Info", "World") // 再次添加,Content-Type: X-Custom-Info: Hello, World
|
||||
|
||||
// 获取请求头部
|
||||
acceptEncoding := c.GetReqHeader("Accept-Encoding")
|
||||
userAgent := c.UserAgent() // 便捷获取 User-Agent
|
||||
|
||||
c.String(http.StatusOK, "Hello from Touka! Your Accept-Encoding: %s, User-Agent: %s", acceptEncoding, userAgent)
|
||||
c.GetLogger().Infof("请求 /hello 来自 IP: %s", c.ClientIP())
|
||||
// 注册路由
|
||||
r.GET("/hello/:name", func(c *touka.Context) {
|
||||
name := c.Param("name")
|
||||
query := c.DefaultQuery("mood", "happy")
|
||||
c.String(http.StatusOK, "Hello, %s! You seem %s.", name, query)
|
||||
})
|
||||
|
||||
r.GET("/json", func(c *touka.Context) {
|
||||
// 删除响应头部
|
||||
c.DelHeader("X-Powered-By") // 假设有这个头部,可以删除它
|
||||
c.JSON(http.StatusOK, touka.H{"message": "Welcome to Touka", "timestamp": time.Now()})
|
||||
})
|
||||
|
||||
// 注册包含路径参数的路由
|
||||
r.GET("/user/:id", func(c *touka.Context) {
|
||||
userID := c.Param("id") // 获取路径参数
|
||||
c.String(http.StatusOK, "User ID: %s", userID)
|
||||
})
|
||||
|
||||
// 注册使用查询参数的路由
|
||||
r.GET("/search", func(c *touka.Context) {
|
||||
query := c.DefaultQuery("q", "default_query") // 获取查询参数,提供默认值
|
||||
paramB := c.Query("paramB") // 获取另一个查询参数
|
||||
c.String(http.StatusOK, "Search query: %s, Param B: %s", query, paramB)
|
||||
})
|
||||
|
||||
// 注册处理 POST 表单的路由
|
||||
r.POST("/submit-form", func(c *touka.Context) {
|
||||
name := c.PostForm("name") // 获取表单字段值
|
||||
email := c.DefaultPostForm("email", "no_email@example.com") // 获取表单字段,提供默认值
|
||||
c.String(http.StatusOK, "Form submitted: Name=%s, Email=%s", name, email)
|
||||
})
|
||||
|
||||
// 演示 Set 和 Get 方法在中间件中传递数据
|
||||
// 在中间件中 Set 数据
|
||||
r.Use(func(c *touka.Context) {
|
||||
c.Set("requestID", "req-12345") // 设置一个数据
|
||||
c.Next()
|
||||
})
|
||||
// 在路由处理函数中 Get 数据
|
||||
r.GET("/context-data", func(c *touka.Context) {
|
||||
requestID, exists := c.Get("requestID") // 获取数据
|
||||
if !exists {
|
||||
requestID = "N/A"
|
||||
}
|
||||
c.String(http.StatusOK, "Request ID from Context: %s", requestID)
|
||||
})
|
||||
|
||||
// 服务静态文件
|
||||
// 使用 r.Static 方法,其错误(如 404)将由上面设置的 ErrorHandler 统一处理
|
||||
// 假设您的静态文件在项目根目录的 'static' 文件夹
|
||||
r.Static("/static", "./static")
|
||||
|
||||
// 演示出站 HTTP 请求 (使用 Context 中绑定的 httpc.Client)
|
||||
r.GET("/fetch-example", func(c *touka.Context) {
|
||||
resp, err := c.Client().Get("https://example.com", httpc.WithTimeout(5*time.Second))
|
||||
if err != nil {
|
||||
c.Errorf("出站请求失败: %v", err) // 记录错误
|
||||
c.String(http.StatusInternalServerError, "Failed to fetch external resource")
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||
c.String(http.StatusOK, "Fetched from example.com (first 100 bytes): %s...", bodyBytes[:min(len(bodyBytes), 100)])
|
||||
})
|
||||
|
||||
// 演示 HTTP 协议控制
|
||||
// 默认已启用 HTTP/1.1。如果需要 HTTP/2,通常需在 TLS 模式下启用。
|
||||
// r.SetProtocols(&touka.ProtocolsConfig{
|
||||
// Http1: true,
|
||||
// Http2: true, // 启用 HTTP/2 (需要 HTTPS)
|
||||
// Http2_Cleartext: false,
|
||||
// })
|
||||
|
||||
// 启动服务器 (支持优雅关闭)
|
||||
log.Println("Touka Server starting on :8080...")
|
||||
err := r.RunShutdown(":8080", 10*time.Second) // 优雅关闭超时10秒
|
||||
if err != nil {
|
||||
if err := r.RunShutdown(":8080", 10*time.Second); err != nil {
|
||||
log.Fatalf("Touka server failed to start: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
```
|
||||
|
||||
## 中间件支持
|
||||
|
||||
### 内置
|
||||
|
||||
Recovery `r.Use(touka.Recovery())`
|
||||
- **Recovery:** `r.Use(touka.Recovery())` (已包含在 `touka.Default()` 中)
|
||||
|
||||
### fenthope
|
||||
### 第三方 (fenthope)
|
||||
|
||||
[访问日志-record](https://github.com/fenthope/record)
|
||||
|
||||
[Gzip](https://github.com/fenthope/gzip)
|
||||
|
||||
[压缩-Compress(Deflate,Gzip,Zstd)](https://github.com/fenthope/compress)
|
||||
|
||||
[请求速率限制-ikumi](https://github.com/fenthope/ikumi)
|
||||
|
||||
[sessions](https://github.com/fenthope/sessions)
|
||||
|
||||
[jwt](https://github.com/fenthope/jwt)
|
||||
|
||||
[带宽限制](https://github.com/fenthope/toukautil/blob/main/bandwithlimiter.go)
|
||||
- [访问日志-record](https://github.com/fenthope/record)
|
||||
- [Gzip](https://github.com/fenthope/gzip)
|
||||
- [压缩-Compress(Deflate,Gzip,Zstd)](https://github.com/fenthope/compress)
|
||||
- [请求速率限制-ikumi](https://github.com/fenthope/ikumi)
|
||||
- [sessions](https://github.com/fenthope/sessions)
|
||||
- [jwt](https://github.com/fenthope/jwt)
|
||||
- [带宽限制](https://github.com/fenthope/toukautil/blob/main/bandwithlimiter.go)
|
||||
|
||||
## 文档与贡献
|
||||
|
||||
* **API 文档:** 访问 [pkg.go.dev/github.com/infinite-iroha/touka](https://pkg.go.dev/github.com/infinite-iroha/touka) 查看完整的 API 参考
|
||||
* **贡献:** 我们欢迎任何形式的贡献,无论是错误报告、功能建议还是代码提交。请遵循项目的贡献指南
|
||||
|
||||
* [](https://deepwiki.com/infinite-iroha/touka) 可供参考, AI生成存在幻觉, 不完全可靠, 请注意辨别
|
||||
* **深度指南:** **[about-touka.md](about-touka.md)**
|
||||
* **API 文档:** 访问 [pkg.go.dev/github.com/infinite-iroha/touka](https://pkg.go.dev/github.com/infinite-iroha/touka) 查看完整的 API 参考。
|
||||
* **贡献:** 我们欢迎任何形式的贡献,无论是错误报告、功能建议还是代码提交。请遵循项目的贡献指南。
|
||||
|
||||
## 相关项目
|
||||
|
||||
[gin](https://github.com/gin-gonic/gin) 参考并引用了相关部分代码
|
||||
|
||||
[reco](https://github.com/fenthope/reco) 灯花框架的默认日志库
|
||||
|
||||
[httpc](https://github.com/WJQSERVER-STUDIO/httpc) 原[touka-httpc](https://github.com/satomitouka/touka-httpc), 一个现代化且易用的HTTP Client, 作为Touka框架Context携带的HTTPC
|
||||
- [gin](https://github.com/gin-gonic/gin): Touka 在路由和 API 设计上参考了 Gin。
|
||||
- [reco](https://github.com/fenthope/reco): Touka 框架的默认日志库。
|
||||
- [httpc](https://github.com/WJQSERVER-STUDIO/httpc): 一个现代化且易用的 HTTP Client,作为 Touka 框架 Context 携带的 HTTPC。
|
||||
|
||||
## 许可证
|
||||
|
||||
本项目使用MPL许可证
|
||||
本项目基于 [Mozilla Public License, v. 2.0](https://mozilla.org/MPL/2.0/) 许可。
|
||||
|
||||
tree部分来自[gin](https://github.com/gin-gonic/gin)与[httprouter](https://github.com/julienschmidt/httprouter)
|
||||
|
||||
[WJQSERVER/httproute](https://github.com/WJQSERVER/httprouter)是本项目的前身(一个[httprouter](https://github.com/julienschmidt/httprouter)的fork版本)
|
||||
`tree.go` 部分代码源自 [gin](https://github.com/gin-gonic/gin) 与 [httprouter](https://github.com/julienschmidt/httprouter),其原始许可为 BSD-style。
|
||||
|
|
|
|||
577
about-touka.md
Normal file
577
about-touka.md
Normal file
|
|
@ -0,0 +1,577 @@
|
|||
# 关于 Touka (灯花) 框架:一份深度指南
|
||||
|
||||
Touka (灯花) 是一个基于 Go 语言构建的、功能丰富且高性能的 Web 框架。它的核心设计目标是为开发者提供一个既强大又灵活的工具集,允许对框架行为进行深度定制,同时通过精心设计的组件和机制,优化在真实业务场景中的开发体验和运行性能。
|
||||
|
||||
本文档旨在提供一份全面而深入的指南,帮助您理解 Touka 的核心概念、设计哲学以及如何利用其特性来构建健壮、高效的 Web 应用。
|
||||
|
||||
---
|
||||
|
||||
## 核心设计哲学
|
||||
|
||||
Touka 的设计哲学根植于以下几个核心原则:
|
||||
|
||||
* **控制力与可扩展性:** 框架在提供强大默认功能的同时,也赋予开发者充分的控制权。我们相信开发者最了解自己的业务需求。因此,无论是路由行为、错误处理逻辑,还是服务器协议,都可以根据具体需求进行精细调整和扩展。
|
||||
* **明确性与可预测性:** API 设计力求直观和一致,使得框架的行为易于理解和预测,减少开发过程中的意外。我们避免使用过多的“魔法”,倾向于让代码的意图清晰可见。
|
||||
* **性能意识:** 在核心组件的设计中,性能是一个至关重要的考量因素。通过采用如对象池、优化的路由算法等技术,Touka 致力于在高并发场景下保持低延迟和高吞吐。
|
||||
* **开发者体验:** 框架内置了丰富的辅助工具和便捷的 API,例如与请求上下文绑定的日志记录器和 HTTP 客户端,旨在简化常见任务,提升开发效率。
|
||||
|
||||
---
|
||||
|
||||
## 核心功能深度剖析
|
||||
|
||||
### 1. 引擎 (Engine):框架的中央枢纽
|
||||
|
||||
`Engine` 是 Touka 框架的实例,也是所有功能的入口和协调者。它实现了 `http.Handler` 接口,可以无缝集成到 Go 的标准 HTTP 生态中。
|
||||
|
||||
#### 1.1. 初始化引擎
|
||||
|
||||
```go
|
||||
// 创建一个“干净”的引擎,不包含任何默认中间件
|
||||
r := touka.New()
|
||||
|
||||
// 创建一个带有默认中间件的引擎,目前仅包含 Recovery()
|
||||
// 推荐在生产环境中使用,以防止 panic 导致整个服务崩溃
|
||||
r := touka.Default()
|
||||
```
|
||||
|
||||
#### 1.2. 引擎配置
|
||||
|
||||
`Engine` 提供了丰富的配置选项,允许您定制其核心行为。
|
||||
|
||||
```go
|
||||
func main() {
|
||||
r := touka.New()
|
||||
|
||||
// === 路由行为配置 ===
|
||||
|
||||
// 自动重定向尾部带斜杠的路径,默认为 true
|
||||
// e.g., /foo/ 会被重定向到 /foo
|
||||
r.SetRedirectTrailingSlash(true)
|
||||
|
||||
// 自动修复路径的大小写,默认为 true
|
||||
// e.g., /FOO 会被重定向到 /foo (如果 /foo 存在)
|
||||
r.SetRedirectFixedPath(true)
|
||||
|
||||
// 当路由存在但方法不匹配时,自动处理 405 Method Not Allowed,默认为 true
|
||||
r.SetHandleMethodNotAllowed(true)
|
||||
|
||||
// === IP 地址解析配置 ===
|
||||
|
||||
// 是否信任 X-Forwarded-For, X-Real-IP 等头部来获取客户端 IP,默认为 true
|
||||
// 在反向代理环境下非常有用
|
||||
r.SetForwardByClientIP(true)
|
||||
// 自定义用于解析 IP 的头部列表,按顺序查找
|
||||
r.SetRemoteIPHeaders([]string{"X-Forwarded-For", "X-App-Client-IP", "X-Real-IP"})
|
||||
|
||||
// === 请求体大小限制 ===
|
||||
|
||||
// 设置全局默认的请求体最大字节数,-1 表示不限制
|
||||
// 这有助于防止 DoS 攻击
|
||||
r.SetGlobalMaxRequestBodySize(10 * 1024 * 1024) // 10 MB
|
||||
|
||||
// ... 其他配置
|
||||
r.Run(":8080")
|
||||
}
|
||||
```
|
||||
|
||||
#### 1.3. 服务器生命周期管理
|
||||
|
||||
Touka 提供了对底层 `*http.Server` 的完全控制,并内置了优雅关闭的逻辑。
|
||||
|
||||
```go
|
||||
func main() {
|
||||
r := touka.New()
|
||||
|
||||
// 通过 ServerConfigurator 对 http.Server 进行自定义配置
|
||||
r.SetServerConfigurator(func(server *http.Server) {
|
||||
// 设置自定义的读写超时时间
|
||||
server.ReadTimeout = 15 * time.Second
|
||||
server.WriteTimeout = 15 * time.Second
|
||||
fmt.Println("自定义的 HTTP 服务器配置已应用")
|
||||
})
|
||||
|
||||
// 启动服务器,并支持优雅关闭
|
||||
// RunShutdown 会阻塞,直到收到 SIGINT 或 SIGTERM 信号
|
||||
// 第二个参数是优雅关闭的超时时间
|
||||
fmt.Println("服务器启动于 :8080")
|
||||
if err := r.RunShutdown(":8080", 10*time.Second); err != nil {
|
||||
log.Fatalf("服务器启动失败: %v", err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. 路由系统 (Routing):强大、灵活、高效
|
||||
|
||||
Touka 的路由系统基于一个经过优化的**基数树 (Radix Tree)**,它支持静态路径、路径参数和通配符,并能实现极高的查找性能。
|
||||
|
||||
#### 2.1. 基本路由
|
||||
|
||||
```go
|
||||
// 精确匹配的静态路由
|
||||
r.GET("/ping", func(c *touka.Context) {
|
||||
c.String(http.StatusOK, "pong")
|
||||
})
|
||||
|
||||
// 注册多个 HTTP 方法
|
||||
r.HandleFunc([]string{"GET", "POST"}, "/data", func(c *touka.Context) {
|
||||
c.String(http.StatusOK, "Data received via %s", c.Request.Method)
|
||||
})
|
||||
|
||||
// 注册所有常见 HTTP 方法
|
||||
r.ANY("/any", func(c *touka.Context) {
|
||||
c.String(http.StatusOK, "Handled with ANY for method %s", c.Request.Method)
|
||||
})
|
||||
```
|
||||
|
||||
#### 2.2. 参数化路由
|
||||
|
||||
使用冒号 `:` 来定义路径参数。
|
||||
|
||||
```go
|
||||
r.GET("/users/:id", func(c *touka.Context) {
|
||||
// 通过 c.Param() 获取路径参数
|
||||
userID := c.Param("id")
|
||||
c.String(http.StatusOK, "获取用户 ID: %s", userID)
|
||||
})
|
||||
|
||||
r.GET("/articles/:category/:article_id", func(c *touka.Context) {
|
||||
category := c.Param("category")
|
||||
articleID := c.Param("article_id")
|
||||
c.JSON(http.StatusOK, touka.H{
|
||||
"category": category,
|
||||
"id": articleID,
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
#### 2.3. 通配符路由 (Catch-all)
|
||||
|
||||
使用星号 `*` 来定义通配符路由,它会捕获该点之后的所有路径段。**通配符路由必须位于路径的末尾**。
|
||||
|
||||
```go
|
||||
// 匹配如 /static/js/main.js, /static/css/style.css 等
|
||||
r.GET("/static/*filepath", func(c *touka.Context) {
|
||||
// 捕获的路径可以通过参数名 "filepath" 获取
|
||||
filePath := c.Param("filepath")
|
||||
c.String(http.StatusOK, "请求的文件路径是: %s", filePath)
|
||||
})
|
||||
```
|
||||
|
||||
#### 2.4. 路由组 (RouterGroup)
|
||||
|
||||
路由组是组织和管理路由的强大工具,特别适用于构建结构化的 API。
|
||||
|
||||
```go
|
||||
func main() {
|
||||
r := touka.New()
|
||||
|
||||
// 所有 /api/v1 下的路由都需要经过 AuthMiddleware
|
||||
v1 := r.Group("/api/v1")
|
||||
v1.Use(AuthMiddleware()) // 应用组级别的中间件
|
||||
{
|
||||
// 匹配 /api/v1/products
|
||||
v1.GET("/products", getProducts)
|
||||
// 匹配 /api/v1/products/:id
|
||||
v1.GET("/products/:id", getProductByID)
|
||||
|
||||
// 可以在组内再嵌套组
|
||||
ordersGroup := v1.Group("/orders")
|
||||
ordersGroup.Use(OrderPermissionsMiddleware()) // 更具体的中间件
|
||||
{
|
||||
// 匹配 /api/v1/orders
|
||||
ordersGroup.GET("", getOrders)
|
||||
// 匹配 /api/v1/orders/:id
|
||||
ordersGroup.GET("/:id", getOrderByID)
|
||||
}
|
||||
}
|
||||
|
||||
r.Run(":8080")
|
||||
}
|
||||
|
||||
func AuthMiddleware() touka.HandlerFunc {
|
||||
return func(c *touka.Context) {
|
||||
// 模拟认证逻辑
|
||||
fmt.Println("V1 Auth Middleware: Checking credentials...")
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
// ... 其他处理器
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. 上下文 (Context):请求的灵魂
|
||||
|
||||
`touka.Context` 是框架中最为核心的结构,它作为每个 HTTP 请求的上下文,在中间件和最终处理器之间流转。它提供了海量的便捷 API 来简化开发。
|
||||
|
||||
#### 3.1. 请求数据解析
|
||||
|
||||
##### 获取查询参数
|
||||
|
||||
```go
|
||||
// 请求 URL: /search?q=touka&lang=go&page=1
|
||||
r.GET("/search", func(c *touka.Context) {
|
||||
// c.Query() 获取指定参数,不存在则返回空字符串
|
||||
query := c.Query("q") // "touka"
|
||||
|
||||
// c.DefaultQuery() 获取参数,不存在则返回指定的默认值
|
||||
lang := c.DefaultQuery("lang", "en") // "go"
|
||||
category := c.DefaultQuery("cat", "all") // "all"
|
||||
|
||||
c.JSON(http.StatusOK, touka.H{
|
||||
"query": query,
|
||||
"language": lang,
|
||||
"category": category,
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
##### 获取 POST 表单数据
|
||||
|
||||
```go
|
||||
// 使用 curl 测试:
|
||||
// curl -X POST http://localhost:8080/register -d "username=test&email=test@example.com"
|
||||
r.POST("/register", func(c *touka.Context) {
|
||||
username := c.PostForm("username")
|
||||
email := c.DefaultPostForm("email", "anonymous@example.com")
|
||||
// 也可以获取所有表单数据
|
||||
// form, _ := c.Request.MultipartForm()
|
||||
|
||||
c.String(http.StatusOK, "注册成功: 用户名=%s, 邮箱=%s", username, email)
|
||||
})
|
||||
```
|
||||
|
||||
##### JSON 数据绑定
|
||||
|
||||
Touka 可以轻松地将请求体中的 JSON 数据绑定到 Go 结构体。
|
||||
|
||||
```go
|
||||
type UserProfile struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Age int `json:"age" binding:"gte=18"`
|
||||
Tags []string `json:"tags"`
|
||||
Address string `json:"address,omitempty"`
|
||||
}
|
||||
|
||||
// 使用 curl 测试:
|
||||
// curl -X POST http://localhost:8080/profile -H "Content-Type: application/json" -d '''
|
||||
// {
|
||||
// "name": "Alice",
|
||||
// "age": 25,
|
||||
// "tags": ["go", "web"]
|
||||
// }
|
||||
// '''
|
||||
r.POST("/profile", func(c *touka.Context) {
|
||||
var profile UserProfile
|
||||
|
||||
// c.ShouldBindJSON() 会解析 JSON 并填充到结构体中
|
||||
if err := c.ShouldBindJSON(&profile); err != nil {
|
||||
// 如果 JSON 格式错误或不满足绑定标签,会返回错误
|
||||
c.JSON(http.StatusBadRequest, touka.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, touka.H{
|
||||
"status": "success",
|
||||
"profile": profile,
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
#### 3.2. 响应构建
|
||||
|
||||
##### 发送 JSON, String, Text
|
||||
|
||||
```go
|
||||
r.GET("/responses", func(c *touka.Context) {
|
||||
// c.JSON(http.StatusOK, touka.H{"framework": "Touka"})
|
||||
// c.String(http.StatusOK, "Hello, %s", "World")
|
||||
c.Text(http.StatusOK, "This is plain text.")
|
||||
})
|
||||
```
|
||||
|
||||
##### 渲染 HTML 模板
|
||||
|
||||
首先,需要为引擎配置一个模板渲染器。
|
||||
|
||||
```go
|
||||
// main.go
|
||||
import "html/template"
|
||||
|
||||
func main() {
|
||||
r := touka.New()
|
||||
// 加载模板文件
|
||||
r.HTMLRender = template.Must(template.ParseGlob("templates/*.html"))
|
||||
|
||||
r.GET("/index", func(c *touka.Context) {
|
||||
// 渲染 index.html 模板,并传入数据
|
||||
c.HTML(http.StatusOK, "index.html", touka.H{
|
||||
"title": "Touka 模板渲染",
|
||||
"user": "Guest",
|
||||
})
|
||||
})
|
||||
|
||||
r.Run(":8080")
|
||||
}
|
||||
|
||||
// templates/index.html
|
||||
// <h1>{{ .title }}</h1>
|
||||
// <p>Welcome, {{ .user }}!</p>
|
||||
```
|
||||
|
||||
##### 文件和流式响应
|
||||
|
||||
```go
|
||||
// 直接发送一个文件
|
||||
r.GET("/download/report", func(c *touka.Context) {
|
||||
// 浏览器会提示下载
|
||||
c.File("./reports/latest.pdf")
|
||||
})
|
||||
|
||||
// 将文件内容作为响应体
|
||||
r.GET("/show/config", func(c *touka.Context) {
|
||||
// 浏览器会直接显示文件内容(如果支持)
|
||||
c.SetRespBodyFile(http.StatusOK, "./config.yaml")
|
||||
})
|
||||
|
||||
// 流式响应,适用于大文件或实时数据
|
||||
r.GET("/stream", func(c *touka.Context) {
|
||||
// 假设 getRealTimeDataStream() 返回一个 io.Reader
|
||||
// dataStream := getRealTimeDataStream()
|
||||
// c.WriteStream(dataStream)
|
||||
})
|
||||
```
|
||||
|
||||
#### 3.3. Cookie 操作
|
||||
|
||||
Touka 提供了简单的 API 来管理 Cookie。
|
||||
|
||||
```go
|
||||
r.GET("/login", func(c *touka.Context) {
|
||||
// 设置一个有效期为 1 小时的 cookie
|
||||
c.SetCookie("session_id", "user-12345", 3600, "/", "localhost", false, true)
|
||||
c.String(http.StatusOK, "登录成功!")
|
||||
})
|
||||
|
||||
r.GET("/me", func(c *touka.Context) {
|
||||
sessionID, err := c.GetCookie("session_id")
|
||||
if err != nil {
|
||||
c.String(http.StatusUnauthorized, "请先登录")
|
||||
return
|
||||
}
|
||||
c.String(http.StatusOK, "您的会话 ID 是: %s", sessionID)
|
||||
})
|
||||
|
||||
r.GET("/logout", func(c *touka.Context) {
|
||||
// 通过将 MaxAge 设置为 -1 来删除 cookie
|
||||
c.DeleteCookie("session_id")
|
||||
c.String(http.StatusOK, "已退出登录")
|
||||
})
|
||||
```
|
||||
|
||||
#### 3.4. 中间件数据传递
|
||||
|
||||
使用 `c.Set()` 和 `c.Get()` 可以在处理链中传递数据。
|
||||
|
||||
```go
|
||||
// 中间件:生成并设置请求 ID
|
||||
func RequestIDMiddleware() touka.HandlerFunc {
|
||||
return func(c *touka.Context) {
|
||||
requestID := fmt.Sprintf("req-%d", time.Now().UnixNano())
|
||||
c.Set("RequestID", requestID)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
r := touka.New()
|
||||
r.Use(RequestIDMiddleware())
|
||||
|
||||
r.GET("/status", func(c *touka.Context) {
|
||||
// 在处理器中获取由中间件设置的数据
|
||||
// c.MustGet() 在 key 不存在时会 panic,适用于确定存在的场景
|
||||
requestID := c.MustGet("RequestID").(string)
|
||||
|
||||
// 或者使用安全的 Get
|
||||
// requestID, exists := c.GetString("RequestID")
|
||||
|
||||
c.JSON(http.StatusOK, touka.H{"status": "ok", "request_id": requestID})
|
||||
})
|
||||
|
||||
r.Run(":8080")
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.5. 集成的工具
|
||||
|
||||
##### 日志记录
|
||||
|
||||
Touka 集成了 `reco` 日志库,可以直接在 `Context` 中使用。
|
||||
|
||||
```go
|
||||
r.GET("/log-test", func(c *touka.Context) {
|
||||
userID := "user-abc"
|
||||
c.Infof("用户 %s 访问了 /log-test", userID)
|
||||
|
||||
err := errors.New("一个模拟的错误")
|
||||
if err != nil {
|
||||
c.Errorf("处理请求时发生错误: %v, 用户: %s", err, userID)
|
||||
}
|
||||
|
||||
c.String(http.StatusOK, "日志已记录")
|
||||
})
|
||||
```
|
||||
|
||||
##### HTTP 客户端
|
||||
|
||||
Touka 集成了 `httpc` 客户端,方便发起出站请求。
|
||||
|
||||
```go
|
||||
r.GET("/fetch-data", func(c *touka.Context) {
|
||||
// 使用 Context 携带的 httpc 客户端
|
||||
resp, err := c.GetHTTPC().Get("https://api.github.com/users/WJQSERVER-STUDIO", httpc.WithTimeout(5*time.Second))
|
||||
if err != nil {
|
||||
c.ErrorUseHandle(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 将外部响应直接流式传输给客户端
|
||||
c.SetHeader("Content-Type", resp.Header.Get("Content-Type"))
|
||||
c.WriteStream(resp.Body)
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. 错误处理:统一且强大
|
||||
|
||||
Touka 的一个标志性特性是其统一的错误处理机制。
|
||||
|
||||
#### 4.1. 自定义全局错误处理器
|
||||
|
||||
```go
|
||||
func main() {
|
||||
r := touka.New()
|
||||
|
||||
// 设置一个自定义的全局错误处理器
|
||||
r.SetErrorHandler(func(c *touka.Context, code int, err error) {
|
||||
// 检查是否是客户端断开连接
|
||||
if errors.Is(err, context.Canceled) {
|
||||
return // 不做任何事
|
||||
}
|
||||
|
||||
// 记录详细错误
|
||||
c.GetLogger().Errorf("捕获到错误: code=%d, err=%v, path=%s", code, err, c.Request.URL.Path)
|
||||
|
||||
// 根据错误码返回不同的响应
|
||||
switch code {
|
||||
case http.StatusNotFound:
|
||||
c.JSON(code, touka.H{"error": "您要找的页面去火星了"})
|
||||
case http.StatusMethodNotAllowed:
|
||||
c.JSON(code, touka.H{"error": "不支持的请求方法"})
|
||||
default:
|
||||
c.JSON(code, touka.H{"error": "服务器内部错误"})
|
||||
}
|
||||
})
|
||||
|
||||
// 这个路由不存在,会触发 404
|
||||
// r.GET("/this-route-does-not-exist", ...)
|
||||
|
||||
// 静态文件服务,如果文件不存在,也会被上面的 ErrorHandler 捕获
|
||||
r.StaticDir("/files", "./non-existent-dir")
|
||||
|
||||
r.Run(":8080")
|
||||
}
|
||||
```
|
||||
|
||||
#### 4.2. `errorCapturingResponseWriter` 的魔力
|
||||
|
||||
Touka 如何捕获 `http.FileServer` 的错误?答案是 `errorCapturingResponseWriter`。
|
||||
|
||||
当您使用 `r.StaticDir` 或类似方法时,Touka 不会直接将 `http.FileServer` 作为处理器。相反,它会用一个自定义的 `ResponseWriter` 实现(即 `ecw`)来包装原始的 `ResponseWriter`,然后才调用 `http.FileServer.ServeHTTP`。
|
||||
|
||||
这个包装器会:
|
||||
1. **拦截 `WriteHeader(statusCode)` 调用:** 当 `http.FileServer` 内部决定要写入一个例如 `404 Not Found` 的状态码时,`ecw` 会捕获这个 `statusCode`。
|
||||
2. **判断是否为错误:** 如果 `statusCode >= 400`,`ecw` 会将此视为一个错误信号。
|
||||
3. **阻止原始响应:** `ecw` 会阻止 `http.FileServer` 继续向客户端写入任何内容(包括响应体)。
|
||||
4. **调用全局 `ErrorHandler`:** 最后,`ecw` 会调用您通过 `r.SetErrorHandler` 设置的全局错误处理器,并将捕获到的 `statusCode` 和一个通用错误传递给它。
|
||||
|
||||
这个机制确保了无论是动态 API 的错误还是静态文件服务的错误,都能被统一、优雅地处理,从而提供一致的用户体验。
|
||||
|
||||
---
|
||||
|
||||
### 5. 静态文件服务与嵌入式资源
|
||||
|
||||
#### 5.1. 服务本地文件
|
||||
|
||||
```go
|
||||
// 将 URL /assets/ 映射到本地的 ./static 目录
|
||||
r.StaticDir("/assets", "./static")
|
||||
|
||||
// 将 URL /favicon.ico 映射到本地的 ./static/img/favicon.ico 文件
|
||||
r.StaticFile("/favicon.ico", "./static/img/favicon.ico")
|
||||
```
|
||||
|
||||
#### 5.2. 服务嵌入式资源 (Go 1.16+)
|
||||
|
||||
使用 `go:embed` 可以将静态资源直接编译到二进制文件中,实现真正的单体应用部署。
|
||||
|
||||
```go
|
||||
// main.go
|
||||
package main
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"github.com/infinite-iroha/touka"
|
||||
)
|
||||
|
||||
//go:embed frontend/dist
|
||||
var embeddedFS embed.FS
|
||||
|
||||
func main() {
|
||||
r := touka.New()
|
||||
|
||||
// 创建一个子文件系统,根目录为 embeddedFS 中的 frontend/dist
|
||||
subFS, err := fs.Sub(embeddedFS, "frontend/dist")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// 使用 StaticFS 来服务这个嵌入式文件系统
|
||||
// 所有对 / 的访问都会映射到嵌入的 frontend/dist 目录
|
||||
r.StaticFS("/", http.FS(subFS))
|
||||
|
||||
r.Run(":8080")
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 6. 与标准库的无缝集成
|
||||
|
||||
Touka 提供了适配器,可以轻松使用任何实现了标准 `http.Handler` 或 `http.HandlerFunc` 接口的组件。
|
||||
|
||||
```go
|
||||
import "net/http/pprof"
|
||||
|
||||
// 适配一个标准的 http.HandlerFunc
|
||||
r.GET("/legacy-handler", touka.AdapterStdFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte("这是一个标准的 http.HandlerFunc"))
|
||||
}))
|
||||
|
||||
// 适配一个标准的 http.Handler,例如 pprof
|
||||
debugGroup := r.Group("/debug/pprof")
|
||||
{
|
||||
debugGroup.GET("/", touka.AdapterStdFunc(pprof.Index))
|
||||
debugGroup.GET("/cmdline", touka.AdapterStdFunc(pprof.Cmdline))
|
||||
debugGroup.GET("/profile", touka.AdapterStdFunc(pprof.Profile))
|
||||
// ... 其他 pprof 路由
|
||||
}
|
||||
```
|
||||
|
||||
这使得您可以方便地利用 Go 生态中大量现有的、遵循标准接口的第三方中间件和工具。
|
||||
Loading…
Add table
Add a link
Reference in a new issue