Go: Go HTTP 客户端与中间件
最后更新:2026-08-26
HTTP 客户端和中间件是微服务通信的两大基石——一个控制连接,一个横切关注点。
当你的服务需要调用其他 5 个下游服务时,每个调用都要做日志、超时、重试、熔断——这些代码写 5 遍还是抽象一次?这节课你将掌握 Go HTTP 客户端的高级用法和中间件设计模式。
1. 你将学到
http.Get/Post/Do基本请求http.Client超时控制http.Transport连接池配置- 中间件模式(洋葱模型)
- 常用中间件:log、恢复、CORS、超时
httputil.ReverseProxy反向代理
2. 一个后端工程师的真实故事
(1) 痛点:5 个下游服务,每个都要写日志+超时+重试
Charlie 负责公司的 API 网关服务,它需要调用 5 个下游服务:
"每个下游调用都要:打印请求日志(方便排查问题)、设置超时(防止卡死)、记录耗时(监控告警)。我写了 5 遍同样的代码——每个服务一个函数,每个函数里都是日志+超时+调用的重复三明治。"
他当时的代码:
GO
// 坏代码:每个下游服务重复同样的逻辑
func callUserService(w http.ResponseWriter, r *http.Request) {
log.Printf("[%s] 请求用户服务: %s", r.Method, r.URL.Path)
start := time.Now()
resp, err := http.Get("http://user-service/api/users")
log.Printf("[%s] 用户服务耗时: %v", r.Method, time.Since(start))
// ... 处理响应
}
func callOrderService(w http.ResponseWriter, r *http.Request) {
log.Printf("[%s] 请求订单服务: %s", r.Method, r.URL.Path)
start := time.Now()
resp, err := http.Get("http://order-service/api/orders")
log.Printf("[%s] 订单服务耗时: %v", r.Method, time.Since(start))
// ... 又是同样的模式!
}
// 每个新服务都重复!
(2) Go 的解法:中间件模式 + 自定义 Client
GO
// middleware:包装 http.RoundTripper
type LoggingMiddleware struct {
next http.RoundTripper
}
func (m *LoggingMiddleware) RoundTrip(req *http.Request) (*http.Response, error) {
start := time.Now()
log.Printf("[%s] %s %s", req.Method, req.URL.Host, req.URL.Path)
resp, err := m.next.RoundTrip(req)
log.Printf("[%s] %s 耗时: %v", req.Method, req.URL.Path, time.Since(start))
return resp, err
}
// 统一 Client:所有下游共享
client := &http.Client{
Timeout: 5 * time.Second,
Transport: &LoggingMiddleware{
next: http.DefaultTransport,
},
}
// 所有下游调用自动获得日志 + 超时
resp1, _ := client.Get("http://user-service/api/users") // 自动日志
resp2, _ := client.Get("http://order-service/api/orders") // 自动日志
(3) 收益:中间件化前后
| 维度 | 重复代码 | 中间件模式 |
|---|---|---|
| 代码量 | 每个下游 30 行,5 个=150 行 | 中间件 15 行,共 30 行 |
| 新增下游 | 复制粘贴 30 行 | 直接调用 client.Get |
| 修改超时 | 改 5 个地方 | 改 1 个 client.Timeout |
3. HTTP 客户端基础
▶ 示例:GET / POST / 自定义请求
GO
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
)
func main() {
// 1. GET 请求
resp, err := http.Get("https://api.example.com/users")
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
fmt.Printf("GET 状态码: %d\n", resp.StatusCode)
fmt.Printf("响应: %s\n", body)
// 2. POST JSON 请求
data := map[string]string{"name": "Alice"}
jsonData, _ := json.Marshal(data)
resp, err = http.Post(
"https://api.example.com/users",
"application/json",
bytes.NewReader(jsonData),
)
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
// 3. 自定义请求(设置 Header)
req, _ := http.NewRequest("DELETE", "https://api.example.com/users/1", nil)
req.Header.Set("Authorization", "Bearer token-123")
req.Header.Set("X-Request-ID", "req-456")
resp, err = http.DefaultClient.Do(req)
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
fmt.Printf("DELETE 状态码: %d\n", resp.StatusCode)
}
(1) http.Get vs http.Client
GO
// 方式 1:直接使用 http.Get(默认 Client)
resp, _ := http.Get(url)
// 问题:默认没有超时,可能永远挂起!
// 方式 2:自定义 Client(推荐)
client := &http.Client{
Timeout: 10 * time.Second,
}
resp, _ := client.Get(url)
// 安全:10 秒超时自动取消
| 方式 | 超时 | 连接池 | 推荐度 |
|---|---|---|---|
http.Get(url) |
无超时 | 默认(复用有限) | ❌ 仅快速测试 |
http.DefaultClient |
无超时 | 默认 | ⚠️ 生产慎用 |
&http.Client{Timeout: 10s} |
全局超时 | 默认 | ✅ 推荐 |
&http.Client{Timeout, Transport} |
全局超时 | 自定义连接池 | ✅ 生产级 |
4. http.Client 超时与 Transport
▶ 示例:配置超时和连接池
GO
package main
import (
"fmt"
"net"
"net/http"
"time"
)
func main() {
// 自定义 Transport
transport := &http.Transport{
// 连接池
MaxIdleConns: 100, // 最大空闲连接数
MaxIdleConnsPerHost: 10, // 每个主机的最大空闲连接数
IdleConnTimeout: 90 * time.Second, // 空闲连接超时
// TLS
TLSHandshakeTimeout: 10 * time.Second,
// join
DialContext: (&net.Dialer{
Timeout: 30 * time.Second, // 连接超时
KeepAlive: 30 * time.Second, // Keep-Alive 间隔
}).DialContext,
}
// 自定义 Client
client := &http.Client{
Timeout: 30 * time.Second, // 请求总超时(包括所有阶段)
Transport: transport,
}
// 使用 client 发送请求
resp, err := client.Get("https://api.example.com/users")
if err != nil {
fmt.Printf("请求失败: %v\n", err)
return
}
defer resp.Body.Close()
fmt.Printf("状态码: %d\n", resp.StatusCode)
}
▶ 示例:超时逐层控制
GO
package main
import (
"fmt"
"net"
"net/http"
"time"
)
func main() {
// 超时分层:
// Transport.DialContext → 连接超时(10s)
// Transport.TLSHandshake → TLS 握手超时(5s)
// Transport.ResponseHeader → 响应头超时(10s)
// Client.Timeout → 总超时(30s,覆盖 Dial + TLS + 发送 + 读取)
transport := &http.Transport{
DialContext: (&net.Dialer{
Timeout: 10 * time.Second,
}).DialContext,
TLSHandshakeTimeout: 5 * time.Second,
ResponseHeaderTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
client := &http.Client{
Timeout: 30 * time.Second,
Transport: transport,
}
start := time.Now()
resp, err := client.Get("https://httpbin.org/delay/5")
if err != nil {
fmt.Printf("错误: %v (耗时: %v)\n", err, time.Since(start))
return
}
defer resp.Body.Close()
fmt.Printf("成功: %d (耗时: %v)\n", resp.StatusCode, time.Since(start))
}
(2) Transport 配置参数
| parameter | 默认值 | 建议值 | 说明 |
|---|---|---|---|
MaxIdleConns |
100 | 100-200 | 全局最大空闲连接 |
MaxIdleConnsPerHost |
2 | 10-50 | 单主机最大空闲连接(默认 2 很低!) |
IdleConnTimeout |
90s | 30-90s | 空闲连接关闭时间 |
TLSHandshakeTimeout |
10s | 5-10s | TLS 握手超时 |
ResponseHeaderTimeout |
0(永不) | 10-30s | 等待响应头超时 |
DialContext.Timeout |
无 | 10-30s | TCP 连接超时 |
💡 提示:
http.Transport 的 MaxIdleConnsPerHost 默认只有 2,对高并发调用同一服务的场景严重不足。务必根据并发量调高(如 50-100)。
5. 中间件模式
▶ 示例:中间件洋葱模型
GO
📖 仅展示
package main
import (
"context"
"fmt"
"log"
"net/http"
"time"
)
// 中间件类型:接收 Handler,返回 Handler
type Middleware func(http.Handler) http.Handler
// 中间件链:将所有中间件组装成一个
func Chain(handler http.Handler, middlewares ...Middleware) http.Handler {
for i := len(middlewares) - 1; i >= 0; i-- {
handler = middlewares[i](handler)
}
return handler
}
// ---------- 具体中间件 ----------
// 1. 日志中间件
func LoggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
log.Printf("[%s] %s %s", r.Method, r.URL.Path, r.RemoteAddr)
next.ServeHTTP(w, r)
log.Printf("[%s] %s 耗时: %v", r.Method, r.URL.Path, time.Since(start))
})
}
// 2. 恢复中间件(防止 panic 导致服务崩溃)
func RecoveryMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
log.Printf("[PANIC] %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
// 3. CORS 中间件
func CORSMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}
// 4. 超时中间件
func TimeoutMiddleware(timeout time.Duration) Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), timeout)
defer cancel()
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
// ---------- 使用 ----------
func helloHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, %s!", r.URL.Query().Get("name"))
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /hello", helloHandler)
// 串联所有中间件(洋葱模型:Recovery → Logging → CORS → Handler)
handler := Chain(mux,
RecoveryMiddleware,
LoggingMiddleware,
CORSMiddleware,
TimeoutMiddleware(5*time.Second),
)
log.Print(http.ListenAndServe(":8080", handler))
}
graph LR
Req[请求] --> R[Recovery]
R --> L[Logging]
L --> C[CORS]
C --> T[Timeout]
T --> H[Handler]
H -->|响应| T
T --> C
C --> L
L --> R
R --> Resp[响应]
(1) 4 个核心中间件职责
| 中间件 | 职责 | 顺序 |
|---|---|---|
| Recovery | 捕获 panic,返回 500 | 最外层 |
| Logging | 记录请求和响应耗时 | 第二层 |
| CORS | 处理跨域请求头 | 第三层 |
| Timeout | 请求超时控制 | 最内层(接近 Handler) |
6. httputil.ReverseProxy 反向代理
▶ 示例:反向代理
GO
package main
import (
"log"
"net/http"
"net/http/httputil"
"net/url"
)
func main() {
// 目标服务地址
target, _ := url.Parse("http://localhost:8081")
// 创建反向代理
proxy := httputil.NewSingleHostReverseProxy(target)
// 自定义 Director(修改请求头)
proxy.Director = func(req *http.Request) {
req.URL.Scheme = target.Scheme
req.URL.Host = target.Host
req.URL.Path = target.Path + req.URL.Path
req.Header.Set("X-Forwarded-Host", req.Header.Get("Host"))
req.Header.Set("X-Real-IP", req.RemoteAddr)
}
// 自定义错误处理
proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
log.Printf("代理错误: %v", err)
http.Error(w, "Bad Gateway", http.StatusBadGateway)
}
mux := http.NewServeMux()
mux.HandleFunc("/api/", proxy.ServeHTTP)
log.Print("API 网关启动于 :8080")
log.Fatal(http.ListenAndServe(":8080", mux))
}
💡 提示:
httputil.ReverseProxy 自动处理了大多数代理细节:Host 头转发、X-Forwarded-For、响应头传递、连接池复用。你只需要提供一个 Director 函数修改请求即可。
7. 完整示例:API 网关 + 中间件管道
GO
// api_gateway.go
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"net/http/httputil"
"net/url"
"os"
"os/signal"
"strings"
"time"
)
// ---------- 配置 ----------
type Route struct {
Path string
Target *url.URL
}
type GatewayConfig struct {
Port string
Routes []Route
Middlewares []Middleware
}
// ---------- 中间件 ----------
type Middleware func(http.Handler) http.Handler
func Chain(handler http.Handler, middlewares ...Middleware) http.Handler {
for i := len(middlewares) - 1; i >= 0; i-- {
handler = middlewares[i](handler)
}
return handler
}
// 日志
func Logging(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
log.Printf("[%s] %s", r.Method, r.URL.Path)
next.ServeHTTP(w, r)
log.Printf("[%s] %s → %v", r.Method, r.URL.Path, time.Since(start))
})
}
// 恢复
func Recovery(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
log.Printf("[PANIC] %v", err)
http.Error(w, `{"error":"internal error"}`, http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
// CORS
func CORS(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}
// 限流(令牌桶简化版)
type RateLimiter struct {
tokens chan struct{}
}
func NewRateLimiter(rate int) *RateLimiter {
rl := &RateLimiter{tokens: make(chan struct{}, rate)}
for i := 0; i < rate; i++ {
rl.tokens <- struct{}{}
}
// 每秒补充
go func() {
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for range ticker.C {
select {
case rl.tokens <- struct{}{}:
default:
}
}
}()
return rl
}
func (rl *RateLimiter) Middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
select {
case <-rl.tokens:
next.ServeHTTP(w, r)
default:
http.Error(w, `{"error":"rate limit exceeded"}`, http.StatusTooManyRequests)
}
})
}
// ---------- 网关 ----------
type Gateway struct {
config GatewayConfig
proxy *httputil.ReverseProxy
}
func NewGateway(config GatewayConfig) *Gateway {
proxy := &httputil.ReverseProxy{
Director: func(req *http.Request) {
// 根据请求路径匹配路由
for _, route := range config.Routes {
if strings.HasPrefix(req.URL.Path, route.Path) {
req.URL.Scheme = route.Target.Scheme
req.URL.Host = route.Target.Host
req.URL.Path = strings.TrimPrefix(req.URL.Path, route.Path)
req.Header.Set("X-Forwarded-Host", req.Header.Get("Host"))
return
}
}
},
ErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) {
log.Printf("代理错误: %v", err)
http.Error(w, `{"error":"bad gateway"}`, http.StatusBadGateway)
},
}
return &Gateway{config: config, proxy: proxy}
}
func (g *Gateway) Start() error {
routesJSON, _ := json.MarshalIndent(g.config.Routes, "", " ")
log.Printf("开始加载路由:\n%s\n", routesJSON)
// 路由分发
mux := http.NewServeMux()
for _, route := range g.config.Routes {
pattern := route.Path
if !strings.HasSuffix(pattern, "/") {
pattern += "/"
}
mux.Handle(pattern, g.proxy)
}
// 健康检查
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
})
// 中间件链
handler := Chain(mux, g.config.Middlewares...)
server := &http.Server{
Addr: ":" + g.config.Port,
Handler: handler,
}
// 优雅关闭
go func() {
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, os.Interrupt)
<-sigCh
log.Println("正在关闭服务...")
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
server.Shutdown(ctx)
}()
log.Printf("API 网关启动于 :%s", g.config.Port)
return server.ListenAndServe()
}
func main() {
userService, _ := url.Parse("http://localhost:8081")
orderService, _ := url.Parse("http://localhost:8082")
config := GatewayConfig{
Port: "8080",
Routes: []Route{
{Path: "/api/users", Target: userService},
{Path: "/api/orders", Target: orderService},
},
}
config.Middlewares = []Middleware{
Recovery,
Logging,
CORS,
}
gateway := NewGateway(config)
if err := gateway.Start(); err != nil && err != http.ErrServerClosed {
log.Fatal(err)
}
}
🔥 易错:
httputil.ReverseProxy 默认会修改 X-Forwarded-For 头。如果你不需要这个行为,在 Director 中删除该头。另外,ReverseProxy 不会自动处理 WebSocket 升级——你需要 httputil.ReverseProxy 的 ServeHTTP 方法不支持 WebSocket,需要单独处理 Upgrade request。
❓ 常见问题
Q http.Get 和 http.Client 怎么选?
A 快速测试用 http.Get。生产环境永远用自定义 http.Client——设置 Timeout 防止请求挂起,设置 Transport 控制连接池和超时细节。http.Get 使用 http.DefaultClient,它没有默认超时。
Q Transport 连接池如何配置?
A 核心参数 MaxIdleConns 和 MaxIdleConnsPerHost。默认 MaxIdleConnsPerHost=2,对高并发服务严重不足。建议设为 50-100(视并发量)。IdleConnTimeout 设为 30-90 秒保持连接活跃。
Q 中间件的执行顺序?
A 洋葱模型——请求从最外层进入,逐层到达 Handler;响应从 Handler 逐层返回最外层。注册顺序决定外层顺序:
Chain(h, A, B, C) → 请求经过 A→B→C→Handler,响应经过 C→B→A。Q httputil.ReverseProxy 适用什么场景?
A API 网关/反向代理/服务路由。它自动处理 Host 头、X-Forwarded-For 头、连接池复用。只需要实现 Director 函数修改请求的 Scheme/Host/Path。适合微服务架构的流量入口。
Q 如何实现请求重试?
A 在 Transport 层实现自定义 RoundTripper,或在 Client 层封装 Do method。注意重试幂等性——只有 GET/HEAD/OPTIONS 等安全方法可以自动重试。POST/PUT 重试前要确认 Body 可以重新读取。
Q http.Client 是并发安全的吗?
A 是。http.Client 可以并发使用(多次调用 Get/Do 不需要额外锁)。但 Transport 的字段在首次使用后不应修改。最佳实践:为每个下游服务集群建一个 Client,不要为每个请求新建 Client。
Q 如何给请求设置 Header?
A 用
http.NewRequest 创建请求后设置 Header:req.Header.Set("Key", "Value")。不要用 req.Header.Add("Key", "Value")——Add 追加而不是覆盖。常见的 Auth/ TraceID/ Content-Type 都在 Header 中传递。📖 小节
- http.Get/Post/Do 三种请求方式
- http.Client 设置 Timeout 防挂起
- http.Transport 连接池配置(MaxIdleConnsPerHost 要调高)
- 中间件模式:洋葱模型,请求从外层到内层
- 4 个核心中间件:Recovery → Logging → CORS → Timeout
- httputil.ReverseProxy 实现反向代理
- API 网关整合中间件链 + 路由分发
📝 作业
-
基础题(难度⭐):写一个自定义 http.Client,设置 5 秒超时时间,MaxIdleConnsPerHost=20。用这个 Client 发送 GET 请求到 https://httpbin.org/delay/3,验证超时效果。
-
进阶题(难度⭐⭐):实现一个请求重试中间件(RoundTripper 层)。要求:(1) 对 GET 请求自动重试最多 3 次;(2) 重试间隔依次为 100ms/200ms/400ms(指数退避);(3) 只重试 5xx response,4xx 不重试;(4) 记录每次重试日志。
-
挑战题(难度⭐⭐⭐):实现一个带负载均衡的反向代理。要求:(1) 支持注册多个后端实例(如 3 个 user-service instance);(2) 轮询(Round-Robin)分发请求;(3) 被动健康检查——连续 3 次失败后摘除实例,恢复后重新加入;(4) 日志记录每个请求分发的后端地址;(5) 用 httputil.ReverseProxy 实现。