2. HTTP 请求基础:net/http 包详解

在 Go 语言中,net/http 标准库是构建 HTTP 客户端与服务端的核心工具。本章聚焦于客户端侧的 HTTP 请求构建与发送,涵盖从基础 GET/POST 到高级超时控制与请求头定制的完整链路。掌握这些基础是后续实现业务回放接口编排的基石。

2.1 net/http 包核心类型

在发起任何 HTTP 请求前,需要理解 net/http 包中的三个核心类型:

类型作用关键字段 / 方法
http.ClientHTTP 客户端,管理连接池、超时与重定向策略Timeout、Transport、CheckRedirect
http.Request封装请求方法、URL、请求头、BodyMethod、URL、Header、Body
http.Response封装服务端返回的状态码、响应头、BodyStatusCode、Header、Body
最佳实践: 不要使用 http.DefaultClient 直接发起生产请求。默认客户端不设置超时,可能导致 goroutine 泄漏。始终创建自定义 http.Client 实例并配置 Timeout。

2.2 创建 HTTP 客户端与发送 GET 请求

以下示例演示如何创建一个带超时的 HTTP 客户端,并发送 GET 请求获取资源:

package main

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

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

    // 构建 GET 请求
    req, err := http.NewRequest("GET", "https://api.example.com/users", nil)
    if err != nil {
        fmt.Println("创建请求失败:", err)
        return
    }

    // 设置请求头
    req.Header.Set("Accept", "application/json")
    req.Header.Set("User-Agent", "GoClient/1.0")

    // 发送请求
    resp, err := client.Do(req)
    if err != nil {
        fmt.Println("请求失败:", err)
        return
    }
    defer resp.Body.Close()

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

    fmt.Printf("状态码: %d\n", resp.StatusCode)
    fmt.Printf("响应体: %s\n", string(body))
}
关键点: 使用 http.NewRequest 预构建请求对象,可以灵活设置请求头、超时上下文,并复用客户端实例。每次请求后必须调用 resp.Body.Close() 以释放连接。

2.3 发送 POST 请求与表单数据

POST 请求通常用于提交 JSON 或表单数据。Go 提供了两种便捷方式:

方式适用场景示例
http.Post快速发送简单 POST,无需自定义请求头http.Post(url, "application/json", body)
http.PostForm发送 application/x-www-form-urlencoded 表单http.PostForm(url, url.Values{"key": {"value"}})
client.Do + http.NewRequest需要自定义请求头、超时或复杂 Body 时手动构建 http.Request 并设置 Body

以下为使用 client.Do 发送 JSON POST 请求的完整示例:

package main

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

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

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

    user := User{Name: "张三", Email: "zhangsan@example.com"}
    jsonData, _ := json.Marshal(user)

    req, err := http.NewRequest("POST", "https://api.example.com/users", bytes.NewReader(jsonData))
    if err != nil {
        fmt.Println("创建请求失败:", err)
        return
    }
    req.Header.Set("Content-Type", "application/json")
    req.Header.Set("Authorization", "Bearer your-token")

    resp, err := client.Do(req)
    if err != nil {
        fmt.Println("请求失败:", err)
        return
    }
    defer resp.Body.Close()

    fmt.Printf("状态码: %d\n", resp.StatusCode)
}

2.4 请求头定制与超时设置

请求头(Header)和超时(Timeout)是业务回放接口编排中最常定制的两个维度。下表总结了常见配置项:

配置项类型说明推荐值
Client.Timeouttime.Duration整个请求(连接 + 读取响应)的超时10s ~ 30s
Transport.TLSHandshakeTimeouttime.DurationTLS 握手超时5s
Transport.IdleConnTimeouttime.Duration连接池中空闲连接的超时90s
Transport.MaxIdleConnsint连接池最大空闲连接数100
Transport.MaxIdleConnsPerHostint每个主机最大空闲连接数10
Request.Header.Set(key, value)string设置单个请求头按需设置
Request.Header.Add(key, value)string添加同名字段(不覆盖)按需设置
避坑指南:
  • Client.Timeout 是整体超时,包含连接、TLS 握手、请求体发送、响应体读取全过程。如果业务需要更细粒度的控制,应使用 context.WithTimeout 结合 req.WithContext。
  • 请求头中的 Host 字段不能通过 Header.Set 修改,必须通过 req.Host 直接赋值。
  • 不要在生产代码中设置 Transport.DisableKeepAlives = true,否则会频繁创建新 TCP 连接,增加延迟。

