touka/docs/context.md
WJQSERVER 7b8c0d7dcb docs: 补充完善文档内容
- context.md: 添加Cookie操作、日志方法、HTTP客户端、IP获取、请求体操作、响应头操作、WANF/GOB绑定等完整API文档
- advanced.md: 添加协议配置、服务器配置器、IP解析配置、请求体大小限制、条件中间件等高级特性文档
- routing.md: 添加HandleFunc多方法注册、NoRoute自定义404、静态文件路由等内容
2026-03-22 10:13:05 +08:00

10 KiB
Raw Blame History

上下文 (Context)

touka.Context 是 Touka 框架中最重要的结构。它携带了关于当前 HTTP 请求的所有必要信息,并提供了一系列方法来解析请求和构建响应。

请求数据解析

路径参数 (Path Parameters)

// 路由: /users/:id
r.GET("/users/:id", func(c *touka.Context) {
    id := c.Param("id")
    c.String(http.StatusOK, "User ID: %s", id)
})

查询参数 (Query Parameters)

// /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)

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,
    })
})

请求体读取

// 读取完整请求体
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...
})

客户端信息

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,
    })
})

请求头

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 绑定功能,它会自动解析请求体并填充到结构体中。

type LoginRequest struct {
    User     string `json:"user"`
    Password string `json:"password"`
}

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"})
})

表单绑定

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 自动选择绑定方式:

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 绑定

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 绑定

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)
})

响应构建

基础格式

Touka 支持多种响应格式:

// JSON
c.JSON(http.StatusOK, touka.H{"message": "hey"})

// 字符串 (支持格式化)
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 响应

// WANF 格式响应
c.WANF(http.StatusOK, touka.H{"message": "wanf format"})

GOB 响应

// GOB 格式响应
c.GOB(http.StatusOK, myData)

文件与流

// 服务本地文件(触发浏览器下载)
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 表示未知大小

响应头操作

// 设置响应头
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()

状态码

// 设置状态码(不写入响应体)
c.Status(http.StatusNoContent)

重定向

c.Redirect(http.StatusMovedPermanently, "http://google.com/")
// 设置 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)

您可以在中间件和处理器之间共享数据。

// 在中间件中设置
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")

错误处理

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 中使用:

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 客户端,方便发起出站请求:

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 接口:

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

其他方法

// 获取原始请求 URI
uri := c.GetRequestURI()

// 获取请求路径
path := c.GetRequestURIPath()

// 获取查询字符串
query := c.GetReqQueryString()

// 获取请求协议版本
proto := c.GetProtocol() // 例如 "HTTP/1.1"

对象池化

为了提高性能Touka 的 Context 对象是复用的。

重要提示:不要在 Goroutine 中持久化持有 touka.Context 指针。如果您需要在 Goroutine 中使用请求数据,请务必在派生 Goroutine 前提取所需的值。

// 错误示例 ❌
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) // 使用提取的值,安全
    }()
})