mirror of
https://github.com/infinite-iroha/touka.git
synced 2026-06-13 23:57:38 +08:00
Compare commits
3 commits
4f1acda553
...
fcc23745b6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fcc23745b6 | ||
|
|
7b8c0d7dcb | ||
|
|
8af515059a |
6 changed files with 662 additions and 16 deletions
252
docs/advanced.md
252
docs/advanced.md
|
|
@ -14,6 +14,73 @@ Touka 使用 `sync.Pool` 来重用 `touka.Context` 对象。这极大减少了
|
||||||
|
|
||||||
在路由匹配过程中,Touka 会预分配路径参数切片,并根据路由深度进行缓存,从而在路由查找时实现几乎零分配。
|
在路由匹配过程中,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)
|
## 优雅停机 (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` 哲学。您可以方便地使用现有的标准库组件。
|
Touka 遵循 `net/http` 哲学。您可以方便地使用现有的标准库组件。
|
||||||
|
|
@ -39,6 +226,14 @@ Touka 遵循 `net/http` 哲学。您可以方便地使用现有的标准库组
|
||||||
r.GET("/pprof/*any", touka.AdapterStdFunc(pprof.Index))
|
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` 接口,您可以将其挂载到任何地方。
|
由于 `Engine` 实现了 `http.Handler` 接口,您可以将其挂载到任何地方。
|
||||||
|
|
@ -60,18 +255,61 @@ Touka 默认集成了 `reco` 日志库。您可以自定义其输出行为。
|
||||||
|
|
||||||
```go
|
```go
|
||||||
logConfig := reco.Config{
|
logConfig := reco.Config{
|
||||||
Level: reco.LevelInfo,
|
Level: reco.LevelInfo,
|
||||||
Output: os.Stdout,
|
Mode: reco.ModeText, // 或 reco.ModeJSON
|
||||||
Async: true, // 异步写入提高性能
|
Output: os.Stdout,
|
||||||
|
Async: true, // 异步写入提高性能
|
||||||
|
TimeFormat: time.RFC3339,
|
||||||
}
|
}
|
||||||
r.SetLoggerCfg(logConfig)
|
r.SetLoggerCfg(logConfig)
|
||||||
|
|
||||||
|
// 或直接传入日志实例
|
||||||
|
logger, _ := reco.New(logConfig)
|
||||||
|
r.SetLogger(logger)
|
||||||
|
|
||||||
|
// 关闭日志(在服务器关闭时)
|
||||||
|
defer r.CloseLogger()
|
||||||
```
|
```
|
||||||
|
|
||||||
## 内存读取限制 (MaxReader)
|
## HTTP 客户端配置
|
||||||
|
|
||||||
为了防止恶意的大数据包攻击(如慢速 HTTP 攻击或内存溢出),Touka 内置了 `MaxReader` 机制。
|
Touka 内置了 `httpc` HTTP 客户端,可以在请求处理中方便地发起出站请求:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// 设置全局最大读取限制(例如 2MB)
|
// 创建自定义 HTTP 客户端
|
||||||
r.SetMaxReader(2 << 20)
|
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)
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
|
||||||
357
docs/context.md
357
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)
|
### 查询参数 (Query Parameters)
|
||||||
|
|
||||||
```go
|
```go
|
||||||
|
|
@ -31,14 +41,80 @@ 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 绑定
|
### JSON 绑定
|
||||||
|
|
||||||
Touka 提供了非常便捷的 JSON 绑定功能,它会自动解析请求体并填充到结构体中,同时进行基本的验证。
|
Touka 提供了非常便捷的 JSON 绑定功能,它会自动解析请求体并填充到结构体中。
|
||||||
|
|
||||||
```go
|
```go
|
||||||
type LoginRequest struct {
|
type LoginRequest struct {
|
||||||
User string `json:"user" binding:"required"`
|
User string `json:"user"`
|
||||||
Password string `json:"password" binding:"required"`
|
Password string `json:"password"`
|
||||||
}
|
}
|
||||||
|
|
||||||
r.POST("/login", func(c *touka.Context) {
|
r.POST("/login", func(c *touka.Context) {
|
||||||
|
|
@ -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.Text(http.StatusOK, "just text")
|
||||||
|
|
||||||
|
// 原始数据
|
||||||
|
c.Raw(http.StatusOK, "application/octet-stream", []byte("raw bytes"))
|
||||||
|
|
||||||
// HTML 模板
|
// HTML 模板
|
||||||
c.HTML(http.StatusOK, "index.tmpl", touka.H{"title": "Main website"})
|
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
|
```go
|
||||||
// 服务本地文件
|
// 服务本地文件(触发浏览器下载)
|
||||||
c.File("/local/file.go")
|
c.File("/local/file.go")
|
||||||
|
|
||||||
// 将文件内容作为响应体(不触发下载)
|
// 将文件内容作为响应体(不触发下载)
|
||||||
c.SetRespBodyFile(http.StatusOK, "config.json")
|
c.SetRespBodyFile(http.StatusOK, "config.json")
|
||||||
|
|
||||||
|
// 以文本形式发送文件
|
||||||
|
c.FileText(http.StatusOK, "/path/to/file.txt")
|
||||||
|
|
||||||
// 写入数据流
|
// 写入数据流
|
||||||
c.WriteStream(reader)
|
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/")
|
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)
|
## 数据传递 (Keys/Values)
|
||||||
|
|
||||||
您可以在中间件和处理器之间共享数据。
|
您可以在中间件和处理器之间共享数据。
|
||||||
|
|
@ -107,14 +324,146 @@ c.Set("user_id", 12345)
|
||||||
// 在处理器中获取
|
// 在处理器中获取
|
||||||
id, exists := c.Get("user_id")
|
id, exists := c.Get("user_id")
|
||||||
val := c.MustGet("user_id").(int)
|
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.Abort()`: 停止执行后续的处理器/中间件。
|
||||||
|
- `c.AbortWithStatus(code)`: 中止并设置状态码。
|
||||||
|
- `c.IsAborted()`: 检查是否已中止。
|
||||||
- `c.Next()`: 执行后续的处理链。这常用于中间件中,在执行完某些前置逻辑后,显式调用 `Next`,并在其返回后执行后置逻辑。
|
- `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 对象是复用的。
|
为了提高性能,Touka 的 Context 对象是复用的。
|
||||||
|
|
||||||
**重要提示:不要在 Goroutine 中持久化持有 `touka.Context` 指针。如果您需要在 Goroutine 中使用请求数据,请务必在派生 Goroutine 前提取所需的值。**
|
**重要提示:不要在 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) // 使用提取的值,安全
|
||||||
|
}()
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ func TimerMiddleware() touka.HandlerFunc {
|
||||||
```go
|
```go
|
||||||
func APIKeyAuth() touka.HandlerFunc {
|
func APIKeyAuth() touka.HandlerFunc {
|
||||||
return func(c *touka.Context) {
|
return func(c *touka.Context) {
|
||||||
apiKey := c.GetHeader("X-API-KEY")
|
apiKey := c.GetReqHeader("X-API-KEY")
|
||||||
if apiKey != "secret-token" {
|
if apiKey != "secret-token" {
|
||||||
// 验证失败,返回错误并中止后续逻辑
|
// 验证失败,返回错误并中止后续逻辑
|
||||||
c.JSON(http.StatusUnauthorized, touka.H{"error": "Invalid API Key"})
|
c.JSON(http.StatusUnauthorized, touka.H{"error": "Invalid API Key"})
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
## 安装
|
## 安装
|
||||||
|
|
||||||
确保您的环境中已经安装了 Go 1.25 或更高版本。
|
确保您的环境中已经安装了 Go 1.26 或更高版本。
|
||||||
|
|
||||||
在您的项目目录中运行:
|
在您的项目目录中运行:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,9 @@ r.OPTIONS("/someOptions", handle)
|
||||||
|
|
||||||
// 注册所有上述方法的路由
|
// 注册所有上述方法的路由
|
||||||
r.ANY("/any", handle)
|
r.ANY("/any", handle)
|
||||||
|
|
||||||
|
// 同时注册多个方法
|
||||||
|
r.HandleFunc([]string{"GET", "POST"}, "/multi", handle)
|
||||||
```
|
```
|
||||||
|
|
||||||
## 路径参数 (Named Parameters)
|
## 路径参数 (Named Parameters)
|
||||||
|
|
@ -78,8 +81,8 @@ Touka 允许您自定义路由匹配的行为:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
r := touka.New()
|
r := touka.New()
|
||||||
r.RedirectTrailingSlash = true
|
r.SetRedirectTrailingSlash(true)
|
||||||
r.HandleMethodNotAllowed = true
|
r.SetHandleMethodNotAllowed(true)
|
||||||
```
|
```
|
||||||
|
|
||||||
## 获取已注册路由信息
|
## 获取已注册路由信息
|
||||||
|
|
@ -92,3 +95,59 @@ for _, route := range routes {
|
||||||
fmt.Printf("Method: %s, Path: %s\n", route.Method, route.Path)
|
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")
|
||||||
|
```
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ func main() {
|
||||||
|
|
||||||
```go
|
```go
|
||||||
r := touka.New()
|
r := touka.New()
|
||||||
r.SetUnMatchFS(http.Dir("./frontend/dist"), true)
|
r.SetUnMatchFS(http.Dir("./frontend/dist"))
|
||||||
|
|
||||||
// API 路由
|
// API 路由
|
||||||
r.GET("/api/status", handleStatus)
|
r.GET("/api/status", handleStatus)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue