From 8af515059a2e06cf1b82db314ccf1fe3ab794ff8 Mon Sep 17 00:00:00 2001 From: WJQSERVER <114663932+WJQSERVER@users.noreply.github.com> Date: Sun, 22 Mar 2026 09:27:20 +0800 Subject: [PATCH 1/2] =?UTF-8?q?docs:=20=E4=BF=AE=E5=A4=8D=E6=96=87?= =?UTF-8?q?=E6=A1=A3=E4=B8=AD=E7=9A=84API=E6=96=B9=E6=B3=95=E5=90=8D?= =?UTF-8?q?=E5=92=8C=E5=8F=82=E6=95=B0=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 更新quickstart.md中的Go版本要求为1.26 - 修复routing.md中使用setter方法而不是直接属性赋值 - 修复middleware.md中GetHeader为GetReqHeader - 更新context.md移除未实现的binding标签 - 修复static-files.md中SetUnMatchFS的参数签名 - 修复advanced.md中SetMaxReader为SetGlobalMaxRequestBodySize --- docs/advanced.md | 2 +- docs/context.md | 6 +++--- docs/middleware.md | 2 +- docs/quickstart.md | 2 +- docs/routing.md | 4 ++-- docs/static-files.md | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/advanced.md b/docs/advanced.md index 613b180..345103d 100644 --- a/docs/advanced.md +++ b/docs/advanced.md @@ -73,5 +73,5 @@ r.SetLoggerCfg(logConfig) ```go // 设置全局最大读取限制(例如 2MB) -r.SetMaxReader(2 << 20) +r.SetGlobalMaxRequestBodySize(2 << 20) ``` diff --git a/docs/context.md b/docs/context.md index b67bec7..9dc962d 100644 --- a/docs/context.md +++ b/docs/context.md @@ -33,12 +33,12 @@ r.POST("/form_post", func(c *touka.Context) { ### JSON 绑定 -Touka 提供了非常便捷的 JSON 绑定功能,它会自动解析请求体并填充到结构体中,同时进行基本的验证。 +Touka 提供了非常便捷的 JSON 绑定功能,它会自动解析请求体并填充到结构体中。 ```go type LoginRequest struct { - User string `json:"user" binding:"required"` - Password string `json:"password" binding:"required"` + User string `json:"user"` + Password string `json:"password"` } r.POST("/login", func(c *touka.Context) { diff --git a/docs/middleware.md b/docs/middleware.md index f13de8e..a222437 100644 --- a/docs/middleware.md +++ b/docs/middleware.md @@ -53,7 +53,7 @@ func TimerMiddleware() touka.HandlerFunc { ```go func APIKeyAuth() touka.HandlerFunc { return func(c *touka.Context) { - apiKey := c.GetHeader("X-API-KEY") + apiKey := c.GetReqHeader("X-API-KEY") if apiKey != "secret-token" { // 验证失败,返回错误并中止后续逻辑 c.JSON(http.StatusUnauthorized, touka.H{"error": "Invalid API Key"}) diff --git a/docs/quickstart.md b/docs/quickstart.md index 6006e54..94f7433 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -4,7 +4,7 @@ ## 安装 -确保您的环境中已经安装了 Go 1.25 或更高版本。 +确保您的环境中已经安装了 Go 1.26 或更高版本。 在您的项目目录中运行: diff --git a/docs/routing.md b/docs/routing.md index 17897d8..039341f 100644 --- a/docs/routing.md +++ b/docs/routing.md @@ -78,8 +78,8 @@ Touka 允许您自定义路由匹配的行为: ```go r := touka.New() -r.RedirectTrailingSlash = true -r.HandleMethodNotAllowed = true +r.SetRedirectTrailingSlash(true) +r.SetHandleMethodNotAllowed(true) ``` ## 获取已注册路由信息 diff --git a/docs/static-files.md b/docs/static-files.md index 46954c8..a2138cd 100644 --- a/docs/static-files.md +++ b/docs/static-files.md @@ -49,7 +49,7 @@ func main() { ```go r := touka.New() -r.SetUnMatchFS(http.Dir("./frontend/dist"), true) +r.SetUnMatchFS(http.Dir("./frontend/dist")) // API 路由 r.GET("/api/status", handleStatus) From 7b8c0d7dcb17f5577b37c82523dc63c57bda346f Mon Sep 17 00:00:00 2001 From: WJQSERVER <114663932+WJQSERVER@users.noreply.github.com> Date: Sun, 22 Mar 2026 10:13:05 +0800 Subject: [PATCH 2/2] =?UTF-8?q?docs:=20=E8=A1=A5=E5=85=85=E5=AE=8C?= =?UTF-8?q?=E5=96=84=E6=96=87=E6=A1=A3=E5=86=85=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - context.md: 添加Cookie操作、日志方法、HTTP客户端、IP获取、请求体操作、响应头操作、WANF/GOB绑定等完整API文档 - advanced.md: 添加协议配置、服务器配置器、IP解析配置、请求体大小限制、条件中间件等高级特性文档 - routing.md: 添加HandleFunc多方法注册、NoRoute自定义404、静态文件路由等内容 --- docs/advanced.md | 252 +++++++++++++++++++++++++++++++++- docs/context.md | 351 ++++++++++++++++++++++++++++++++++++++++++++++- docs/routing.md | 59 ++++++++ 3 files changed, 654 insertions(+), 8 deletions(-) diff --git a/docs/advanced.md b/docs/advanced.md index 345103d..4b68f93 100644 --- a/docs/advanced.md +++ b/docs/advanced.md @@ -14,6 +14,73 @@ Touka 使用 `sync.Pool` 来重用 `touka.Context` 对象。这极大减少了 在路由匹配过程中,Touka 会预分配路径参数切片,并根据路由深度进行缓存,从而在路由查找时实现几乎零分配。 +## 服务器配置 + +### 服务器配置器 (ServerConfigurator) + +Touka 允许您在服务器启动前对底层 `*http.Server` 进行自定义配置: + +```go +r := touka.New() + +// 配置 HTTP 服务器 +r.SetServerConfigurator(func(server *http.Server) { + server.ReadTimeout = 30 * time.Second + server.WriteTimeout = 30 * time.Second + server.IdleTimeout = 120 * time.Second + server.MaxHeaderBytes = 1 << 20 // 1MB +}) + +// 专门配置 HTTPS 服务器(优先级高于 ServerConfigurator) +r.SetTLSServerConfigurator(func(server *http.Server) { + server.ReadTimeout = 30 * time.Second + server.WriteTimeout = 30 * time.Second + // HTTPS 特定配置... +}) +``` + +### 协议配置 + +Touka 支持配置 HTTP/1.1、HTTP/2 和 H2C(HTTP/2 Cleartext): + +```go +// 使用默认协议配置(仅 HTTP/1.1) +r.SetDefaultProtocols() + +// 自定义协议配置 +r.SetProtocols(&touka.ProtocolsConfig{ + Http1: true, // 启用 HTTP/1.1 + Http2: true, // 启用 HTTP/2(需要 TLS) + Http2_Cleartext: true, // 启用 H2C(无需 TLS 的 HTTP/2) +}) +``` + +### 启动方式 + +Touka 提供了多种服务器启动方式: + +```go +// 1. 简单启动(无优雅停机) +r.Run(":8080") + +// 2. 带优雅停机的启动 +r.RunShutdown(":8080", 10*time.Second) + +// 3. 带上下文的优雅停机 +ctx, cancel := context.WithCancel(context.Background()) +r.RunShutdownWithContext(":8080", ctx, 10*time.Second) + +// 4. HTTPS 启动 +tlsConfig := &tls.Config{ + MinVersion: tls.VersionTLS12, + // 其他 TLS 配置... +} +r.RunTLS(":443", tlsConfig, 10*time.Second) + +// 5. HTTPS + HTTP 重定向 +r.RunTLSRedir(":80", ":443", tlsConfig, 10*time.Second) +``` + ## 优雅停机 (Graceful Shutdown) 在部署新版本时,我们希望服务器停止接收新请求,但能处理完当前正在进行的请求。 @@ -29,6 +96,126 @@ if err := r.RunShutdown(":8080", 10*time.Second); err != nil { } ``` +### SSE 长连接的优雅关闭 + +对于 SSE 等长连接场景,Touka 会自动将引擎的关闭信号注入到请求的 Context 中: + +```go +r.GET("/events", func(c *touka.Context) { + c.EventStream(func(w io.Writer) bool { + select { + case <-c.Request.Context().Done(): + // 收到关闭信号,优雅退出 + return false + case <-time.After(1 * time.Second): + // 发送数据 + event := touka.Event{Data: "tick"} + event.Render(w) + return true + } + }) +}) +``` + +## 路由行为配置 + +```go +r := touka.New() + +// 是否自动重定向尾部斜杠(默认 true) +// /foo/ -> /foo 或 /foo -> /foo/ +r.SetRedirectTrailingSlash(true) + +// 是否自动修复路径大小写(默认 true) +// /FOO -> /foo +r.SetRedirectFixedPath(true) + +// 是否处理 405 Method Not Allowed(默认 true) +// 当路径匹配但方法不匹配时返回 405 而非 404 +r.SetHandleMethodNotAllowed(true) +``` + +### 自定义 404 处理 + +```go +// 单个处理器 +r.NoRoute(func(c *touka.Context) { + c.JSON(http.StatusNotFound, touka.H{ + "error": "Page not found", + "path": c.Request.URL.Path, + }) +}) + +// 处理器链(可以在 404 前执行额外中间件) +r.NoRoutes( + LogNotFoundMiddleware(), + func(c *touka.Context) { + c.JSON(http.StatusNotFound, touka.H{"error": "Not found"}) + }, +) +``` + +### 未匹配路径作为静态文件服务 + +```go +// 当没有路由匹配时,尝试从文件系统中查找文件 +// 非常适合单页应用(SPA)部署 +r.SetUnMatchFS(http.Dir("./frontend/dist")) + +// 也可以添加额外的中间件 +r.SetUnMatchFS(http.Dir("./frontend/dist"), AuthMiddleware()) +``` + +## IP 地址解析配置 + +在反向代理环境中,正确配置 IP 解析非常重要: + +```go +r := touka.New() + +// 是否信任代理头部获取客户端 IP(默认 true) +r.SetForwardByClientIP(true) + +// 设置用于获取客户端 IP 的头部列表(按优先级排序) +r.SetRemoteIPHeaders([]string{ + "X-Forwarded-For", + "X-Real-IP", + "CF-Connecting-IP", // Cloudflare +}) +``` + +## 请求体大小限制 + +为了防止恶意的大数据包攻击(如慢速 HTTP 攻击或内存溢出),Touka 内置了请求体大小限制机制。 + +### 全局限制 + +```go +// 设置全局最大请求体大小(例如 10MB) +r.SetGlobalMaxRequestBodySize(10 << 20) +``` + +### 单个请求限制 + +```go +r.POST("/upload", func(c *touka.Context) { + // 为特定请求设置限制(覆盖全局设置) + c.SetMaxRequestBodySize(100 << 20) // 100MB + + body, err := c.GetReqBodyFull() + if err != nil { + // 如果超过限制,会返回 ErrBodyTooLarge + if errors.Is(err, touka.ErrBodyTooLarge) { + c.ErrorUseHandle(http.StatusRequestEntityTooLarge, err) + return + } + c.ErrorUseHandle(http.StatusBadRequest, err) + return + } + // 处理 body... +}) +``` + ## 与标准库集成 Touka 遵循 `net/http` 哲学。您可以方便地使用现有的标准库组件。 @@ -39,6 +226,14 @@ Touka 遵循 `net/http` 哲学。您可以方便地使用现有的标准库组 r.GET("/pprof/*any", touka.AdapterStdFunc(pprof.Index)) ``` +### 适配 `http.Handler` + +```go +// 适配 http.FileServer +fileServer := http.FileServer(http.Dir("./static")) +r.GET("/static/*filepath", touka.AdapterStdHandle(http.StripPrefix("/static", fileServer))) +``` + ### 手动注入 由于 `Engine` 实现了 `http.Handler` 接口,您可以将其挂载到任何地方。 @@ -60,18 +255,61 @@ Touka 默认集成了 `reco` 日志库。您可以自定义其输出行为。 ```go logConfig := reco.Config{ - Level: reco.LevelInfo, - Output: os.Stdout, - Async: true, // 异步写入提高性能 + Level: reco.LevelInfo, + Mode: reco.ModeText, // 或 reco.ModeJSON + Output: os.Stdout, + Async: true, // 异步写入提高性能 + TimeFormat: time.RFC3339, } r.SetLoggerCfg(logConfig) + +// 或直接传入日志实例 +logger, _ := reco.New(logConfig) +r.SetLogger(logger) + +// 关闭日志(在服务器关闭时) +defer r.CloseLogger() ``` -## 内存读取限制 (MaxReader) +## HTTP 客户端配置 -为了防止恶意的大数据包攻击(如慢速 HTTP 攻击或内存溢出),Touka 内置了 `MaxReader` 机制。 +Touka 内置了 `httpc` HTTP 客户端,可以在请求处理中方便地发起出站请求: ```go -// 设置全局最大读取限制(例如 2MB) -r.SetGlobalMaxRequestBodySize(2 << 20) +// 创建自定义 HTTP 客户端 +customClient := httpc.New() +r.SetHTTPClient(customClient) + +// 在处理器中使用 +r.GET("/proxy", func(c *touka.Context) { + resp, err := c.GetHTTPC().Get("https://api.example.com/data") + // ... +}) +``` + +## 条件中间件 + +Touka 支持根据条件动态启用或禁用中间件: + +```go +// 单个条件中间件 +r.Use(r.UseIf(config.EnableLogging, AccessLoggerMiddleware())) + +// 条件中间件链 +r.Use(r.UseChainIf(config.EnableMetrics, + MetricsMiddleware, + PrometheusMiddleware, + MonitoringMiddleware, +)) +``` + +## 获取路由信息 + +```go +// 获取所有已注册的路由信息 +routes := r.GetRouterInfo() +for _, route := range routes { + fmt.Printf("Method: %s, Path: %s, Handler: %s, Group: %s\n", + route.Method, route.Path, route.Handler, route.Group) +} ``` diff --git a/docs/context.md b/docs/context.md index 9dc962d..c13c158 100644 --- a/docs/context.md +++ b/docs/context.md @@ -4,6 +4,16 @@ ## 请求数据解析 +### 路径参数 (Path Parameters) + +```go +// 路由: /users/:id +r.GET("/users/:id", func(c *touka.Context) { + id := c.Param("id") + c.String(http.StatusOK, "User ID: %s", id) +}) +``` + ### 查询参数 (Query Parameters) ```go @@ -31,6 +41,72 @@ r.POST("/form_post", func(c *touka.Context) { }) ``` +### 请求体读取 + +```go +// 读取完整请求体 +r.POST("/raw", func(c *touka.Context) { + body, err := c.GetReqBodyFull() + if err != nil { + c.ErrorUseHandle(http.StatusBadRequest, err) + return + } + c.Raw(http.StatusOK, "application/octet-stream", body) +}) + +// 获取 io.ReadCloser(只能读取一次) +r.POST("/stream", func(c *touka.Context) { + reader := c.GetReqBody() + defer reader.Close() + // 处理 reader... +}) +``` + +### 客户端信息 + +```go +r.GET("/client-info", func(c *touka.Context) { + // 获取客户端 IP(支持代理转发) + ip := c.RequestIP() + // 或使用别名 + ip = c.ClientIP() + + // 获取 User-Agent + ua := c.UserAgent() + + // 获取 Content-Type + ct := c.ContentType() + + // 获取请求协议 + proto := c.GetProtocol() + + c.JSON(http.StatusOK, touka.H{ + "ip": ip, + "userAgent": ua, + "protocol": proto, + }) +}) +``` + +### 请求头 + +```go +r.GET("/headers", func(c *touka.Context) { + // 获取单个请求头 + auth := c.GetReqHeader("Authorization") + + // 获取所有请求头 + allHeaders := c.GetAllReqHeader() + + c.JSON(http.StatusOK, touka.H{ + "authorization": auth, + "allHeaders": allHeaders, + }) +}) +``` + +## 数据绑定 + ### JSON 绑定 Touka 提供了非常便捷的 JSON 绑定功能,它会自动解析请求体并填充到结构体中。 @@ -57,6 +133,67 @@ r.POST("/login", func(c *touka.Context) { }) ``` +### 表单绑定 + +```go +type UserForm struct { + Name string `form:"name"` + Email string `form:"email"` + Age int `form:"age"` +} + +r.POST("/user", func(c *touka.Context) { + var form UserForm + if err := c.ShouldBindForm(&form); err != nil { + c.JSON(http.StatusBadRequest, touka.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, form) +}) +``` + +### 通用绑定 + +`ShouldBind` 方法会根据请求的 `Content-Type` 自动选择绑定方式: + +```go +r.POST("/data", func(c *touka.Context) { + var data MyData + // 自动根据 Content-Type 绑定(支持 JSON、Form、WANF、GOB) + if err := c.ShouldBind(&data); err != nil { + c.JSON(http.StatusBadRequest, touka.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, data) +}) +``` + +### WANF 绑定 + +```go +r.POST("/wanf", func(c *touka.Context) { + var data MyData + if err := c.ShouldBindWANF(&data); err != nil { + c.JSON(http.StatusBadRequest, touka.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, data) +}) +``` + +### GOB 绑定 + +```go +r.POST("/gob", func(c *touka.Context) { + var data MyData + if err := c.ShouldBindGOB(&data); err != nil { + c.JSON(http.StatusBadRequest, touka.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, data) +}) +``` + ## 响应构建 ### 基础格式 @@ -73,21 +210,73 @@ c.String(http.StatusOK, "welcome %s", name) // 纯文本 c.Text(http.StatusOK, "just text") +// 原始数据 +c.Raw(http.StatusOK, "application/octet-stream", []byte("raw bytes")) + // HTML 模板 c.HTML(http.StatusOK, "index.tmpl", touka.H{"title": "Main website"}) ``` +### WANF 响应 + +```go +// WANF 格式响应 +c.WANF(http.StatusOK, touka.H{"message": "wanf format"}) +``` + +### GOB 响应 + +```go +// GOB 格式响应 +c.GOB(http.StatusOK, myData) +``` + ### 文件与流 ```go -// 服务本地文件 +// 服务本地文件(触发浏览器下载) c.File("/local/file.go") // 将文件内容作为响应体(不触发下载) c.SetRespBodyFile(http.StatusOK, "config.json") +// 以文本形式发送文件 +c.FileText(http.StatusOK, "/path/to/file.txt") + // 写入数据流 c.WriteStream(reader) + +// 设置响应体为流 +c.SetBodyStream(reader, contentSize) // contentSize 为 -1 表示未知大小 +``` + +### 响应头操作 + +```go +// 设置响应头 +c.SetHeader("X-Custom-Header", "value") + +// 添加响应头(不覆盖已有值) +c.AddHeader("X-Custom-Header", "another-value") + +// 删除响应头 +c.DelHeader("X-Custom-Header") + +// 批量设置响应头 +c.SetHeaders(map[string][]string{ + "X-Header-1": {"value1"}, + "X-Header-2": {"value2a", "value2b"}, +}) + +// 获取所有响应头 +headers := c.GetAllRespHeader() +``` + +### 状态码 + +```go +// 设置状态码(不写入响应体) +c.Status(http.StatusNoContent) ``` ### 重定向 @@ -96,6 +285,34 @@ c.WriteStream(reader) c.Redirect(http.StatusMovedPermanently, "http://google.com/") ``` +## Cookie 操作 + +```go +// 设置 Cookie +c.SetCookie("session_id", "abc123", 3600, "/", "example.com", true, true) + +// 设置 SameSite 属性 +c.SetSameSite(http.SameSiteStrictMode) + +// 使用完整 Cookie 对象 +cookie := &http.Cookie{ + Name: "token", + Value: "xyz", + Path: "/", +} +c.SetCookieData(cookie) + +// 获取 Cookie +value, err := c.GetCookie("session_id") +if err != nil { + c.String(http.StatusUnauthorized, "Cookie not found") + return +} + +// 删除 Cookie +c.DeleteCookie("session_id") +``` + ## 数据传递 (Keys/Values) 您可以在中间件和处理器之间共享数据。 @@ -107,14 +324,146 @@ c.Set("user_id", 12345) // 在处理器中获取 id, exists := c.Get("user_id") val := c.MustGet("user_id").(int) + +// 类型安全的获取方法 +str, exists := c.GetString("key") +i, exists := c.GetInt("key") +b, exists := c.GetBool("key") +f, exists := c.GetFloat64("key") +t, exists := c.GetTime("key") +d, exists := c.GetDuration("key") +``` + +## 错误处理 + +```go +r.GET("/error", func(c *touka.Context) { + // 添加错误到上下文(可以添加多个) + c.AddError(errors.New("error 1")) + c.AddError(errors.New("error 2")) + + // 获取所有错误 + errs := c.GetErrors() + + // 使用全局错误处理器 + c.ErrorUseHandle(http.StatusInternalServerError, errors.New("something went wrong")) +}) +``` + +## 日志记录 + +Touka 集成了 `reco` 日志库,可以直接在 Context 中使用: + +```go +r.GET("/log", func(c *touka.Context) { + c.Debugf("Debug message: %s", "details") + c.Infof("User accessed /log") + c.Warnf("Warning: %v", someWarning) + c.Errorf("Error occurred: %v", someError) + + // 获取底层日志器 + logger := c.GetLogger() + logger.CustomLog("level", "message") + + c.String(http.StatusOK, "Logged") +}) +``` + +## HTTP 客户端 + +Touka 集成了 `httpc` HTTP 客户端,方便发起出站请求: + +```go +r.GET("/proxy", func(c *touka.Context) { + // 获取 HTTP 客户端 + client := c.GetHTTPC() + // 或 + client = c.Client() + + // 发起请求 + resp, err := client.Get("https://api.example.com/data") + if err != nil { + c.ErrorUseHandle(http.StatusBadGateway, err) + return + } + defer resp.Body.Close() + + // 将响应流式传输给客户端 + c.SetHeader("Content-Type", resp.Header.Get("Content-Type")) + c.WriteStream(resp.Body) +}) ``` ## 状态管理 - `c.Abort()`: 停止执行后续的处理器/中间件。 +- `c.AbortWithStatus(code)`: 中止并设置状态码。 +- `c.IsAborted()`: 检查是否已中止。 - `c.Next()`: 执行后续的处理链。这常用于中间件中,在执行完某些前置逻辑后,显式调用 `Next`,并在其返回后执行后置逻辑。 +## 请求上下文 (Go Context) + +Touka Context 实现了 Go 标准库的 `context.Context` 接口: + +```go +r.GET("/long-task", func(c *touka.Context) { + // 获取 Go context + ctx := c.Context() + + // 监听取消信号 + select { + case <-ctx.Done(): + // 客户端断开连接或超时 + return + case result := <-doLongTask(ctx): + c.JSON(http.StatusOK, result) + } +}) + +// 其他 context 方法 +done := c.Done() // 获取 Done channel +err := c.Err() // 获取错误 +val := c.Value("key") // 获取值(同时查找 Keys 和 Go context) +``` + +## 其他方法 + +```go +// 获取原始请求 URI +uri := c.GetRequestURI() + +// 获取请求路径 +path := c.GetRequestURIPath() + +// 获取查询字符串 +query := c.GetReqQueryString() + +// 获取请求协议版本 +proto := c.GetProtocol() // 例如 "HTTP/1.1" +``` + ## 对象池化 为了提高性能,Touka 的 Context 对象是复用的。 + **重要提示:不要在 Goroutine 中持久化持有 `touka.Context` 指针。如果您需要在 Goroutine 中使用请求数据,请务必在派生 Goroutine 前提取所需的值。** + +```go +// 错误示例 ❌ +r.GET("/bad", func(c *touka.Context) { + go func() { + time.Sleep(5 * time.Second) + // 此时 c 可能已被复用,数据不安全 + log.Println(c.Query("name")) + }() +}) + +// 正确示例 ✓ +r.GET("/good", func(c *touka.Context) { + name := c.Query("name") // 提前提取值 + go func() { + time.Sleep(5 * time.Second) + log.Println(name) // 使用提取的值,安全 + }() +}) +``` diff --git a/docs/routing.md b/docs/routing.md index 039341f..e90308e 100644 --- a/docs/routing.md +++ b/docs/routing.md @@ -17,6 +17,9 @@ r.OPTIONS("/someOptions", handle) // 注册所有上述方法的路由 r.ANY("/any", handle) + +// 同时注册多个方法 +r.HandleFunc([]string{"GET", "POST"}, "/multi", handle) ``` ## 路径参数 (Named Parameters) @@ -92,3 +95,59 @@ for _, route := range routes { fmt.Printf("Method: %s, Path: %s\n", route.Method, route.Path) } ``` + +## 自定义 404 处理 + +当请求没有匹配到任何路由时,Touka 会返回 404。您可以自定义 404 的处理逻辑: + +```go +// 使用单个处理器 +r.NoRoute(func(c *touka.Context) { + c.JSON(http.StatusNotFound, touka.H{ + "error": "资源未找到", + "path": c.Request.URL.Path, + }) +}) + +// 使用处理器链 +r.NoRoutes( + LogNotFoundMiddleware(), + func(c *touka.Context) { + c.JSON(http.StatusNotFound, touka.H{"error": "Not found"}) + }, +) +``` + +**注意**:`NoRoute` 和 `NoRoutes` 不是处理链的终点,您仍然可以在其中调用 `c.Next()` 来继续执行默认的 404 处理。 + +## 静态文件路由 + +Touka 提供了便捷的方法来注册静态文件路由: + +```go +// 服务整个目录 +r.StaticDir("/assets", "./static") +// 访问 /assets/js/main.js 将返回 ./static/js/main.js + +// 服务单个文件 +r.StaticFile("/favicon.ico", "./resources/favicon.ico") + +// 服务嵌入式文件系统 +//go:embed dist/* +var content embed.FS + +func main() { + r := touka.Default() + fsroot, _ := fs.Sub(content, "dist") + r.StaticFS("/", http.FS(fsroot)) + r.Run(":8080") +} +``` + +这些方法同样可以在路由组中使用: + +```go +api := r.Group("/api") +api.StaticDir("/files", "./uploads") +api.StaticFile("/logo", "./assets/logo.png") +```