mirror of
https://github.com/infinite-iroha/touka.git
synced 2026-06-13 15:47:38 +08:00
docs: 添加 httpc 集成文档和示例
- 新增 examples/httpc 示例代码 - 新增 docs/httpc.md 文档说明
This commit is contained in:
parent
f2295c3084
commit
4f262b2497
2 changed files with 291 additions and 0 deletions
188
docs/httpc.md
Normal file
188
docs/httpc.md
Normal file
|
|
@ -0,0 +1,188 @@
|
||||||
|
# HTTP Client (httpc)
|
||||||
|
|
||||||
|
Touka 内置了 [httpc](https://github.com/WJQSERVER-STUDIO/httpc) HTTP 客户端,方便在请求处理函数中发起出站 HTTP 请求。
|
||||||
|
|
||||||
|
## 核心特性
|
||||||
|
|
||||||
|
- **自动 Context 关联**:使用 `HTTPC()` 方法时,出站请求会自动关联当前请求的 Context
|
||||||
|
- **请求取消传播**:当客户端断开连接时,出站请求会自动取消,避免资源泄漏
|
||||||
|
- **链式调用**:保持 httpc 原有的组合式构建器风格
|
||||||
|
|
||||||
|
## 基本用法
|
||||||
|
|
||||||
|
### 简单 GET 请求
|
||||||
|
|
||||||
|
```go
|
||||||
|
r.GET("/proxy", func(c *touka.Context) {
|
||||||
|
body, err := c.HTTPC().
|
||||||
|
GET("https://api.example.com/data").
|
||||||
|
Text()
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(500, touka.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.String(200, body)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### POST JSON 请求
|
||||||
|
|
||||||
|
```go
|
||||||
|
r.POST("/users", func(c *touka.Context) {
|
||||||
|
var req struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
}
|
||||||
|
c.ShouldBindJSON(&req)
|
||||||
|
|
||||||
|
var result struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
err := c.HTTPC().
|
||||||
|
POST("https://api.example.com/users").
|
||||||
|
SetHeader("Authorization", "Bearer "+token).
|
||||||
|
SetJSONBody(req).
|
||||||
|
DecodeJSON(&result)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(500, touka.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(200, result)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 带查询参数
|
||||||
|
|
||||||
|
```go
|
||||||
|
r.GET("/search", func(c *touka.Context) {
|
||||||
|
query := c.Query("q")
|
||||||
|
|
||||||
|
var result SearchResult
|
||||||
|
err := c.HTTPC().
|
||||||
|
GET("https://api.example.com/search").
|
||||||
|
SetQueryParam("q", query).
|
||||||
|
SetQueryParam("limit", "10").
|
||||||
|
DecodeJSON(&result)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(500, touka.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(200, result)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## API 对比
|
||||||
|
|
||||||
|
### 旧方式(Deprecated)
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 需要手动 WithContext,容易忘记
|
||||||
|
resp, err := c.Client().
|
||||||
|
WithContext(c.Context()).
|
||||||
|
GET(url).
|
||||||
|
Execute()
|
||||||
|
```
|
||||||
|
|
||||||
|
### 新方式(推荐)
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 自动关联请求 Context
|
||||||
|
resp, err := c.HTTPC().
|
||||||
|
GET(url).
|
||||||
|
Execute()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Context 取消机制
|
||||||
|
|
||||||
|
使用 `HTTPC()` 时,当客户端断开连接(如关闭浏览器),出站请求会自动取消:
|
||||||
|
|
||||||
|
```go
|
||||||
|
r.GET("/long-task", func(c *touka.Context) {
|
||||||
|
// 这个请求会在客户端断开时自动取消
|
||||||
|
resp, err := c.HTTPC().
|
||||||
|
GET("https://slow-api.example.com/data").
|
||||||
|
Execute()
|
||||||
|
|
||||||
|
// 如果客户端已断开,err 会包含 context.Canceled
|
||||||
|
if errors.Is(err, context.Canceled) {
|
||||||
|
return // 客户端已断开,无需处理
|
||||||
|
}
|
||||||
|
// ...
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## 完整 API
|
||||||
|
|
||||||
|
### contextHTTPClient 方法
|
||||||
|
|
||||||
|
| 方法 | 返回类型 | 说明 |
|
||||||
|
|------|----------|------|
|
||||||
|
| `NewRequestBuilder(method, url)` | `*httpc.RequestBuilder` | 创建通用请求构建器 |
|
||||||
|
| `GET(url)` | `*httpc.RequestBuilder` | 创建 GET 请求 |
|
||||||
|
| `POST(url)` | `*httpc.RequestBuilder` | 创建 POST 请求 |
|
||||||
|
| `PUT(url)` | `*httpc.RequestBuilder` | 创建 PUT 请求 |
|
||||||
|
| `DELETE(url)` | `*httpc.RequestBuilder` | 创建 DELETE 请求 |
|
||||||
|
| `PATCH(url)` | `*httpc.RequestBuilder` | 创建 PATCH 请求 |
|
||||||
|
| `HEAD(url)` | `*httpc.RequestBuilder` | 创建 HEAD 请求 |
|
||||||
|
| `OPTIONS(url)` | `*httpc.RequestBuilder` | 创建 OPTIONS 请求 |
|
||||||
|
|
||||||
|
### httpc.RequestBuilder 链式方法
|
||||||
|
|
||||||
|
返回 `*httpc.RequestBuilder`(用于链式调用):
|
||||||
|
|
||||||
|
| 方法 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `WithContext(ctx)` | 设置 Context(通常不需要,已自动关联) |
|
||||||
|
| `NoDefaultHeaders()` | 不添加默认 Header |
|
||||||
|
| `SetHeader(key, value)` | 设置 Header |
|
||||||
|
| `AddHeader(key, value)` | 添加 Header(可重复) |
|
||||||
|
| `SetHeaders(map)` | 批量设置 Headers |
|
||||||
|
| `SetQueryParam(key, value)` | 设置查询参数 |
|
||||||
|
| `AddQueryParam(key, value)` | 添加查询参数(可重复) |
|
||||||
|
| `SetQueryParams(map)` | 批量设置查询参数 |
|
||||||
|
| `SetBody(io.Reader)` | 设置请求 Body |
|
||||||
|
| `SetRawBody([]byte)` | 设置字节 Body |
|
||||||
|
|
||||||
|
返回 `(*httpc.RequestBuilder, error)`(可能失败):
|
||||||
|
|
||||||
|
| 方法 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `SetJSONBody(any)` | 设置 JSON Body |
|
||||||
|
| `SetXMLBody(any)` | 设置 XML Body |
|
||||||
|
| `SetGOBBody(any)` | 设置 GOB Body |
|
||||||
|
|
||||||
|
### 终结方法
|
||||||
|
|
||||||
|
| 方法 | 返回类型 | 说明 |
|
||||||
|
|------|----------|------|
|
||||||
|
| `Build()` | `(*http.Request, error)` | 构建请求但不执行 |
|
||||||
|
| `Execute()` | `(*http.Response, error)` | 执行并返回原始响应 |
|
||||||
|
| `DecodeJSON(v)` | `error` | 执行并解码 JSON |
|
||||||
|
| `DecodeXML(v)` | `error` | 执行并解码 XML |
|
||||||
|
| `DecodeGOB(v)` | `error` | 执行并解码 GOB |
|
||||||
|
| `Text()` | `(string, error)` | 执行并返回文本 |
|
||||||
|
| `Bytes()` | `([]byte, error)` | 执行并返回字节 |
|
||||||
|
| `SSE()` | `(*SSEStream, error)` | 建立 SSE 流连接 |
|
||||||
|
|
||||||
|
## 迁移指南
|
||||||
|
|
||||||
|
### go:fix inline 兼容
|
||||||
|
|
||||||
|
旧代码 `c.GetHTTPC()` 可通过 `go fix` 自动迁移到 `c.Client()`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go fix ./...
|
||||||
|
```
|
||||||
|
|
||||||
|
### 手动迁移
|
||||||
|
|
||||||
|
| 旧代码 | 新代码 |
|
||||||
|
|--------|--------|
|
||||||
|
| `c.GetHTTPC()` | `c.Client()` 或 `c.HTTPC()` |
|
||||||
|
| `c.Client().WithContext(ctx).GET(url)` | `c.HTTPC().GET(url)` |
|
||||||
|
|
||||||
|
## 示例
|
||||||
|
|
||||||
|
完整示例请参考 [examples/httpc](../examples/httpc)。
|
||||||
103
examples/httpc/main.go
Normal file
103
examples/httpc/main.go
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/infinite-iroha/touka"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
r := touka.Default()
|
||||||
|
|
||||||
|
// 示例 1:简单 GET 请求(自动关联请求 Context)
|
||||||
|
r.GET("/proxy", func(c *touka.Context) {
|
||||||
|
// 使用 HTTPC() 方法,自动关联请求 Context
|
||||||
|
// 当客户端断开连接时,出站请求也会自动取消
|
||||||
|
body, err := c.HTTPC().
|
||||||
|
GET("https://httpbin.org/get").
|
||||||
|
Text()
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, touka.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.String(http.StatusOK, body)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 示例 2:带 Header 的 POST 请求
|
||||||
|
r.POST("/users", func(c *touka.Context) {
|
||||||
|
var req struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
}
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, touka.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var result struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 链式调用,保持 httpc 风格
|
||||||
|
// 注意:SetJSONBody 返回 (*RequestBuilder, error)
|
||||||
|
rb, err := c.HTTPC().
|
||||||
|
POST("https://httpbin.org/post").
|
||||||
|
SetHeader("X-API-Key", "secret").
|
||||||
|
SetJSONBody(req)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, touka.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := rb.DecodeJSON(&result); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, touka.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, result)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 示例 3:带查询参数的请求
|
||||||
|
r.GET("/search", func(c *touka.Context) {
|
||||||
|
query := c.DefaultQuery("q", "")
|
||||||
|
page := c.DefaultQuery("page", "1")
|
||||||
|
|
||||||
|
var result struct {
|
||||||
|
Items []string `json:"items"`
|
||||||
|
Total int `json:"total"`
|
||||||
|
}
|
||||||
|
|
||||||
|
err := c.HTTPC().
|
||||||
|
GET("https://httpbin.org/get").
|
||||||
|
SetQueryParam("q", query).
|
||||||
|
SetQueryParam("page", page).
|
||||||
|
DecodeJSON(&result)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, touka.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, result)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 示例 4:使用底层 httpc.Client(旧方式,仍可用但不推荐)
|
||||||
|
r.GET("/legacy", func(c *touka.Context) {
|
||||||
|
// 旧方式:需要手动 WithContext
|
||||||
|
body, err := c.Client().
|
||||||
|
GET("https://httpbin.org/get").
|
||||||
|
WithContext(c.Context()).
|
||||||
|
Text()
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, touka.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.String(http.StatusOK, body)
|
||||||
|
})
|
||||||
|
|
||||||
|
fmt.Println("Server running on :8080")
|
||||||
|
fmt.Println("Try:")
|
||||||
|
fmt.Println(" curl http://localhost:8080/proxy")
|
||||||
|
fmt.Println(" curl -X POST -d '{\"name\":\"test\",\"email\":\"test@example.com\"}' http://localhost:8080/users")
|
||||||
|
fmt.Println(" curl 'http://localhost:8080/search?q=golang&page=1'")
|
||||||
|
|
||||||
|
// r.Run(touka.WithAddr(":8080"))
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue