3. HTTP 客户端基础:net/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 客户端发送请求的完整生命周期:
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),避免频繁创建新连接带来的延迟。