# HttpClient
**Repository Path**: vipkwds/httpclient
## Basic Information
- **Project Name**: HttpClient
- **Description**: `httpclient` 提供了一个灵活的 HTTP 客户端封装,支持链式调用、拦截器、重试机制、Cookie管理和URL构建等功能
- **Primary Language**: Go
- **License**: MIT
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 0
- **Created**: 2026-05-21
- **Last Updated**: 2026-05-22
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# HTTP Client 客户端文档
`httpclient` 提供了一个灵活的 HTTP 客户端封装,支持链式调用、拦截器、重试机制、Cookie管理和URL构建等功能。
---
## 安装
```bash
go get gitee.com/vipkwds/httpclient
```
---
## 目录
1. [快速开始](#快速开始)
2. [Client 客户端](#client-客户端)
3. [Config 配置](#clientconfig-配置)
4. [Request 请求构建器](#request-请求构建器)
5. [Response 响应封装](#response-响应封装)
6. [Interceptor 拦截器](#interceptor-拦截器)
7. [RetryConfig 重试配置](#retryconfig-重试配置)
8. [便捷函数](#便捷函数)
9. [完整使用示例](#完整使用示例)
---
## 快速开始
### 基本 GET 请求
```go
client := httpclient.NewClient(nil)
resp, err := client.Get("http://example.com/api/users").Do()
if err != nil {
log.Fatal(err)
}
// 响应体已在 Do() 内部读取并关闭,无需手动 Close()
// 直接使用 bodyBytes
var users []User
err = resp.ToJSON(&users)
```
### POST JSON 请求
```go
resp, err := client.Post("http://example.com/api/users").
SetJSONBody(User{Name: "张三", Age: 25}).
Do()
```
### 带认证和重试
```go
client := httpclient.NewClient(&Config{Timeout: 30 * time.Second})
client.Use(httpclient.WithBearerInterceptor("your-token-here"))
client.SetRetry(&httpclient.RetryConfig{
MaxRetries: 3,
RetryDelay: time.Second,
MaxDelay: 10 * time.Second,
RetryableStatus: []int{500, 502, 503, 504},
})
resp, err := client.Get("http://example.com/api/data").Do()
```
---
## Client 客户端
### 创建客户端
```go
// 使用默认配置
client := httpclient.NewClient(nil)
// 使用自定义配置
client := httpclient.NewClient(&httpclient.Config{
Timeout: 30 * time.Second,
ConnectTimeout: 10 * time.Second,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
})
```
### 链式方法
所有 Client 方法都支持链式调用:
```go
client := httpclient.NewClient(nil).
SetBaseURL("http://api.example.com").
SetRetry(&httpclient.RetryConfig{MaxRetries: 3}).
WithAutoCookie(true)
```
---
## Config 配置
| 字段 | 类型 | 说明 | 默认值 |
|------|------|------|--------|
| `Timeout` | `time.Duration` | 整体超时时间 | 30秒 |
| `ConnectTimeout` | `time.Duration` | 连接超时时间 | 10秒 |
| `RequestTimeout` | `time.Duration` | 请求超时时间 | 30秒 |
| `KeepAlive` | `time.Duration` | Keep-Alive超时 | 30秒 |
| `MaxIdleConns` | `int` | 最大空闲连接数 | 100 |
| `IdleConnTimeout` | `time.Duration` | 空闲连接超时 | 90秒 |
| `Proxy` | `string` | 代理地址 | 空 |
| `TLSConfig` | `*tls.Config` | TLS配置 | nil |
| `Cookies` | `[]*http.Cookie` | 默认Cookie | nil |
| `Headers` | `map[string]string` | 默认请求头 | nil |
### 示例
```go
config := &httpclient.Config{
Timeout: 60 * time.Second,
ConnectTimeout: 5 * time.Second,
MaxIdleConns: 50,
IdleConnTimeout: 60 * time.Second,
Proxy: "http://proxy.example.com:8080",
TLSConfig: &tls.Config{
InsecureSkipVerify: true, // 仅测试环境使用
},
Headers: map[string]string{
"User-Agent": "MyApp/1.0",
},
}
client := httpclient.NewClient(config)
```
---
## Client 方法
### SetBaseURL(baseURL string) *Client
设置基础URL,用于拼接相对路径。
```go
client := httpclient.NewClient(nil)
client.SetBaseURL("http://api.example.com")
// 请求会自动拼接为 http://api.example.com/users
req := client.Get("/users")
```
**注意:**
- 会自动去除baseURL末尾的斜杠
- 如果reqURL是完整URL(以http://或https://开头),直接使用reqURL,忽略baseURL
- 有baseURL时,相对路径自动拼接
- **空baseURL时,只接受完整URL**,相对路径会返回错误
**示例:**
```go
client.SetBaseURL("http://api.example.com/v1")
// 相对路径 → 拼接
client.Get("/users") // → http://api.example.com/v1/users
client.Get("users") // → http://api.example.com/v1/users
// 绝对路径(带协议)→ 直接使用
client.Get("http://other.com/api") // → http://other.com/api
// 空baseURL时
client2 := httpclient.NewClient(nil)
// client2.Get("/users") → 错误: Request url is invalid
// client2.Get("http://example.com/users") → 正常
```
### WithAutoCookie(enabled bool) *Client
启用或禁用Cookie自动管理。
```go
// 启用Cookie管理(默认启用)
client.WithAutoCookie(true)
// 禁用Cookie管理
client.WithAutoCookie(false)
```
### IsCookieAutoEnabled() bool
检查Cookie自动管理是否启用。
```go
if client.IsCookieAutoEnabled() {
cookies := client.GetCookies(parseURL)
}
```
### GetCookies(u *url.URL) []*http.Cookie
获取Jar中的所有Cookie。
```go
u, _ := url.Parse("http://example.com")
cookies := client.GetCookies(u)
for _, c := range cookies {
fmt.Printf("Cookie: %s=%s\n", c.Name, c.Value)
}
```
### ClearCookies() *Client
清空所有Cookie。
```go
client.ClearCookies()
```
### SetRetry(config *RetryConfig) *Client
设置全局重试策略。
```go
client.SetRetry(&httpclient.RetryConfig{
MaxRetries: 3,
RetryDelay: time.Second,
MaxDelay: 10 * time.Second,
RetryableStatus: []int{500, 502, 503, 504},
})
```
### Use(interceptors ...Interceptor) *Client
添加拦截器。
```go
client.Use(&httpclient.LoggingInterceptor{
Logger: func(format string, args ...interface{}) {
log.Printf(format, args...)
},
})
client.Use(httpclient.WithBearerInterceptor("token"))
```
### HTTP方法快捷方式
```go
client.Get(url string) // GET请求
client.Post(url string) // POST请求
client.Put(url string) // PUT请求
client.Delete(url string) // DELETE请求
client.Patch(url string) // PATCH请求
client.Request(method, url) // 通用请求
```
---
## Request 请求构建器
### 方法链
所有Request方法都支持链式调用:
```go
resp, err := client.Post("http://example.com/api/goods/{status}").
SetHeader("X-Request-ID", "12345").
SetHeader("Authorization", "Bearer token").
SetQueryParam("page", 1).
SetQueryParam("limit", 20).
SetPathParam("status", "all").
SetJSONBody(user).
SetTimeout(30 * time.Second).
Do()
```
### GetParams(pos ...string) map[string]interface{}
获取请求参数,用于调试。**必须在Do()之前调用。**
```go
// 获取全部8类参数
all := req.GetParams()
// 结果: {"header": {...}, "query": {...}, "path": {...}, "body": {...}, "json": {...}, "xml": {...}, "form": {...}, "cookie": [...]}
// 获取单个参数(只返回首个有效参数)
query := req.GetParams("query")
// 结果: {"query": {"template_name": "yykweishop", "weapp_pages": "index", ...}}
// 无效参数或空参数,返回全部
req.GetParams() // 返回全部
req.GetParams("") // 返回全部
req.GetParams("invalid") // 返回全部
```
**参数类型:**
| 参数 | 说明 | 示例 |
|------|------|------|
| `header` | 请求头 | `{"Content-Type": "application/json"}` |
| `query` | URL查询参数(自动从URL解析) | `{"page": "1", "limit": "20"}` |
| `path` | 路径参数 | `{"id": "123"}` |
| `body` | 原始body(延迟解析) | `{"name": "test"}` |
| `json` | JSON解析后的body | `{"name": "test"}` |
| `xml` | XML解析后的body | `test` |
| `form` | 表单解析后的body | `{"username": "test"}` |
| `cookie` | Cookie列表(Data为key-value) | `[{"data": {"session": "abc"}, "path": "/", ...}]` |
**注意:**
- `Do()` 调用后 `GetParams()` 失效,防止Request复用导致参数堆积
- URL中的query参数(如 `?template=shop`)会自动解析
- body延迟解析:首次调用GetParams时才会解析
### GetRequestPath() string
获取请求路径(不含查询参数)。
```go
req := client.Get("/api/users?id=123")
req.GetRequestPath() // "/api/users"
```
### GetRequestURI() string
获取完整请求URI(包含查询参数)。
```go
req := client.Get("/api/users?id=123")
req.GetRequestURI() // "/api/users?id=123"
```
### SetHeader(key, value string) *Request
设置单个请求头。
```go
req.SetHeader("Content-Type", "application/json")
req.SetHeader("Authorization", "Bearer token")
req.SetHeader("X-Custom-Header", "value")
```
### SetHeaders(headers map[string]string) *Request
批量设置请求头。
```go
req.SetHeaders(map[string]string{
"Content-Type": "application/json",
"Accept": "application/json",
"X-Request-ID": "12345",
})
```
### SetQueryParam(key string, value interface{}) *Request
设置查询参数。
```go
req.SetQueryParam("page", 1)
req.SetQueryParam("limit", "20")
req.SetQueryParam("sort", "name")
```
### SetQueryParams(params map[string]interface{}) *Request
批量设置查询参数。
```go
req.SetQueryParams(map[string]interface{}{
"page": 1,
"limit": 20,
"sort": "name",
"filter": "active",
})
```
**注意:** value会自动转换为字符串
### SetPathParam(key, value string) *Request
设置路径参数(用于RESTful URL)。
```go
// URL: /users/{id}
req.SetPathParam("id", "123")
// 实际请求: /users/123
```
### SetPathParams(params map[string]string) *Request
批量设置路径参数。
```go
// URL: /users/{id}/posts/{postId}
req.SetPathParams(map[string]string{
"id": "123",
"postId": "456",
})
```
### SetBody(body interface{}) *Request
设置请求体(原始类型)。
```go
req.SetBody("raw string body")
req.SetBody([]byte("byte array body"))
```
**支持的body类型:**
- `string` - 直接作为请求体
- `[]byte` - 直接作为请求体
- `io.Reader` - 读取作为请求体
- 其他类型 - 需要配合Content-Type使用
### SetJSONBody(body interface{}) *Request
设置JSON请求体,自动设置Content-Type为application/json。
```go
data := map[string]interface{}{
"name": "张三",
"age": 25,
}
req.SetJSONBody(data)
// 或者使用结构体
type User struct {
Name string `json:"name"`
Age int `json:"age"`
}
req.SetJSONBody(User{Name: "张三", Age: 25})
```
### SetXMLBody(body interface{}) *Request
设置XML请求体,自动设置Content-Type为application/xml。
```go
type User struct {
XMLName xml.Name `xml:"user"`
Name string `xml:"name"`
Age int `xml:"age"`
}
req.SetXMLBody(User{Name: "张三", Age: 25})
```
### SetFormBody(data map[string]string) *Request
设置表单请求体,自动设置Content-Type为application/x-www-form-urlencoded。
```go
req.SetFormBody(map[string]string{
"username": "testuser",
"password": "password123",
})
```
### SetCookie(cookie *http.Cookie) *Request
添加单个Cookie。
```go
req.SetCookie(&http.Cookie{
Name: "session",
Value: "abc123",
Path: "/",
})
```
### SetCookies(cookies []*http.Cookie) *Request
批量添加Cookie。
```go
req.SetCookies([]*http.Cookie{
{Name: "a", Value: "1"},
{Name: "b", Value: "2"},
})
```
### SetContext(ctx context.Context) *Request
设置请求上下文,用于取消请求或传递请求级数据。
```go
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
req.SetContext(ctx)
resp, err := req.Do()
```
### SetTimeout(timeout time.Duration) *Request
设置单次请求超时(覆盖客户端配置)。
```go
req.SetTimeout(10 * time.Second)
```
### WithRetry(config *RetryConfig) *Request
为单个请求设置重试策略(覆盖客户端配置)。
```go
req.WithRetry(&httpclient.RetryConfig{
MaxRetries: 5,
RetryDelay: 2 * time.Second,
RetryableStatus: []int{500, 502, 503},
})
```
### Do() (*Response, error)
执行请求。**注意:Do() 会自动读取并关闭响应体,用户无需手动处理。**
```go
resp, err := client.Get("http://example.com").Do()
if err != nil {
// 处理错误
return
}
// 响应体已在 Do() 内部读取并关闭,直接使用响应内容
body := resp.ToBytes()
fmt.Println(string(body))
```
### DoContext(ctx context.Context) (<-chan *Response, <-chan error)
异步执行请求,支持context取消。返回两个chan,任一先到达即完成。
```go
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
respCh, errCh := client.Get("http://example.com/api").
DoContext(ctx)
select {
case resp := <-respCh:
err := <-errCh
if err != nil {
// 请求失败
return
}
// 处理响应
fmt.Println(resp.StatusCode)
case <-ctx.Done():
// context取消触发
log.Println("请求被取消:", ctx.Err())
}
```
**使用场景:**
- 需要同时监听"请求完成"和"外部信号取消"
- 请求时间不确定,需要主动中断
- 等待多个请求时选择最快返回的那个
**注意:** context取消后,请求可能已完成或正在完成,errCh会收到`context.Canceled`
---
## Response 响应封装
### ToBytes() []byte
获取响应体字节( 还有一个 GetRaw()[]byte 别名函数)。
```go
body := resp.ToBytes()
```
### ToString() string
获取响应体字符串。
```go
text := resp.ToString()
```
### ToJSON(v interface{}) error
解析JSON响应。
```go
var result map[string]interface{}
err := resp.ToJSON(&result)
if err != nil {
log.Fatal(err)
}
fmt.Println(result["name"])
// 使用结构体
type User struct {
ID int `json:"id"`
Name string `json:"name"`
}
var user User
resp.ToJSON(&user)
```
### ToXML(v interface{}) error
解析XML响应。
```go
type User struct {
Name string `xml:"name"`
Age int `xml:"age"`
}
var user User
err := resp.ToXML(&user)
```
### GetCost() time.Duration
获取请求耗时。
```go
cost := resp.GetCost()
fmt.Printf("请求耗时: %v\n", cost)
```
### IsSuccess() bool
判断是否成功(2xx状态码)。
```go
if resp.IsSuccess() {
fmt.Println("请求成功")
} else {
fmt.Printf("请求失败,状态码: %d\n", resp.StatusCode)
}
```
### 原生http.Response
Response嵌入了`*http.Response`,可以直接访问:
```go
fmt.Println(resp.Status) // "200 OK"
fmt.Println(resp.StatusCode) // 200
fmt.Println(resp.Header.Get("Content-Type"))
```
---
## Interceptor 拦截器
### LoggingInterceptor
日志拦截器,记录请求和响应信息。
```go
client.Use(&httpclient.LoggingInterceptor{
Logger: func(format string, args ...interface{}) {
log.Printf("[HTTP] "+format, args...)
},
})
```
**输出示例:**
```
[HTTP] Request: GET https://api.example.com/users, Cost: 125.5ms, Status: 200, Error:
```
### RetryInterceptor
自动重试拦截器,基于成功条件重试。
```go
client.Use(&httpclient.RetryInterceptor{
MaxRetries: 3,
RetryDelay: time.Second,
})
```
**注意:** 此拦截器在响应`IsSuccess()`为true时停止重试
### AuthInterceptor
认证拦截器,自动添加认证头。
```go
// Bearer Token
client.Use(httpclient.WithBearerInterceptor("your-token-here"))
// Basic Auth
client.Use(httpclient.WithBasicInterceptor("username:password"))
// 自定义头
client.Use(httpclient.WithCustomAuthInterceptor("X-API-Key", "your-api-key"))
```
#### AuthInterceptor 字段
| 字段 | 类型 | 说明 |
|------|------|------|
| `Token` | `string` | Token值 |
| `TokenType` | `string` | Token类型(Bearer、Basic等),空则直接使用Token |
| `TokenKey` | `string` | 请求头名称,默认"Authorization" |
#### AuthInterceptor 示例
```go
auth := &httpclient.AuthInterceptor{
Token: "mytoken",
TokenType: "Bearer",
TokenKey: "Authorization",
}
client.Use(auth)
// 请求头: Authorization: Bearer mytoken
```
**前缀处理:**
- 如果Token已包含前缀(如"Bearer token"),不会重复添加
- TokenType大小写不敏感("bearer"和"Bearer"效果相同)
---
## RetryConfig 重试配置
### 字段说明
| 字段 | 类型 | 说明 |
|------|------|------|
| `MaxRetries` | `int` | 最大重试次数 |
| `RetryDelay` | `time.Duration` | 初始重试延迟 |
| `MaxDelay` | `time.Duration` | 最大重试延迟 |
| `RetryConditions` | `[]func(*httpclient.Response, error) bool` | 自定义重试条件 |
| `RetryableStatus` | `[]int` | 需要重试的HTTP状态码 |
### 重试条件优先级
1. **自定义条件优先**:每行代码会按顺序检查RetryConditions
2. **状态码检查**:如果自定义条件都不满足,检查RetryableStatus
### 自定义重试条件示例
```go
config := &httpclient.RetryConfig{
MaxRetries: 3,
RetryDelay: time.Second,
MaxDelay: 10 * time.Second,
RetryConditions: []func(*httpclient.Response, error) bool{
// 当响应包含特定错误码时重试
func(r *httpclient.Response, err error) bool {
if err != nil {
return true // 网络错误重试
}
if r != nil && r.StatusCode == 429 { // 限流重试
return true
}
return false
},
},
}
```
### 指数退避
```go
config := &httpclient.RetryConfig{
MaxRetries: 5,
RetryDelay: time.Second, // 第1次重试: 1s
MaxDelay: 30 * time.Second, // 最大延迟30s
RetryableStatus: []int{500, 502, 503, 504},
}
// 重试延迟: 1s -> 2s -> 4s -> 8s -> 16s (达到MaxDelay后不再增长)
```
### 请求级重试覆盖
```go
// 特定请求使用不同的重试配置
client.SetRetry(&httpclient.RetryConfig{MaxRetries: 1}) // 客户端默认重试1次
// 某个请求重试5次
client.Get("http://example.com").
WithRetry(&httpclient.RetryConfig{MaxRetries: 5}).
Do()
```
---
## 便捷函数
### 包级函数
使用全局默认客户端,适合简单场景:
```go
// GET请求
resp, err := Get("http://example.com/api/data")
// POST JSON请求
resp, err := PostJSON("http://example.com/api/users", map[string]string{
"name": "张三",
})
// 带选项的请求
resp, err := Get("http://example.com/api/data",
func(r *httpclient.Request) {
r.SetHeader("Authorization", "Bearer token")
},
)
```
**注意:** 包级函数使用DefaultClient,共享配置和拦截器
### DefaultClient
全局默认客户端实例:
```go
var DefaultClient = httpclient.NewClient(nil)
// 可以在启动时配置全局客户端
func init() {
DefaultClient.Use(httpclient.WithBearerInterceptor("global-token"))
}
```
---
## 完整使用示例
### 示例1: 完整的RESTful API调用
```go
// 创建客户端
client := httpclient.NewClient(&httpclient.Config{
Timeout: 30 * time.Second,
ConnectTimeout: 10 * time.Second,
})
// 添加日志拦截器
client.Use(&httpclient.LoggingInterceptor{
Logger: func(format string, args ...interface{}) {
log.Printf(format, args...)
},
})
// 添加认证
client.Use(httpclient.WithBearerInterceptor("your-access-token"))
// 设置基础URL
client.SetBaseURL("https://api.example.com/v1")
// GET - 获取列表
listResp, err := client.Get("/users").
SetQueryParam("page", 1).
SetQueryParam("limit", 20).
Do()
if err != nil {
log.Fatal(err)
}
// 响应体已在 Do() 内部读取并关闭
var users []User
if err := listResp.ToJSON(&users); err != nil {
log.Fatal(err)
}
// GET - 获取单个
userResp, err := client.Get("/users/{id}").
SetPathParam("id", "123").
Do()
if err != nil {
log.Fatal(err)
}
// 响应体已在 Do() 内部读取并关闭
var user User
userResp.ToJSON(&user)
// POST - 创建
createResp, err := client.Post("/users").
SetJSONBody(map[string]interface{}{
"name": "张三",
"email": "zhangsan@example.com",
}).
Do()
if err != nil {
log.Fatal(err)
}
// 响应体已在 Do() 内部读取并关闭
var created User
createResp.ToJSON(&created)
// PUT - 更新
updateResp, err := client.Put("/users/{id}").
SetPathParam("id", "123").
SetJSONBody(map[string]interface{}{
"name": "李四",
}).
Do()
if err != nil {
log.Fatal(err)
}
// 响应体已在 Do() 内部读取并关闭
// DELETE - 删除
_, err = client.Delete("/users/{id}").
SetPathParam("id", "123").
Do()
if err != nil {
log.Fatal(err)
}
```
### 示例2: 带重试的请求
```go
client := httpclient.NewClient(nil)
client.SetRetry(&httpclient.RetryConfig{
MaxRetries: 3,
RetryDelay: time.Second,
MaxDelay: 10 * time.Second,
RetryableStatus: []int{500, 502, 503, 504, 429},
RetryConditions: []func(*httpclient.Response, error) bool{
func(r *httpclient.Response, err error) bool {
// 网络错误重试
if err != nil {
return true
}
// 特定业务错误码重试
if r != nil {
var result map[string]interface{}
if err := r.ToJSON(&result); err == nil {
if code, ok := result["error_code"].(float64); ok && code == 1001 {
return true
}
}
}
return false
},
},
})
resp, err := client.Get("http://example.com/api/unstable").Do()
```
### 示例3: Cookie管理
```go
client := httpclient.NewClient(nil)
client.SetBaseURL("http://example.com")
// 登录并自动保存Cookie
loginResp, err := client.Post("/api/login").
SetJSONBody(map[string]string{
"username": "user",
"password": "pass",
}).
Do()
// 服务器设置的Cookie会自动保存到Jar
// 后续请求自动带上Cookie
profileResp, err := client.Get("/api/profile").Do()
// Cookie会自动从Jar中读取并发送
```
### 示例4: 表单上传
```go
client := httpclient.NewClient(nil)
// Form提交
resp, err := client.Post("http://example.com/api/submit").
SetFormBody(map[string]string{
"username": "testuser",
"password": "password",
}).
Do()
// XML Body
type Request struct {
XMLName xml.Name `xml:"request"`
Action string `xml:"action"`
Data string `xml:"data"`
}
resp, err = client.Post("http://example.com/api/xml").
SetXMLBody(Request{Action: "query", Data: "test"}).
Do()
```
### 示例5: 超时和上下文
```go
client := httpclient.NewClient(nil)
// 使用请求级超时
resp, err := client.Get("http://example.com/api/slow").
SetTimeout(5 * time.Second).
Do()
// 使用DoContext监听取消
ctx, cancel := context.WithCancel(context.Background())
go func() {
time.Sleep(10 * time.Second)
cancel() // 取消请求
}()
respCh, errCh := client.Get("http://example.com/api/data").
DoContext(ctx)
select {
case resp := <-respCh:
err := <-errCh
if err != nil {
log.Println("请求失败:", err)
return
}
// 处理响应
fmt.Println(resp.StatusCode)
case <-ctx.Done():
log.Println("请求被取消:", ctx.Err())
}
```
### 示例6: 批量请求
```go
client := httpclient.NewClient(nil)
client.SetBaseURL("http://api.example.com")
// 收集请求
requests := []*httpclient.Request{
client.Get("/user/1"),
client.Get("/user/2"),
client.Get("/user/3"),
}
// 并发执行
type Result struct {
User *User
Error error
}
results := make(chan Result, len(requests))
for _, req := range requests {
r := req
go func() {
resp, err := r.Do()
if err != nil {
results <- Result{Error: err}
return
}
// 响应体已在 Do() 内部读取并关闭,无需手动 Close()
var user User
if err := resp.ToJSON(&user); err != nil {
results <- Result{Error: err}
return
}
results <- Result{User: &user}
}()
}
// 收集结果
for i := 0; i < len(requests); i++ {
result := <-results
if result.Error != nil {
log.Printf("请求失败: %v", result.Error)
} else {
fmt.Printf("用户: %s\n", result.User.Name)
}
}
```
---
## URL构建规则
### baseURL拼接规则
| baseURL | reqURL | 结果 | 说明 |
|---------|--------|------|------|
| `http://api.example.com` | `/users` | `http://api.example.com/users` | 相对路径,拼接 |
| `http://api.example.com` | `users` | `http://api.example.com/users` | 相对路径,拼接 |
| `http://api.example.com` | `http://other.com` | `http://other.com` | 完整URL,忽略baseURL |
| `http://api.example.com/` | `/users` | `http://api.example.com/users` | 末尾斜杠自动去除 |
| `` (空) | `http://example.com` | `http://example.com` | 空baseURL,完整URL正常 |
| `` (空) | `/users` | 错误 | 空baseURL,相对路径报错 |
### 路径参数替换
```go
// URL模板: /users/{id}/posts/{postId}
req.SetPathParams(map[string]string{
"id": "123",
"postId": "456",
})
// 结果: /users/123/posts/456
```
### 查询参数追加
```go
// 已有URL: /users?name=张三
req.SetQueryParam("age", "25")
// 结果: /users?name=张三&age=25
```
---
## 错误处理
### Request.err - URL构建错误
```go
client := httpclient.NewClient(nil)
client.SetBaseURL("") // 不设置baseURL
req := client.Get("/relative/path")
if req.err != nil {
fmt.Println("URL构建失败:", req.err)
}
```
### Do()返回的错误
```go
resp, err := client.Get("http://example.com").Do()
if err != nil {
// 网络错误
fmt.Println("网络错误:", err)
// 请求错误(状态码 >= 400)
if resp != nil {
fmt.Println("HTTP错误:", resp.StatusCode)
}
}
```
### 常见错误类型
| 错误 | 说明 | 处理方式 |
|------|------|----------|
| `context.DeadlineExceeded` | 请求超时 | 增加超时时间 |
| `net.ErrConnectionRefused` | 连接被拒绝 | 检查服务端是否启动 |
| `Request url is invalid` | URL无效 | 检查baseURL和路径 |
| `unsupported body type` | 不支持的body类型 | 设置正确的Content-Type |
---
## 测试示例
### 使用httptest
```go
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 验证请求
if r.Method != "POST" {
t.Errorf("期望POST请求,实际: %s", r.Method)
}
// 验证请求头
if r.Header.Get("Content-Type") != "application/json" {
t.Errorf("Content-Type错误")
}
// 验证查询参数
if r.URL.Query().Get("page") != "1" {
t.Errorf("page参数错误")
}
// 返回响应
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"success": true}`))
}))
defer server.Close()
// 发送请求
client := httpclient.NewClient(nil)
resp, err := client.Post(server.URL+"/api").
SetQueryParam("page", 1).
SetJSONBody(map[string]string{"name": "test"}).
Do()
if err != nil {
t.Fatal(err)
}
// 注意: 响应体已在 Do() 内部读取并关闭,无需手动 Close()
if !resp.IsSuccess() {
t.Errorf("请求失败: %d", resp.StatusCode)
}
```
---
## 性能考虑
### 连接池
```go
client := httpclient.NewClient(&httpclient.Config{
MaxIdleConns: 100, // 最大空闲连接
IdleConnTimeout: 90 * time.Second,
})
```
### Cookie Jar复用
```go
// 全局复用单个客户端
var sharedClient = httpclient.NewClient(nil)
func API_CALL() {
resp, _ := sharedClient.Get("http://api.example.com/data")
// Cookie会自动复用
}
```
---
## 与标准库http.Client对比
| 特性 | 标准库 | 本客户端 |
|------|--------|----------|
| 链式调用 | ❌ | ✅ |
| 拦截器 | ❌ | ✅ |
| 重试机制 | ❌ | ✅ |
| Cookie管理 | 需手动 | ✅ 自动 |
| URL构建 | 手动拼接 | ✅ 自动化 |
| 响应解析 | 手动json.Unmarshal | ✅ ToJSON/ToXML |
| BaseURL | 需手动处理 | ✅ 内置 |