2.5 HTTP 请求生命周期流程

下图展示了从创建客户端到接收响应的完整流程,以及各阶段超时控制的作用点:

HTTP 请求生命周期与超时控制 创建 http.Client 构建 http.Request client.Do(req) DNS 解析 + 连接 TLS 握手(HTTPS) 发送请求体 读取响应体 关闭 resp.Body Client.Timeout 覆盖整个流程;TLSHandshakeTimeout 仅作用于 TLS 握手阶段

2.6 使用 context 实现精细化超时

当需要为单个请求设置比 Client.Timeout 更精细的超时(例如:连接超时 3 秒,读取超时 7 秒),应使用 context.WithTimeout:

package main

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

func main() {
    client := &http.Client{
        // 不设置全局 Timeout,由 context 控制
        Transport: &http.Transport{
            TLSHandshakeTimeout: 5 * time.Second,
        },
    }

    // 创建带超时的 context
    ctx, cancel := context.WithTimeout(context.Background(), 8*time.Second)
    defer cancel()

    req, _ := http.NewRequestWithContext(ctx, "GET", "https://api.example.com/data", nil)
    req.Header.Set("Accept", "application/json")

    resp, err := client.Do(req)
    if err != nil {
        // 判断是否为超时错误
        if ctx.Err() == context.DeadlineExceeded {
            fmt.Println("请求超时")
        } else {
            fmt.Println("请求失败:", err)
        }
        return
    }
    defer resp.Body.Close()

    body, _ := io.ReadAll(resp.Body)
    fmt.Printf("状态码: %d, 响应长度: %d\n", resp.StatusCode, len(body))
}
进阶技巧: 在业务回放场景中,建议为每个回放请求单独创建 context,并设置与原始请求一致的超时时间。这样可以精确模拟原始请求的延迟特征,避免因超时配置不一致导致回放结果失真。

2.7 常见 HTTP 客户端配置模板

以下是一个适用于业务回放场景的 HTTP 客户端配置模板,可直接用于生产环境:

func NewReplayClient() *http.Client {
    transport := &http.Transport{
        MaxIdleConns:          100,
        MaxIdleConnsPerHost:   10,
        IdleConnTimeout:       90 * time.Second,
        TLSHandshakeTimeout:   5 * time.Second,
        ExpectContinueTimeout: 1 * time.Second,
    }

    return &http.Client{
        Timeout:   30 * time.Second,
        Transport: transport,
        // 禁止自动重定向,回放场景需精确记录每次请求
        CheckRedirect: func(req *http.Request, via []*http.Request) error {
            return http.ErrUseLastResponse
        },
    }
}
配置要点:
  • 连接池复用: MaxIdleConns 和 MaxIdleConnsPerHost 控制连接复用,减少 TCP 握手开销。
  • 禁止自动重定向: 回放场景需要记录原始请求的完整链路,自动跟随重定向会丢失中间状态。
  • 超时兜底: Timeout: 30s 作为全局安全网,防止异常请求长时间挂起。

2.8 本章小结

通过本章学习,你已掌握:

  • net/http 包中 Client、Request、Response 三大核心类型的使用方法。
  • GET 与 POST 请求的构建与发送,包括 JSON 和表单数据的处理。
  • 请求头定制与多层级超时控制(Client.Timeout、Transport 超时、context 超时)。
  • HTTP 请求的完整生命周期及各阶段超时作用点。
  • 适用于业务回放场景的 HTTP 客户端配置模板。