2. HTTP 请求基础:net/http 包详解
在 Go 语言中,net/http 标准库是构建 HTTP 客户端与服务端的核心工具。本章聚焦于客户端侧的 HTTP 请求构建与发送,涵盖从基础 GET/POST 到高级超时控制与请求头定制的完整链路。掌握这些基础是后续实现业务回放接口编排的基石。
2.1 net/http 包核心类型
在发起任何 HTTP 请求前,需要理解 net/http 包中的三个核心类型:
| 类型 | 作用 | 关键字段 / 方法 |
|---|---|---|
http.Client | HTTP 客户端,管理连接池、超时与重定向策略 | Timeout、Transport、CheckRedirect |
http.Request | 封装请求方法、URL、请求头、Body | Method、URL、Header、Body |
http.Response | 封装服务端返回的状态码、响应头、Body | StatusCode、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.Timeout | time.Duration | 整个请求(连接 + 读取响应)的超时 | 10s ~ 30s |
Transport.TLSHandshakeTimeout | time.Duration | TLS 握手超时 | 5s |
Transport.IdleConnTimeout | time.Duration | 连接池中空闲连接的超时 | 90s |
Transport.MaxIdleConns | int | 连接池最大空闲连接数 | 100 |
Transport.MaxIdleConnsPerHost | int | 每个主机最大空闲连接数 | 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 请求生命周期流程
下图展示了从创建客户端到接收响应的完整流程,以及各阶段超时控制的作用点:
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 客户端配置模板。