Go: Go HTTP 客户端与中间件

最后更新:2026-08-26

HTTP 客户端和中间件是微服务通信的两大基石——一个控制连接,一个横切关注点。

当你的服务需要调用其他 5 个下游服务时,每个调用都要做日志、超时、重试、熔断——这些代码写 5 遍还是抽象一次?这节课你将掌握 Go HTTP 客户端的高级用法和中间件设计模式。

1. 你将学到


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.TransportMaxIdleConnsPerHost 默认只有 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))
}
逻辑代码 69 行(超过 40 行限制,仅展示)
100%
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.ReverseProxyServeHTTP 方法不支持 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?
Ahttp.NewRequest 创建请求后设置 Header:req.Header.Set("Key", "Value")。不要用 req.Header.Add("Key", "Value")——Add 追加而不是覆盖。常见的 Auth/ TraceID/ Content-Type 都在 Header 中传递。

📖 小节


📝 作业

  1. 基础题(难度⭐):写一个自定义 http.Client,设置 5 秒超时时间,MaxIdleConnsPerHost=20。用这个 Client 发送 GET 请求到 https://httpbin.org/delay/3,验证超时效果。

  2. 进阶题(难度⭐⭐):实现一个请求重试中间件(RoundTripper 层)。要求:(1) 对 GET 请求自动重试最多 3 次;(2) 重试间隔依次为 100ms/200ms/400ms(指数退避);(3) 只重试 5xx response,4xx 不重试;(4) 记录每次重试日志。

  3. 挑战题(难度⭐⭐⭐):实现一个带负载均衡的反向代理。要求:(1) 支持注册多个后端实例(如 3 个 user-service instance);(2) 轮询(Round-Robin)分发请求;(3) 被动健康检查——连续 3 次失败后摘除实例,恢复后重新加入;(4) 日志记录每个请求分发的后端地址;(5) 用 httputil.ReverseProxy 实现。

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