package httphelper import ( "bytes" "compress/gzip" "encoding/json" "fmt" "io" "net/http" "time" ) // HTTPClient 定义一个通用的 HTTP 客户端结构 type HTTPClient struct { Client *http.Client // 底层的 http.Client 实例 BaseURL string // 基础 URL,所有请求将基于此 URL Headers map[string]string // 默认请求头 } // NewHTTPClient 创建一个新的 HTTPClient 实例 // timeout: 请求超时时间,例如 10 * time.Second // baseURL: 基础 URL,例如 "https://api.example.com" // defaultHeaders: 默认请求头,例如 {"Content-Type": "application/json"} func NewHTTPClient(timeout time.Duration, baseURL string, defaultHeaders map[string]string) *HTTPClient { return &HTTPClient{ Client: &http.Client{ Timeout: timeout, }, BaseURL: baseURL, Headers: defaultHeaders, } } // applyHeaders 为请求应用默认和自定义请求头 func (c *HTTPClient) applyHeaders(req *http.Request, customHeaders map[string]string) { // 应用默认请求头 for key, value := range c.Headers { req.Header.Set(key, value) } // 应用自定义请求头(覆盖默认请求头) for key, value := range customHeaders { req.Header.Set(key, value) } } // doRequest 执行实际的 HTTP 请求 // method: HTTP 方法 (GET, POST, PUT, DELETE等) // path: 请求路径,将与 BaseURL 拼接 // requestBody: 请求体数据,如果为 GET/DELETE 请求则为 nil // customHeaders: 自定义请求头,将覆盖默认请求头 // responseData: 用于存储响应数据的目标结构体(指针类型),如果为 nil 则表示不需要 JSON 解码 // rawResponse: 用于存储原始响应体字节切片(*[]byte),如果为 nil 则表示不需要原始响应 func (c *HTTPClient) DoRequest( method, path string, requestBody interface{}, customHeaders map[string]string, responseData interface{}, rawResponse *[]byte, // 新增参数:指向字节切片的指针,用于存储原始响应 ) (int, error) { // 拼接完整的 URL url := c.BaseURL + path var reqBodyReader io.Reader if requestBody != nil { // 将请求体编码为 JSON jsonBody, err := json.Marshal(requestBody) if err != nil { return -1, fmt.Errorf("json marshal request body failed: %w", err) } reqBodyReader = bytes.NewBuffer(jsonBody) } // 创建新的 HTTP 请求 req, err := http.NewRequest(method, url, reqBodyReader) if err != nil { return -1, fmt.Errorf("create http request failed: %w", err) } // 应用请求头 c.applyHeaders(req, customHeaders) // 默认添加 gzip 接收头 req.Header.Set("Accept-Encoding", "gzip") // 发送请求 resp, err := c.Client.Do(req) if err != nil { return -1, fmt.Errorf("send http request failed: %w", err) } defer resp.Body.Close() // 检查 HTTP 状态码 if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusBadRequest { bodyBytes, _ := io.ReadAll(resp.Body) // 读取错误响应体 return resp.StatusCode, fmt.Errorf("http request failed with status: %d, body: %s", resp.StatusCode, string(bodyBytes)) } // 解码响应(支持 gzip) var reader io.Reader = resp.Body if resp.Header.Get("Content-Encoding") == "gzip" { gzipReader, err := gzip.NewReader(resp.Body) if err != nil { return http.StatusOK, fmt.Errorf("create gzip reader failed: %w", err) } defer gzipReader.Close() reader = gzipReader } // 首先读取整个响应体,然后决定如何处理 bodyBytes, err := io.ReadAll(reader) if err != nil { return http.StatusOK, fmt.Errorf("read response body failed: %w", err) } // 如果提供了原始响应目标,则填充它 if rawResponse != nil { *rawResponse = bodyBytes } // 如果提供了 JSON 解码目标,则尝试解码 if responseData != nil { err = json.Unmarshal(bodyBytes, responseData) // 直接对字节切片使用 Unmarshal if err != nil { return http.StatusOK, fmt.Errorf("json decode response body failed: %w", err) } } return http.StatusOK, nil } // Get 发送 GET 请求 // path: 请求路径 // customHeaders: 自定义请求头 // responseData: 用于存储响应数据的目标结构体(指针类型) func (c *HTTPClient) Get(path string, customHeaders map[string]string, responseData interface{}) (int, error) { return c.DoRequest(http.MethodGet, path, nil, customHeaders, responseData, nil) // rawResponse 传递 nil } // Post 发送 POST 请求 // path: 请求路径 // requestBody: 请求体数据 // customHeaders: 自定义请求头 // responseData: 用于存储响应数据的目标结构体(指针类型) func (c *HTTPClient) Post(path string, requestBody interface{}, customHeaders map[string]string, responseData interface{}) (int, error) { return c.DoRequest(http.MethodPost, path, requestBody, customHeaders, responseData, nil) // rawResponse 传递 nil } // PostWithContentType 发送 POST 请求,支持自定义 Content-Type(如 application/json 或 multipart/form-data) // contentType: 请求体类型(如 "application/json", "multipart/form-data") // requestBody: 请求体(可以是结构体、map、或 multipart/form 格式) // 注意:如果是 multipart/form-data,请确保 requestBody 是 io.Reader 和 contentType 是完整的包含 boundary 的值。 func (c *HTTPClient) PostWithContentType(path string, requestBody interface{}, contentType string, customHeaders map[string]string, responseData interface{}) error { // 拼接 URL url := c.BaseURL + path var reqBodyReader io.Reader switch body := requestBody.(type) { case io.Reader: reqBodyReader = body default: // 默认处理为 JSON jsonBody, err := json.Marshal(body) if err != nil { return fmt.Errorf("json marshal request body failed: %w", err) } reqBodyReader = bytes.NewBuffer(jsonBody) if contentType == "" { contentType = "application/json" } } // 创建请求 req, err := http.NewRequest(http.MethodPost, url, reqBodyReader) if err != nil { return fmt.Errorf("create http request failed: %w", err) } // 设置 Content-Type if contentType != "" { req.Header.Set("Content-Type", contentType) } // 应用其他头部 c.applyHeaders(req, customHeaders) // gzip 支持 req.Header.Set("Accept-Encoding", "gzip") // 发出请求 resp, err := c.Client.Do(req) if err != nil { return fmt.Errorf("send http request failed: %w", err) } defer resp.Body.Close() // 检查状态码 if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusBadRequest { bodyBytes, _ := io.ReadAll(resp.Body) return fmt.Errorf("http request failed with status: %d, body: %s", resp.StatusCode, string(bodyBytes)) } // 读取响应 var reader io.Reader = resp.Body if resp.Header.Get("Content-Encoding") == "gzip" { gzipReader, err := gzip.NewReader(resp.Body) if err != nil { return fmt.Errorf("create gzip reader failed: %w", err) } defer gzipReader.Close() reader = gzipReader } // 首先读取整个响应体,然后决定如何处理 bodyBytes, err := io.ReadAll(reader) if err != nil { return fmt.Errorf("read response body failed: %w", err) } // 如果提供了 JSON 解码目标,则尝试解码 if responseData != nil { err = json.Unmarshal(bodyBytes, responseData) if err != nil { return fmt.Errorf("json decode response body failed: %w", err) } } return nil } // Put 发送 PUT 请求 // path: 请求路径 // requestBody: 请求体数据 // customHeaders: 自定义请求头 // responseData: 用于存储响应数据的目标结构体(指针类型) // returns 状态码 和 错误信息 func (c *HTTPClient) Put(path string, requestBody interface{}, customHeaders map[string]string, responseData interface{}) (int, error) { return c.DoRequest(http.MethodPut, path, requestBody, customHeaders, responseData, nil) // rawResponse 传递 nil } // Delete 发送 DELETE 请求 // path: 请求路径 // customHeaders: 自定义请求头 // responseData: 用于存储响应数据的目标结构体(指针类型) // returns 状态码 和 错误信息 func (c *HTTPClient) Delete(path string, customHeaders map[string]string, responseData interface{}) (int, error) { // DELETE 请求通常没有请求体,但某些 RESTful API 可能支持 return c.DoRequest(http.MethodDelete, path, nil, customHeaders, responseData, nil) // rawResponse 传递 nil } // Patch 发送 PATCH 请求 // path: 请求路径 // requestBody: 请求体数据 // customHeaders: 自定义请求头 // responseData: 用于存储响应数据的目标结构体(指针类型) // returns 状态码 和 错误信息 func (c *HTTPClient) Patch(path string, requestBody interface{}, customHeaders map[string]string, responseData interface{}) (int, error) { return c.DoRequest(http.MethodPatch, path, requestBody, customHeaders, responseData, nil) // rawResponse 传递 nil } // GetRaw 发送 GET 请求并返回原始响应体 // path: 请求路径 // customHeaders: 自定义请求头 // 返回值: 原始响应体字节切片或错误 // 状态码 // 错误信息 func (c *HTTPClient) GetRaw(path string, customHeaders map[string]string) ([]byte, int, error) { var raw []byte // responseData 传递 nil,rawResponse 传递 &raw statusCode, err := c.DoRequest(http.MethodGet, path, nil, customHeaders, nil, &raw) if err != nil { return nil, statusCode, err } return raw, statusCode, nil } // PostRaw 发送 POST 请求并返回原始响应体 // path: 请求路径 // customHeaders: 自定义请求头 // 返回值: 原始响应体字节切片或错误 // 状态码 // 错误信息 func (c *HTTPClient) PostRaw(path string, customHeaders map[string]string) ([]byte, int, error) { var raw []byte // responseData 传递 nil,rawResponse 传递 &raw statusCode, err := c.DoRequest(http.MethodPost, path, nil, customHeaders, nil, &raw) if err != nil { return nil, statusCode, err } return raw, statusCode, nil }