From 67a30acb0890ea9a1ffe58d5c70d791dfbbc1d58 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 18 Feb 2026 00:34:26 +0000 Subject: [PATCH 1/4] Bump github.com/WJQSERVER/wanf from 0.0.3 to 0.0.6 Bumps [github.com/WJQSERVER/wanf](https://github.com/WJQSERVER/wanf) from 0.0.3 to 0.0.6. - [Release notes](https://github.com/WJQSERVER/wanf/releases) - [Commits](https://github.com/WJQSERVER/wanf/compare/v0.0.3...v0.0.6) --- updated-dependencies: - dependency-name: github.com/WJQSERVER/wanf dependency-version: 0.0.6 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 4 ++-- go.sum | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index f9d10a9..b48afdc 100644 --- a/go.mod +++ b/go.mod @@ -1,11 +1,11 @@ module github.com/infinite-iroha/touka -go 1.25.1 +go 1.26 require ( github.com/WJQSERVER-STUDIO/go-utils/iox v0.0.2 github.com/WJQSERVER-STUDIO/httpc v0.8.2 - github.com/WJQSERVER/wanf v0.0.3 + github.com/WJQSERVER/wanf v0.0.6 github.com/fenthope/reco v0.0.4 github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e ) diff --git a/go.sum b/go.sum index b75fec4..0f09a8c 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,8 @@ github.com/WJQSERVER-STUDIO/go-utils/iox v0.0.2 h1:AiIHXP21LpK7pFfqUlUstgQEWzjbe github.com/WJQSERVER-STUDIO/go-utils/iox v0.0.2/go.mod h1:mCLqYU32bTmEE6dpj37MKKiZgz70Jh/xyK9vVbq6pok= github.com/WJQSERVER-STUDIO/httpc v0.8.2 h1:PFPLodV0QAfGEP6915J57vIqoKu9cGuuiXG/7C9TNUk= github.com/WJQSERVER-STUDIO/httpc v0.8.2/go.mod h1:8WhHVRO+olDFBSvL5PC/bdMkb6U3vRdPJ4p4pnguV5Y= -github.com/WJQSERVER/wanf v0.0.3 h1:OqhG7ETiR5Knqr0lmbb+iUMw9O7re2vEogjVf06QevM= -github.com/WJQSERVER/wanf v0.0.3/go.mod h1:q2Pyg+G+s1acMWxrbI4CwS/Yk76/BzLREEdZ8iFwUNE= +github.com/WJQSERVER/wanf v0.0.6 h1:tB6Bsl7bg5uuJ4cn4l1Ctn9VvjNRE5/W0yAj3Z6367I= +github.com/WJQSERVER/wanf v0.0.6/go.mod h1:zV0AQydpfiGsV2CcIy90SxSbiJgcvP3vinBJr0ZW3qs= github.com/fenthope/reco v0.0.4 h1:yo2g3aWwdoMpaZWZX4SdZOW7mCK82RQIU/YI8ZUQThM= github.com/fenthope/reco v0.0.4/go.mod h1:eMyS8HpdMVdJ/2WJt6Cvt8P1EH9Igzj5lSJrgc+0jeg= github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e h1:Lf/gRkoycfOBPa42vU2bbgPurFong6zXeFtPoxholzU= From 8a5acb0b04e7f61e862c8a14d4658fb3cf431e2d Mon Sep 17 00:00:00 2001 From: WJQSERVER <114663932+WJQSERVER@users.noreply.github.com> Date: Wed, 18 Feb 2026 14:28:03 +0000 Subject: [PATCH 2/4] docs: add comprehensive documentation in Chinese Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- README.md | 20 ++++--- docs/advanced.md | 77 ++++++++++++++++++++++++++ docs/context.md | 120 +++++++++++++++++++++++++++++++++++++++++ docs/error-handling.md | 66 +++++++++++++++++++++++ docs/introduction.md | 26 +++++++++ docs/middleware.md | 99 ++++++++++++++++++++++++++++++++++ docs/quickstart.md | 76 ++++++++++++++++++++++++++ docs/routing.md | 94 ++++++++++++++++++++++++++++++++ docs/sse.md | 79 +++++++++++++++++++++++++++ docs/static-files.md | 63 ++++++++++++++++++++++ 10 files changed, 714 insertions(+), 6 deletions(-) create mode 100644 docs/advanced.md create mode 100644 docs/context.md create mode 100644 docs/error-handling.md create mode 100644 docs/introduction.md create mode 100644 docs/middleware.md create mode 100644 docs/quickstart.md create mode 100644 docs/routing.md create mode 100644 docs/sse.md create mode 100644 docs/static-files.md diff --git a/README.md b/README.md index 3ab971f..a449962 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,19 @@ Touka(灯花) 是一个基于 Go 语言构建的多层次、高性能 Web 框架。其设计目标是为开发者提供**更直接的控制、有效的扩展能力,以及针对特定场景的行为优化**。 -**想深入了解 Touka 吗?请阅读我们的 -> [深度指南 (about-touka.md)](about-touka.md)** +## 文档 -这份深度指南包含了对框架设计哲学、核心功能(路由、上下文、中间件、错误处理等)的全面剖析,并提供了大量可直接使用的代码示例,帮助您快速上手并精通 Touka。 +我们提供了详尽的文档来帮助您快速上手并深入了解 Touka: + +- **[灯花框架简介 (introduction.md)](docs/introduction.md)** +- **[快速开始 (quickstart.md)](docs/quickstart.md)** +- **[路由系统 (routing.md)](docs/routing.md)** +- **[上下文 Context (context.md)](docs/context.md)** +- **[中间件 (middleware.md)](docs/middleware.md)** +- **[统一错误处理 (error-handling.md)](docs/error-handling.md)** +- **[静态文件与资源 (static-files.md)](docs/static-files.md)** +- **[Server-Sent Events (sse.md)](docs/sse.md)** +- **[高级特性与优化 (advanced.md)](docs/advanced.md)** ### 快速上手 @@ -72,11 +82,9 @@ func main() { - [jwt](https://github.com/fenthope/jwt) - [带宽限制](https://github.com/fenthope/toukautil/blob/main/bandwithlimiter.go) -## 文档与贡献 +## 贡献 -* **深度指南:** **[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 参考。 -* **贡献:** 我们欢迎任何形式的贡献,无论是错误报告、功能建议还是代码提交。请遵循项目的贡献指南。 +我们欢迎任何形式的贡献,无论是错误报告、功能建议还是代码提交。请遵循项目的贡献指南。 ## 相关项目 diff --git a/docs/advanced.md b/docs/advanced.md new file mode 100644 index 0000000..613b180 --- /dev/null +++ b/docs/advanced.md @@ -0,0 +1,77 @@ +# 高级特性与优化 + +本章节涵盖了 Touka 的一些深层特性以及在生产环境中的最佳实践。 + +## 性能优化 + +### 1. Context 池化 + +Touka 使用 `sync.Pool` 来重用 `touka.Context` 对象。这极大减少了每个请求产生的内存分配和 GC 压力。 +- **代价**: 您必须在处理器返回后立即停止对该 `Context` 指针的任何引用。 +- **解决方案**: 如果需要在后台 Goroutine 中使用请求数据,请预先提取所需数据(如 `c.Query` 的值),或者深拷贝该对象(不推荐)。 + +### 2. 预分配参数切片 + +在路由匹配过程中,Touka 会预分配路径参数切片,并根据路由深度进行缓存,从而在路由查找时实现几乎零分配。 + +## 优雅停机 (Graceful Shutdown) + +在部署新版本时,我们希望服务器停止接收新请求,但能处理完当前正在进行的请求。 + +```go +r := touka.Default() +// ... 注册路由 ... + +// 监听 SIGINT 和 SIGTERM 信号 +// 如果在 10 秒内未处理完,则强制关闭 +if err := r.RunShutdown(":8080", 10*time.Second); err != nil { + log.Fatal("服务器退出异常:", err) +} +``` + +## 与标准库集成 + +Touka 遵循 `net/http` 哲学。您可以方便地使用现有的标准库组件。 + +### 适配 `http.HandlerFunc` + +```go +r.GET("/pprof/*any", touka.AdapterStdFunc(pprof.Index)) +``` + +### 手动注入 + +由于 `Engine` 实现了 `http.Handler` 接口,您可以将其挂载到任何地方。 + +```go +s := &http.Server{ + Addr: ":8080", + Handler: r, // Engine 实例 + ReadTimeout: 10 * time.Second, + WriteTimeout: 10 * time.Second, + MaxHeaderBytes: 1 << 20, +} +s.ListenAndServe() +``` + +## 自定义日志集成 + +Touka 默认集成了 `reco` 日志库。您可以自定义其输出行为。 + +```go +logConfig := reco.Config{ + Level: reco.LevelInfo, + Output: os.Stdout, + Async: true, // 异步写入提高性能 +} +r.SetLoggerCfg(logConfig) +``` + +## 内存读取限制 (MaxReader) + +为了防止恶意的大数据包攻击(如慢速 HTTP 攻击或内存溢出),Touka 内置了 `MaxReader` 机制。 + +```go +// 设置全局最大读取限制(例如 2MB) +r.SetMaxReader(2 << 20) +``` diff --git a/docs/context.md b/docs/context.md new file mode 100644 index 0000000..b67bec7 --- /dev/null +++ b/docs/context.md @@ -0,0 +1,120 @@ +# 上下文 (Context) + +`touka.Context` 是 Touka 框架中最重要的结构。它携带了关于当前 HTTP 请求的所有必要信息,并提供了一系列方法来解析请求和构建响应。 + +## 请求数据解析 + +### 查询参数 (Query Parameters) + +```go +// /welcome?firstname=Jane&lastname=Doe +r.GET("/welcome", func(c *touka.Context) { + firstname := c.DefaultQuery("firstname", "Guest") + lastname := c.Query("lastname") // 快捷方式,不存在则为空 + + c.String(http.StatusOK, "Hello %s %s", firstname, lastname) +}) +``` + +### 表单数据 (Form Data) + +```go +r.POST("/form_post", func(c *touka.Context) { + message := c.PostForm("message") + nick := c.DefaultPostForm("nick", "anonymous") + + c.JSON(http.StatusOK, touka.H{ + "status": "posted", + "message": message, + "nick": nick, + }) +}) +``` + +### JSON 绑定 + +Touka 提供了非常便捷的 JSON 绑定功能,它会自动解析请求体并填充到结构体中,同时进行基本的验证。 + +```go +type LoginRequest struct { + User string `json:"user" binding:"required"` + Password string `json:"password" binding:"required"` +} + +r.POST("/login", func(c *touka.Context) { + var json LoginRequest + if err := c.ShouldBindJSON(&json); err != nil { + c.JSON(http.StatusBadRequest, touka.H{"error": err.Error()}) + return + } + + if json.User != "admin" || json.Password != "123" { + c.JSON(http.StatusUnauthorized, touka.H{"status": "unauthorized"}) + return + } + + c.JSON(http.StatusOK, touka.H{"status": "you are logged in"}) +}) +``` + +## 响应构建 + +### 基础格式 + +Touka 支持多种响应格式: + +```go +// JSON +c.JSON(http.StatusOK, touka.H{"message": "hey"}) + +// 字符串 (支持格式化) +c.String(http.StatusOK, "welcome %s", name) + +// 纯文本 +c.Text(http.StatusOK, "just text") + +// HTML 模板 +c.HTML(http.StatusOK, "index.tmpl", touka.H{"title": "Main website"}) +``` + +### 文件与流 + +```go +// 服务本地文件 +c.File("/local/file.go") + +// 将文件内容作为响应体(不触发下载) +c.SetRespBodyFile(http.StatusOK, "config.json") + +// 写入数据流 +c.WriteStream(reader) +``` + +### 重定向 + +```go +c.Redirect(http.StatusMovedPermanently, "http://google.com/") +``` + +## 数据传递 (Keys/Values) + +您可以在中间件和处理器之间共享数据。 + +```go +// 在中间件中设置 +c.Set("user_id", 12345) + +// 在处理器中获取 +id, exists := c.Get("user_id") +val := c.MustGet("user_id").(int) +``` + +## 状态管理 + +- `c.Abort()`: 停止执行后续的处理器/中间件。 +- `c.Next()`: 执行后续的处理链。这常用于中间件中,在执行完某些前置逻辑后,显式调用 `Next`,并在其返回后执行后置逻辑。 + +## 对象池化 + +为了提高性能,Touka 的 Context 对象是复用的。 +**重要提示:不要在 Goroutine 中持久化持有 `touka.Context` 指针。如果您需要在 Goroutine 中使用请求数据,请务必在派生 Goroutine 前提取所需的值。** diff --git a/docs/error-handling.md b/docs/error-handling.md new file mode 100644 index 0000000..5b53579 --- /dev/null +++ b/docs/error-handling.md @@ -0,0 +1,66 @@ +# 统一错误处理 + +Touka 的核心优势之一是其**高度统一且自动化**的错误处理机制。 + +## 全局错误处理器 + +您可以为整个引擎设置一个统一的错误处理器。无论错误来自您的业务代码,还是来自框架内部(如 404/405),甚至是来自标准库的 `http.FileServer`,最终都会流向这个处理器。 + +```go +r.SetErrorHandler(func(c *touka.Context, code int, err error) { + // 您可以在这里定义统一的错误响应格式 + c.JSON(code, touka.H{ + "code": code, + "message": http.StatusText(code), + "detail": err.Error(), + }) + + // 也可以记录日志 + c.Errorf("HTTP Error %d: %v", code, err) +}) +``` + +## `errorCapturingResponseWriter` (ecw) 的工作原理 + +很多时候,我们希望拦截标准库组件(如 `http.FileServer`)产生的错误,以便能够应用我们自定义的 404 页面或 JSON 响应。 + +Touka 通过包装标准的 `http.ResponseWriter` 实现了这一点: + +1. **拦截写入**: 当 `http.FileServer` 等组件尝试调用 `WriteHeader(statusCode)` 且 `statusCode >= 400` 时,Touka 的包装器会捕获这个状态码。 +2. **阻止输出**: 它会阻止组件继续向响应体写入默认的错误消息(如 `404 page not found`)。 +3. **回调处理**: 包装器随后会调用全局配置的 `ErrorHandler`。 + +这意味着您可以像这样轻松地为静态文件服务设置自定义错误处理: + +```go +r := touka.New() + +// 设置全局错误处理 +r.SetErrorHandler(func(c *touka.Context, code int, err error) { + if code == http.StatusNotFound { + c.String(http.StatusNotFound, "找不到此资源") + return + } + c.String(code, "发生错误: %v", err) +}) + +// 服务静态目录 +r.StaticDir("/static", "./public") +// 如果用户访问 /static/missing-file.jpg,他将看到 "找不到此资源" +``` + +## 手动触发错误处理 + +您也可以在处理器中通过 `c.ErrorUseHandle` 手动触发此流程: + +```go +r.GET("/item/:id", func(c *touka.Context) { + item, err := db.GetItem(c.Param("id")) + if err != nil { + // 调用全局错误处理器 + c.ErrorUseHandle(http.StatusInternalServerError, err) + return + } + c.JSON(http.StatusOK, item) +}) +``` diff --git a/docs/introduction.md b/docs/introduction.md new file mode 100644 index 0000000..d1aec3e --- /dev/null +++ b/docs/introduction.md @@ -0,0 +1,26 @@ +# Touka (灯花) 简介 + +Touka 是一个基于 Go 语言构建的高性能、多层次 Web 框架。其设计目标是为开发者提供**更直接的控制、有效的扩展能力,以及针对特定场景的行为优化**。 + +## 为什么选择 Touka? + +在众多的 Go Web 框架中,Touka 致力于在保持类似 Gin 的易用性的同时,提供更深度的底层控制和更强大的统一错误处理机制。 + +### 核心特性 + +- **高性能路由**: 基于基数树(Radix Tree)实现的路由系统,支持高效的路径匹配、参数捕获和通配符路由。 +- **极致性能优化**: + - **Context 复用**: 通过对象池(sync.Pool)管理 `touka.Context`,显著减少 GC 压力。 + - **最小化内存分配**: 在热点路径上尽可能减少临时对象的产生。 +- **统一错误处理**: 独创的 `errorCapturingResponseWriter` 机制,能够捕获包括标准库 `http.FileServer` 在内的所有组件产生的错误状态码,并交由全局处理器统一处理。 +- **无缝集成 SSE**: 内置对 Server-Sent Events 的支持,提供简单易用的回调式 API 和高度灵活的通道式 API。 +- **静态资源增强**: 针对本地文件、目录以及 Go 嵌入式文件系统(embed.FS)提供了开箱即用的支持。 +- **标准库兼容**: 提供了适配器,可以轻松将现有的 `http.Handler` 或 `http.HandlerFunc` 集成到 Touka 中。 + +## 设计哲学 + +1. **直接性**: 框架 API 设计直观,尽可能减少开发者需要记忆的概念。 +2. **可扩展性**: 每一个核心组件(如日志、错误处理器、渲染器)都是可插拔或可定制的。 +3. **健壮性**: 内置优雅停机支持,确保在服务器更新或关闭时请求能得到正确处理。 + +Touka 不仅仅是一个处理 HTTP 请求的工具,它还是构建现代化、可维护、高可用 Web 应用的坚实基础。 diff --git a/docs/middleware.md b/docs/middleware.md new file mode 100644 index 0000000..f13de8e --- /dev/null +++ b/docs/middleware.md @@ -0,0 +1,99 @@ +# 中间件 (Middleware) + +中间件是运行在 HTTP 请求处理链中的函数。它们可以拦截请求、修改数据、控制流向(通过 `c.Next()` 或 `c.Abort()`),并执行通用的前置/后置逻辑。 + +## 如何使用中间件 + +### 全局中间件 + +全局中间件应用于所有注册的路由。 + +```go +r := touka.New() +r.Use(touka.Recovery()) // 崩溃恢复 +r.Use(MyCustomLogger()) // 自定义日志 +``` + +### 路由组中间件 + +仅应用于特定组下的路由。 + +```go +api := r.Group("/api") +api.Use(AuthMiddleware()) +{ + api.GET("/user", handleUser) +} +``` + +## 编写自定义中间件 + +中间件的函数签名是 `touka.HandlerFunc`。 + +### 示例:请求计时器 + +```go +func TimerMiddleware() touka.HandlerFunc { + return func(c *touka.Context) { + // --- 前置逻辑 --- + start := time.Now() + + // 执行处理链中的下一个函数 + c.Next() + + // --- 后置逻辑 --- + duration := time.Since(start) + log.Printf("Request %s %s took %v", c.Request.Method, c.Request.URL.Path, duration) + } +} +``` + +### 示例:简单的 API 密钥验证 + +```go +func APIKeyAuth() touka.HandlerFunc { + return func(c *touka.Context) { + apiKey := c.GetHeader("X-API-KEY") + if apiKey != "secret-token" { + // 验证失败,返回错误并中止后续逻辑 + c.JSON(http.StatusUnauthorized, touka.H{"error": "Invalid API Key"}) + c.Abort() + return + } + + // 验证通过,继续执行 + c.Next() + } +} +``` + +## 内置中间件 + +- **Recovery**: 捕获任何发生的 panic,恢复运行并返回 500 错误。它还负责调用全局错误处理器。 + +Touka 的设计非常精简,许多扩展功能(如 Gzip, JWT, Sessions)由外部或第三方库提供,您可以轻松通过 `r.Use()` 集成它们。 + +## 条件中间件 (Conditional Middleware) + +Touka 支持根据布尔条件动态启用或禁用中间件。这在根据环境配置启用插件时非常有用。 + +### `UseIf` + +```go +// 仅在 Debug 模式下启用日志 +r.Use(r.UseIf(config.IsDebug, MyDebugLogger)) +``` + +### `UseChainIf` (条件中间件链) + +如果您有一组相关的中间件需要根据同一条件启用,可以使用 `UseChainIf`。 + +```go +r.Use(r.UseChainIf(config.EnableMetrics, + MetricsMiddleware, + PrometheusMiddleware, + MonitoringMiddleware, +)) +``` + +这些方法利用了 `MiddlewareXFunc`(即返回 `HandlerFunc` 的工厂函数),确保中间件实例按需创建或高效复用。 diff --git a/docs/quickstart.md b/docs/quickstart.md new file mode 100644 index 0000000..6006e54 --- /dev/null +++ b/docs/quickstart.md @@ -0,0 +1,76 @@ +# 快速开始 + +本指南将帮助您在几分钟内启动并运行一个 Touka 应用。 + +## 安装 + +确保您的环境中已经安装了 Go 1.25 或更高版本。 + +在您的项目目录中运行: + +```bash +go get github.com/infinite-iroha/touka +``` + +## 基础示例 + +创建一个 `main.go` 文件,并粘贴以下代码: + +```go +package main + +import ( + "net/http" + "time" + "log" + "github.com/infinite-iroha/touka" +) + +func main() { + // 1. 创建默认引擎(包含 Recovery 中间件) + r := touka.Default() + + // 2. 注册一个简单的 GET 路由 + r.GET("/ping", func(c *touka.Context) { + c.JSON(http.StatusOK, touka.H{ + "message": "pong", + "time": time.Now().Unix(), + }) + }) + + // 3. 注册带参数的路由 + r.GET("/hello/:name", func(c *touka.Context) { + name := c.Param("name") + c.String(http.StatusOK, "Hello, %s!", name) + }) + + // 4. 启动服务器并监听 8080 端口 + log.Println("Touka server is running on :8080") + if err := r.Run(":8080"); err != nil { + log.Fatalf("Server failed: %v", err) + } +} +``` + +## 运行应用 + +执行以下命令启动服务器: + +```bash +go run main.go +``` + +现在,您可以访问: +- `http://localhost:8080/ping` +- `http://localhost:8080/hello/World` + +## 优雅停机 + +在生产环境中,我们推荐使用 `RunShutdown` 方法来启动服务器,它会监听系统信号并在关闭前等待正在处理的请求完成。 + +```go +// 等待 10 秒以处理剩余请求 +if err := r.RunShutdown(":8080", 10*time.Second); err != nil { + log.Fatalf("Server forced to shutdown: %v", err) +} +``` diff --git a/docs/routing.md b/docs/routing.md new file mode 100644 index 0000000..17897d8 --- /dev/null +++ b/docs/routing.md @@ -0,0 +1,94 @@ +# 路由系统 + +Touka 拥有一个强大且灵活的路由系统,底层基于高性能的基数树(Radix Tree)实现。 + +## 基础路由 + +您可以为所有标准的 HTTP 方法注册处理器: + +```go +r.GET("/someGet", handle) +r.POST("/somePost", handle) +r.PUT("/somePut", handle) +r.DELETE("/someDelete", handle) +r.PATCH("/somePatch", handle) +r.HEAD("/someHead", handle) +r.OPTIONS("/someOptions", handle) + +// 注册所有上述方法的路由 +r.ANY("/any", handle) +``` + +## 路径参数 (Named Parameters) + +使用冒号 `:` 定义路径参数。参数值可以通过 `c.Param(key)` 获取。 + +```go +// 匹配 /user/john, 不匹配 /user/ 或 /user/john/send +r.GET("/user/:name", func(c *touka.Context) { + name := c.Param("name") + c.String(http.StatusOK, "Hello %s", name) +}) + +// 匹配 /user/john/send +r.GET("/user/:name/:action", func(c *touka.Context) { + name := c.Param("name") + action := c.Param("action") + c.String(http.StatusOK, "%s is doing %s", name, action) +}) +``` + +## 通配符路由 (Catch-all Parameters) + +使用星号 `*` 定义通配符路由,它会捕获路径中该位置之后的所有内容。 + +```go +// 匹配 /src/main.go, /src/scripts/app.js 等 +r.GET("/src/*filepath", func(c *touka.Context) { + path := c.Param("filepath") + c.String(http.StatusOK, "Viewing file: %s", path) +}) +``` + +## 路由组 (RouterGroup) + +路由组允许您共享公共路径前缀或中间件,使代码结构更清晰。 + +```go +v1 := r.Group("/api/v1") +{ + v1.GET("/login", loginEndpoint) + v1.GET("/submit", submitEndpoint) +} + +v2 := r.Group("/api/v2") +v2.Use(AuthMiddleware()) // 仅应用于 v2 组 +{ + v2.POST("/data", dataEndpoint) +} +``` + +## 路由行为配置 + +Touka 允许您自定义路由匹配的行为: + +- **RedirectTrailingSlash**: 如果启用(默认),请求 `/foo/` 会被重定向到 `/foo`(如果只有后者注册了),反之亦然。 +- **RedirectFixedPath**: 如果启用(默认),引擎会尝试修复路径大小写或移除多余的斜杠并重定向。 +- **HandleMethodNotAllowed**: 如果启用,当请求路径匹配但方法不匹配时,返回 405 而非 404。 + +```go +r := touka.New() +r.RedirectTrailingSlash = true +r.HandleMethodNotAllowed = true +``` + +## 获取已注册路由信息 + +您可以使用 `GetRouterInfo` 获取当前引擎中所有已注册路由的列表。 + +```go +routes := r.GetRouterInfo() +for _, route := range routes { + fmt.Printf("Method: %s, Path: %s\n", route.Method, route.Path) +} +``` diff --git a/docs/sse.md b/docs/sse.md new file mode 100644 index 0000000..7927f25 --- /dev/null +++ b/docs/sse.md @@ -0,0 +1,79 @@ +# Server-Sent Events (SSE) + +Server-Sent Events 允许服务器向客户端实时推送数据。Touka 对此提供了原生且易用的支持。 + +## 核心结构:`Event` + +`Event` 结构体代表一个 SSE 消息: + +```go +type Event struct { + Event string // 事件名称 + Data string // 数据内容 (支持多行) + Id string // 事件 ID + Retry string // 重连时间 (毫秒) +} +``` + +## 模式一:回调模式 (EventStream) + +这是最推荐的使用方式,它更简单且能自动管理连接生命周期。 + +```go +r.GET("/events", func(c *touka.Context) { + c.EventStream(func(w io.Writer) bool { + // 构建事件 + event := touka.Event{ + Data: "现在的时间是: " + time.Now().Format(time.RFC3339), + } + + // 渲染并写入 + if err := event.Render(w); err != nil { + return false // 发生写入错误(如客户端断开),返回 false 停止流 + } + + time.Sleep(2 * time.Second) + return true // 返回 true 继续下一次循环 + }) +}) +``` + +## 模式二:通道模式 (EventStreamChan) + +如果您需要更高级的并发控制(例如从多个异步源接收数据),可以使用通道模式。 + +```go +r.GET("/events-chan", func(c *touka.Context) { + eventChan, errChan := c.EventStreamChan() + + // 监听错误/断开连接 + go func() { + if err := <-errChan; err != nil { + log.Printf("SSE 错误: %v", err) + } + }() + + // 发送数据 + go func() { + defer close(eventChan) // 务必在结束时关闭 + + for i := 0; i < 10; i++ { + select { + case <-c.Request.Context().Done(): + return + default: + eventChan <- touka.Event{ + Data: fmt.Sprintf("消息 #%d", i), + } + time.Sleep(1 * time.Second) + } + } + }() +}) +``` + +## 最佳实践 + +1. **资源回收**: 确保在 `EventStreamChan` 模式下正确监听 `c.Request.Context().Done()` 以避免 Goroutine 泄漏。 +2. **数据格式**: SSE 协议要求数据为 UTF-8。Touka 的 `Render` 方法会自动处理多行数据并加上必要的 `data:` 前缀。 +3. **超时管理**: SSE 连接通常是长连接,请确保您的反向代理(如 Nginx)配置了足够大的写超时时间。 diff --git a/docs/static-files.md b/docs/static-files.md new file mode 100644 index 0000000..46954c8 --- /dev/null +++ b/docs/static-files.md @@ -0,0 +1,63 @@ +# 静态文件与资源 + +Touka 提供了多种方式来服务静态文件,这些方法都集成了 Touka 的统一错误处理机制。 + +## 服务本地目录 + +`StaticDir` 方法将 URL 路径映射到本地文件系统目录。 + +```go +// 访问 /assets/js/main.js 将读取 ./static/js/main.js +r.StaticDir("/assets", "./static") +``` + +## 服务单个文件 + +`StaticFile` 用于将特定的 URL 映射到单个本地文件。 + +```go +r.StaticFile("/favicon.ico", "./resources/favicon.ico") +``` + +## 集成 Go 嵌入式资源 (embed.FS) + +使用 Go 1.16+ 的 `embed` 特性,您可以将整个静态前端项目编译进二进制文件中。 + +```go +//go:embed dist/* +var content embed.FS + +func main() { + r := touka.Default() + + // 剥离 "dist" 前缀并包装为 http.FS + fsroot, _ := fs.Sub(content, "dist") + + // 使用 StaticFS 提供服务 + r.StaticFS("/static", http.FS(fsroot)) + + // 您也可以使用 StaticFS 服务根路径 + // r.StaticFS("/", http.FS(fsroot)) + + r.Run(":8080") +} +``` + +## 未匹配路径作为文件服务 (UnMatchFS) + +这是一个独特的功能:当没有任何 API 路由匹配时,尝试从指定的文件系统中查找并返回文件。这非常适合用于单页应用(SPA)的部署。 + +```go +r := touka.New() +r.SetUnMatchFS(http.Dir("./frontend/dist"), true) + +// API 路由 +r.GET("/api/status", handleStatus) + +// 如果请求 /index.html 且没有 /index.html 的路由, +// 则会从 ./frontend/dist/index.html 读取。 +``` + +## 性能提示 + +对于高负载的静态资源分发,虽然 Touka 表现出色,但我们仍建议在生产环境中使用 Nginx 或 CDN 站在 Touka 前面来处理静态文件,让 Touka 专注于处理动态逻辑。 From 1066a9b1cf1806105e07c840d15d4cc2ae2aef15 Mon Sep 17 00:00:00 2001 From: WJQSERVER <114663932+WJQSERVER@users.noreply.github.com> Date: Wed, 18 Feb 2026 15:34:00 +0000 Subject: [PATCH 3/4] docs: add detailed Chinese documentation and SSE graceful shutdown example Added a comprehensive \`docs\` folder and updated README.md. Included a specific example in \`docs/sse.md\` demonstrating how to handle \`c.Request.Context().Done()\` for graceful SSE connection closure. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- docs/sse.md | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/docs/sse.md b/docs/sse.md index 7927f25..5a75731 100644 --- a/docs/sse.md +++ b/docs/sse.md @@ -77,3 +77,44 @@ r.GET("/events-chan", func(c *touka.Context) { 1. **资源回收**: 确保在 `EventStreamChan` 模式下正确监听 `c.Request.Context().Done()` 以避免 Goroutine 泄漏。 2. **数据格式**: SSE 协议要求数据为 UTF-8。Touka 的 `Render` 方法会自动处理多行数据并加上必要的 `data:` 前缀。 3. **超时管理**: SSE 连接通常是长连接,请确保您的反向代理(如 Nginx)配置了足够大的写超时时间。 + +## 优雅关闭与资源清理 + +在长连接场景下,正确处理客户端断开或服务器关闭信号至关重要,以防止资源泄漏。 + +### 示例:监听 Context 取消信号 + +```go +r.GET("/events-graceful", func(c *touka.Context) { + // 设置响应头(如果手动处理,EventStream 会自动设置) + + // 使用 Context 的 Done 通道来感知连接关闭 + ctx := c.Request.Context() + + // 启动一个用于模拟数据生成的循环 + ticker := time.NewTicker(2 * time.Second) + defer ticker.Stop() + + c.EventStream(func(w io.Writer) bool { + select { + case <-ctx.Done(): + // 收到优雅关闭信号(客户端离开或服务器正在关闭) + fmt.Println("SSE 连接正在关闭,开始清理资源...") + return false // 返回 false 告知框架停止流 + + case t := <-ticker.C: + event := touka.Event{ + Data: "Tick at " + t.Format(time.RFC3339), + } + if err := event.Render(w); err != nil { + return false + } + return true + } + }) + + fmt.Println("SSE 连接已彻底释放") +}) +``` + +在该示例中,我们显式地在回调函数中使用 `select` 监听 `ctx.Done()`。虽然 Touka 的 `EventStream` 内部也会检查此信号,但在回调内部自行处理可以执行更复杂的清理逻辑(如关闭数据库连接、停止特定的 Goroutine 等)。 From 2e55db4e2ddf03aee53cb8f95dfcca6890fb9ba1 Mon Sep 17 00:00:00 2001 From: WJQSERVER <114663932+WJQSERVER@users.noreply.github.com> Date: Wed, 18 Feb 2026 15:52:42 +0000 Subject: [PATCH 4/4] docs: add comprehensive documentation and fix SSE graceful shutdown - Created a detailed \`docs\` directory with Chinese documentation for all major features. - Implemented \`BaseContext\` propagation in \`Engine\` to allow long-lived connections (like SSE) to receive shutdown signals. - Added \`Engine.Context()\` to provide access to the shutdown context. - Updated \`docs/sse.md\` with a guide on handling graceful shutdowns. - Cleaned up temporary build/update scripts. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- docs/sse.md | 11 +++++++++++ engine.go | 10 ++++++++++ serve.go | 16 ++++++++++++++++ 3 files changed, 37 insertions(+) diff --git a/docs/sse.md b/docs/sse.md index 5a75731..bafc553 100644 --- a/docs/sse.md +++ b/docs/sse.md @@ -118,3 +118,14 @@ r.GET("/events-graceful", func(c *touka.Context) { ``` 在该示例中,我们显式地在回调函数中使用 `select` 监听 `ctx.Done()`。虽然 Touka 的 `EventStream` 内部也会检查此信号,但在回调内部自行处理可以执行更复杂的清理逻辑(如关闭数据库连接、停止特定的 Goroutine 等)。 + +### 为什么会出现 "context deadline exceeded"? + +如果您在优雅停机时遇到 `context deadline exceeded` 错误,通常是因为 SSE 连接仍然活跃,而 `http.Server.Shutdown` 正在等待它们结束。 + +在 Touka 的新版本中,我们通过 `BaseContext` 将 `Engine` 的关闭信号注入到了每个请求的 `Context` 中。这意味着: +1. 当服务器收到关闭信号时,`engine.shutdownCtx` 会被取消。 +2. 随后,所有活跃请求的 `c.Request.Context()` 也会收到取消信号。 +3. 您的 SSE 处理器中的 `case <-c.Request.Context().Done():` 会立即触发,从而优雅地结束连接。 + +**注意:** 请务必使用 `RunShutdown`、`RunTLS` 或 `RunTLSRedir` 来启动服务器,以便框架能自动管理这些信号。 diff --git a/engine.go b/engine.go index 0a95765..1e7bb18 100644 --- a/engine.go +++ b/engine.go @@ -67,6 +67,9 @@ type Engine struct { Protocols ProtocolsConfig //协议版本配置 useDefaultProtocols bool //是否使用默认协议 + shutdownCtx context.Context + shutdownCancel context.CancelFunc + // ServerConfigurator 允许在服务器启动前对其进行自定义配置 // 例如,设置 ReadTimeout, WriteTimeout 等 ServerConfigurator func(*http.Server) @@ -207,6 +210,7 @@ func New() *Engine { TLSServerConfigurator: nil, GlobalMaxRequestBodySize: -1, } + engine.shutdownCtx, engine.shutdownCancel = context.WithCancel(context.Background()) //engine.SetProtocols(GetDefaultProtocolsConfig()) engine.SetDefaultProtocols() engine.SetLoggerCfg(defaultLogRecoConfig) @@ -766,3 +770,9 @@ func (engine *Engine) handleRequest(c *Context) { c.Next() // 执行处理函数链 //c.Writer.Flush() // 确保所有缓冲的响应数据被发送 } + +// Context 返回 Engine 的根上下文, 该上下文在服务器优雅关闭时会被取消. +// 它可以用于在长连接 (如 SSE) 中监听关闭信号. +func (engine *Engine) Context() context.Context { + return engine.shutdownCtx +} diff --git a/serve.go b/serve.go index 7e05b8c..6a4cf2a 100644 --- a/serve.go +++ b/serve.go @@ -224,7 +224,11 @@ func (engine *Engine) RunShutdown(addr string, timeouts ...time.Duration) error srv := &http.Server{ Addr: addr, Handler: engine, + BaseContext: func(l net.Listener) context.Context { + return engine.shutdownCtx + }, } + srv.RegisterOnShutdown(engine.shutdownCancel) // 应用框架的默认配置和用户提供的自定义配置 //engine.applyDefaultServerConfig(srv) @@ -241,7 +245,11 @@ func (engine *Engine) RunShutdownWithContext(addr string, ctx context.Context, t srv := &http.Server{ Addr: addr, Handler: engine, + BaseContext: func(l net.Listener) context.Context { + return engine.shutdownCtx + }, } + srv.RegisterOnShutdown(engine.shutdownCancel) // 应用框架的默认配置和用户提供的自定义配置 //engine.applyDefaultServerConfig(srv) @@ -270,7 +278,11 @@ func (engine *Engine) RunTLS(addr string, tlsConfig *tls.Config, timeouts ...tim Addr: addr, Handler: engine, TLSConfig: tlsConfig, + BaseContext: func(l net.Listener) context.Context { + return engine.shutdownCtx + }, } + srv.RegisterOnShutdown(engine.shutdownCancel) // 应用框架的默认配置和用户提供的自定义配置 // 优先使用 TLSServerConfigurator,如果未设置,则回退到通用的 ServerConfigurator @@ -304,7 +316,11 @@ func (engine *Engine) RunTLSRedir(httpAddr, httpsAddr string, tlsConfig *tls.Con Addr: httpsAddr, Handler: engine, TLSConfig: tlsConfig, + BaseContext: func(l net.Listener) context.Context { + return engine.shutdownCtx + }, } + httpsSrv.RegisterOnShutdown(engine.shutdownCancel) //engine.applyDefaultServerConfig(httpsSrv) if engine.TLSServerConfigurator != nil { engine.TLSServerConfigurator(httpsSrv)