3. HTTP客户端基础

在 Go 语言中,net/http 标准库提供了构建 HTTP 客户端和服务器的核心能力。对于跨服务调用场景,掌握该包的基本使用是后续实现超时控制与重试机制的基石。本节将聚焦于如何使用 net/http 发送 GET 和 POST 请求,并理解其核心组件。

3.1 核心类型与结构

在发起 HTTP 请求前,需要理解三个核心类型:

类型 作用 关键字段/方法
http.Client 发送请求并管理连接池、超时、重定向策略 Timeout、Transport、CheckRedirect
http.Request 封装请求的 URL、方法、Header、Body NewRequest、WithContext、Header.Set
http.Response 封装服务端返回的状态码、Header、Body StatusCode、Body、Header
最佳实践: 不要使用 http.DefaultClient 直接发起生产级请求。它没有设置超时,可能导致 goroutine 泄漏。始终创建自定义 http.Client 实例。

3.2 发送 GET 请求

GET 请求用于从服务端获取资源。以下示例展示了如何携带查询参数和自定义 Header:

package main

import (
    "fmt"
    "io"
    "net/http"
    "time"
)

func main() {
    // 1. 创建自定义 Client,设置 5 秒超时
    client := &http.Client{
        Timeout: 5 * time.Second,
    }

    // 2. 构建请求 URL(带查询参数)
    reqURL := "https://api.example.com/users?page=1&limit=20"

    // 3. 创建 Request 对象
    req, err := http.NewRequest("GET", reqURL, nil)
    if err != nil {
        fmt.Printf("创建请求失败: %v\n", err)
        return
    }

    // 4. 设置请求头
    req.Header.Set("Accept", "application/json")
    req.Header.Set("Authorization", "Bearer your-token-here")

    // 5. 发送请求
    resp, err := client.Do(req)
    if err != nil {
        fmt.Printf("请求执行失败: %v\n", err)
        return
    }
    defer resp.Body.Close()

    // 6. 读取响应体
    body, err := io.ReadAll(resp.Body)
    if err != nil {
        fmt.Printf("读取响应体失败: %v\n", err)
        return
    }

    fmt.Printf("状态码: %d\n", resp.StatusCode)
    fmt.Printf("响应体: %s\n", string(body))
}
常见陷阱: 忘记调用 resp.Body.Close() 会导致连接无法复用,最终耗尽系统文件描述符。即使不读取 Body,也必须关闭。

3.3 发送 POST 请求

POST 请求通常用于向服务端提交数据。根据 Content-Type 的不同,常见的数据格式有 application/json 和 application/x-www-form-urlencoded。

3.3.1 发送 JSON 格式 POST 请求

package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "time"
)

type CreateUserRequest struct {
    Name  string `json:"name"`
    Email string `json:"email"`
}

func main() {
    client := &http.Client{
        Timeout: 5 * time.Second,
    }

    // 1. 序列化请求体为 JSON
    reqBody := CreateUserRequest{
        Name:  "张三",
        Email: "zhangsan@example.com",
    }
    jsonData, _ := json.Marshal(reqBody)

    // 2. 创建 POST 请求,Body 使用 bytes.NewReader
    req, err := http.NewRequest("POST", "https://api.example.com/users", bytes.NewReader(jsonData))
    if err != nil {
        fmt.Printf("创建请求失败: %v\n", err)
        return
    }

    // 3. 设置 Content-Type 为 application/json
    req.Header.Set("Content-Type", "application/json")
    req.Header.Set("Authorization", "Bearer your-token-here")

    resp, err := client.Do(req)
    if err != nil {
        fmt.Printf("请求执行失败: %v\n", err)
        return
    }
    defer resp.Body.Close()

    body, _ := io.ReadAll(resp.Body)
    fmt.Printf("状态码: %d, 响应: %s\n", resp.StatusCode, string(body))
}

3.3.2 发送表单格式 POST 请求

package main

import (
    "fmt"
    "io"
    "net/http"
    "net/url"
    "strings"
    "time"
)

func main() {
    client := &http.Client{
        Timeout: 5 * time.Second,
    }

    // 1. 构建表单数据
    formData := url.Values{}
    formData.Set("username", "zhangsan")
    formData.Set("password", "secret123")

    // 2. 创建 POST 请求,Body 使用 strings.NewReader
    req, err := http.NewRequest("POST", "https://api.example.com/login",
        strings.NewReader(formData.Encode()))
    if err != nil {
        fmt.Printf("创建请求失败: %v\n", err)
        return
    }

    // 3. 设置 Content-Type 为 application/x-www-form-urlencoded
    req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

    resp, err := client.Do(req)
    if err != nil {
        fmt.Printf("请求执行失败: %v\n", err)
        return
    }
    defer resp.Body.Close()

    body, _ := io.ReadAll(resp.Body)
    fmt.Printf("状态码: %d, 响应: %s\n", resp.StatusCode, string(body))
}

3.4 HTTP 请求生命周期

下图展示了使用 net/http 客户端发送请求的完整生命周期:

HTTP 请求生命周期 1. 创建 http.Client 2. 构建 http.Request 3. client.Do(req) 4. 读取 resp.Body 5. defer resp.Body.Close() 关键说明 • Client 可复用,建议全局单例 • Request 每次请求新建 • Do() 是阻塞调用 • 超时在 Client 级别设置 • Body 必须显式关闭 • 连接复用由 Transport 管理 • 错误处理需区分网络错误 与业务错误 • 建议使用 io.ReadAll 读取 小响应体 • 大响应体使用流式读取

3.5 请求与响应处理对比

维度 GET 请求 POST 请求
请求体 通常为 nil 需要构造 io.Reader
Content-Type 无需设置(或设置 Accept) 必须设置(json/form 等)
参数传递 URL 查询参数 请求体或 URL 参数
幂等性 是(安全方法) 否(非安全方法)
重试风险 低(可安全重试) 高(需考虑幂等性)

3.6 常见错误处理模式

在跨服务调用中,错误处理需要区分网络层错误和业务层错误:

resp, err := client.Do(req)
if err != nil {
    // 网络层错误:超时、DNS 解析失败、连接被拒绝等
    // 这类错误适合触发重试
    fmt.Printf("网络错误: %v\n", err)
    return
}
defer resp.Body.Close()

// 业务层错误:根据状态码判断
switch {
case resp.StatusCode >= 200 && resp.StatusCode < 300:
    // 成功,处理响应体
case resp.StatusCode == 429:
    // 限流,需要等待后重试
case resp.StatusCode >= 500:
    // 服务端错误,可以重试
default:
    // 客户端错误(4xx 非 429),不应重试
}
核心要点: 只有网络层错误(如超时、连接重置)和服务端 5xx 错误才适合触发重试。4xx 客户端错误(除 429 限流外)通常不应重试,因为重试只会得到相同的结果。

3.7 连接池与复用

http.Client 底层使用 http.Transport 管理连接池。默认情况下,Transport 会复用 keep-alive 连接,减少 TCP 握手开销。以下参数对跨服务调用的性能至关重要:

Transport 参数 默认值 说明
MaxIdleConns 100 所有 host 的最大空闲连接数
MaxIdleConnsPerHost 2 每个 host 的最大空闲连接数(常需调大)
IdleConnTimeout 90s 空闲连接的超时关闭时间
DisableKeepAlives false 禁用 keep-alive(通常不设置)
性能调优建议: 对于高频调用的下游服务,建议将 MaxIdleConnsPerHost 设置为预期的并发连接数(如 10-50),避免频繁创建新连接带来的延迟。