3、Go HTTP客户端基础:net/http包介绍、发送GET请求、发送POST请求、处理响应与错误
在契约压测中,所有接口调用最终都依赖HTTP客户端。Go标准库net/http提供了强大且稳定的客户端实现,是构建压测工具的基石。本章将系统讲解如何使用net/http发送GET/POST请求、处理响应结构以及管理常见错误,为后续编写压测脚本打下坚实基础。
3.1 net/http客户端核心结构
net/http包的核心是http.Client结构体,它负责管理连接池、超时设置和请求重定向策略。压测场景下,必须复用同一个Client实例,避免每次请求都创建新连接。
| 结构体/类型 | 作用 | 压测关键点 |
|---|---|---|
http.Client |
发送HTTP请求并接收响应 | 复用实例、设置超时、控制连接池 |
http.Transport |
底层连接管理(TCP连接池、TLS配置) | 调整MaxIdleConns和MaxConnsPerHost |
http.Request |
封装请求方法、URL、Header、Body | 每次请求新建,避免Body复用 |
http.Response |
封装响应状态码、Header、Body | 必须关闭Body,否则连接泄漏 |
压测最佳实践:始终使用
&http.Client{Timeout: 30 * time.Second}创建客户端,并配置Transport的MaxIdleConnsPerHost为压测并发数,避免端口耗尽。
3.2 发送GET请求
GET请求用于从服务端获取资源。在压测中,GET请求通常用于查询接口、健康检查或数据拉取。以下代码展示了标准GET请求的完整流程:
package main
import (
"fmt"
"io"
"net/http"
"time"
)
func main() {
// 创建复用客户端,设置5秒超时
client := &http.Client{
Timeout: 5 * time.Second,
}
// 构建GET请求
req, err := http.NewRequest("GET", "https://api.example.com/users/123", nil)
if err != nil {
fmt.Printf("创建请求失败: %v\n", err)
return
}
// 设置请求头(模拟真实客户端)
req.Header.Set("User-Agent", "GoPTest/1.0")
req.Header.Set("Accept", "application/json")
// 发送请求
resp, err := client.Do(req)
if err != nil {
fmt.Printf("请求执行失败: %v\n", err)
return
}
defer resp.Body.Close() // 关键:必须关闭Body
// 读取响应体
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))
}
常见陷阱:忘记
defer resp.Body.Close()会导致TCP连接无法复用,压测时很快耗尽系统文件描述符。即使不关心响应体内容,也必须读取并关闭Body。
3.3 发送POST请求
POST请求用于向服务端提交数据。压测中POST请求常用于创建资源、提交表单或发送JSON负载。根据Content-Type不同,Body编码方式也有差异:
| Content-Type | Body编码方式 | 适用场景 |
|---|---|---|
application/json |
json.Marshal + bytes.NewReader |
REST API JSON接口 |
application/x-www-form-urlencoded |
url.Values.Encode + strings.NewReader |
传统表单提交 |
multipart/form-data |
multipart.Writer + bytes.Buffer |
文件上传 |
以下代码演示发送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: 10 * time.Second}
// 构造请求体
user := CreateUserRequest{
Name: "张三",
Email: "zhangsan@example.com",
}
jsonData, _ := json.Marshal(user)
// 创建POST请求
req, err := http.NewRequest("POST", "https://api.example.com/users",
bytes.NewReader(jsonData))
if err != nil {
fmt.Printf("创建请求失败: %v\n", err)
return
}
// 设置JSON Content-Type
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.4 处理响应与错误
HTTP响应包含状态码、Header和Body三部分。压测中需要精确解析每个部分,并区分网络错误与业务错误。
3.4.1 响应结构解析
// 典型响应处理模式
resp, err := client.Do(req)
if err != nil {
// 网络层错误:超时、DNS解析失败、连接拒绝等
fmt.Printf("网络错误: %v\n", err)
return
}
defer resp.Body.Close()
// 1. 检查状态码
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
fmt.Printf("业务错误: 状态码=%d, 响应=%s\n", resp.StatusCode, string(body))
return
}
// 2. 读取Header
contentType := resp.Header.Get("Content-Type")
fmt.Printf("Content-Type: %s\n", contentType)
// 3. 读取Body(限制读取大小防止内存溢出)
body, err := io.ReadAll(io.LimitReader(resp.Body, 10*1024*1024)) // 10MB限制
if err != nil {
fmt.Printf("读取Body失败: %v\n", err)
return
}
3.4.2 常见错误分类
| 错误类型 | 典型场景 | 压测处理策略 |
|---|---|---|
url.Error (超时) |
服务端响应超过Timeout设置 |
记录超时率,调整超时时间或降低并发 |
url.Error (连接拒绝) |
目标端口未监听或防火墙拦截 | 立即停止压测,检查服务状态 |
io.ErrUnexpectedEOF |
响应Body被截断 | 记录为异常响应,计入错误统计 |
net.OpError |
DNS解析失败、TLS握手失败 | 区分瞬时错误与配置错误 |
| HTTP 4xx/5xx | 服务端返回业务错误 | 按状态码分类统计,不视为网络错误 |
压测错误处理原则:网络错误(超时、连接失败)应单独统计为"系统错误";HTTP 4xx/5xx应视为"业务错误"并记录响应体用于分析。两者在压测报告中需要分开展示。
3.5 请求生命周期SVG流程图
下图展示了使用net/http客户端发送一次完整请求的生命周期,包含关键操作节点和错误处理路径:
3.6 压测场景下的最佳实践
在契约压测中,net/http的使用需要遵循以下原则,以确保压测结果的准确性和稳定性:
- 复用Client实例:全局只创建一个
http.Client,所有压测协程共享该实例,充分利用连接池。 - 配置Transport参数:设置
MaxIdleConns(默认100)和MaxIdleConnsPerHost(默认2),压测时建议将MaxIdleConnsPerHost设置为并发数。 - 禁用重定向:压测场景下通常不需要跟随重定向,设置
CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }。 - 超时分层设置:使用
http.Client.Timeout作为总超时,或通过context.WithTimeout实现更精细的超时控制。 - Body必须关闭:即使不读取响应体,也要调用
resp.Body.Close(),否则连接无法归还连接池。
压测客户端初始化模板:
transport := &http.Transport{
MaxIdleConns: 200,
MaxIdleConnsPerHost: 100,
IdleConnTimeout: 90 * time.Second,
DisableCompression: true, // 压测时关闭压缩减少CPU开销
}
client := &http.Client{
Transport: transport,
Timeout: 30 * time.Second,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
3.7 常见错误排查速查表
| 错误信息 | 可能原因 | 解决方案 |
|---|---|---|
dial tcp: i/o timeout |
TCP连接超时,服务端未响应 | 检查服务端负载,增加超时时间 |
connection refused |
端口未监听或服务未启动 | 确认服务端进程是否运行 |
no such host |
DNS解析失败 | 检查域名配置或使用IP直连 |
unexpected EOF |
响应Body被截断 | 检查服务端是否主动关闭连接 |
http: server gave HTTP response to HTTPS client |
协议不匹配 | 检查URL使用http还是https |
tls: first record does not look like a TLS handshake |
HTTPS请求发到了HTTP端口 | 确认端口对应的协议类型 |