# 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 | 需手动处理 | ✅ 内置 |