Skip to content
Go
net/http — HTTP 服务器与客户端

net/http — HTTP 服务器与客户端

net/http 是 Go 网络编程中使用频率最高的包。它提供了生产级的 HTTP 客户端和服务器实现。


1. HTTP 服务器

1.1 最小服务器

package main

import "net/http"

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("Hello, World!"))
    })
    http.ListenAndServe(":8080", nil) // nil → 使用 DefaultServeMux
}

1.2 带超时的生产级配置

server := &http.Server{
    Addr:         ":8080",
    Handler:      router,
    ReadTimeout:  5 * time.Second,   // 读取整个请求的超时
    WriteTimeout: 10 * time.Second,   // 写响应的超时
    IdleTimeout:  120 * time.Second,  // Keep-Alive 空闲超时
    MaxHeaderBytes: 1 << 20,          // 1MB
}

// 优雅关闭
go func() {
    sigCh := make(chan os.Signal, 1)
    signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
    <-sigCh

    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()
    server.Shutdown(ctx) // 不再接受新请求,等现有请求完成
}()

if err := server.ListenAndServe(); err != http.ErrServerClosed {
    log.Fatal(err)
}

🚨 陷阱ReadTimeoutWriteTimeout整个请求/响应的超时,而不是单个读写操作的超时。一个慢客户端可能占用连接很长时间。


2. 中间件模式

Go 的 HTTP 中间件是函数装饰器模式的经典应用。

2.1 中间件签名

type Middleware func(http.Handler) http.Handler

2.2 常用中间件实现

// 日志中间件
func LoggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()

        // 包装 ResponseWriter 以获取状态码
        wrapped := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}
        next.ServeHTTP(wrapped, r)

        log.Printf("%s %s %d %v", r.Method, r.URL.Path, wrapped.statusCode, time.Since(start))
    })
}

type responseWriter struct {
    http.ResponseWriter
    statusCode int
}

func (rw *responseWriter) WriteHeader(code int) {
    rw.statusCode = code
    rw.ResponseWriter.WriteHeader(code)
}

// Recovery 中间件(防止 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\n%s", err, debug.Stack())
                http.Error(w, "Internal Server Error", http.StatusInternalServerError)
            }
        }()
        next.ServeHTTP(w, r)
    })
}

// 超时中间件
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))
        })
    }
}

2.3 链式组合

// 方式 1:手动包装
handler := RecoveryMiddleware(
    LoggingMiddleware(
        TimeoutMiddleware(5*time.Second)(
            actualHandler,
        ),
    ),
)

// 方式 2:使用辅助函数
func Chain(h http.Handler, middlewares ...Middleware) http.Handler {
    for i := len(middlewares) - 1; i >= 0; i-- {
        h = middlewares[i](h)
    }
    return h
}

handler := Chain(actualHandler,
    RecoveryMiddleware,
    LoggingMiddleware,
    TimeoutMiddleware(5*time.Second),
)

3. HTTP 客户端

3.1 🔬 深入原理:Transport 连接池

Go 的 HTTP 客户端通过 http.Transport 管理连接池。理解 Transport 的配置是写出高性能 HTTP 客户端的关键。

// 🔬 http.Transport 参数详解
transport := &http.Transport{
    // MaxIdleConns: 总连接池中最大空闲连接数(跨所有 host)
    // 默认值 100;设为 0 表示无限制,但可能导致 fd 耗尽
    MaxIdleConns: 100,

    // MaxIdleConnsPerHost: 每个 host 的最大空闲连接数
    // 应该 >= 你对该 host 的最大并发请求数;默认值 2(太小!生产至少 10+)
    MaxIdleConnsPerHost: 10,

    // MaxConnsPerHost: 每个 host 的最大总连接数(活跃+空闲)
    // 超过此限制时新请求会阻塞等待;设为 0 表示无限制
    MaxConnsPerHost: 20,

    // IdleConnTimeout: 空闲连接在池中保留的最长时间
    // 超过后连接被关闭;默认 0 表示永不过期(小心服务端断开导致写错误)
    IdleConnTimeout: 90 * time.Second,

    // DialContext: 自定义连接建立逻辑(DNS 解析 + TCP 握手)
    // 下面的 Dialer 设置了建连超时 30s + TCP Keep-Alive 30s
    DialContext: (&net.Dialer{
        Timeout:   30 * time.Second,  // 建连总超时(DNS + TCP握手)
        KeepAlive: 30 * time.Second,  // TCP Keep-Alive 探测间隔
    }).DialContext,

    // TLSHandshakeTimeout: TLS 握手超时
    // 只影响 TLS 连接;普通 HTTP 忽略此值
    TLSHandshakeTimeout: 10 * time.Second,

    // ResponseHeaderTimeout: 从发出请求到收到响应头的超时
    // 不包括读取响应体的时间
    ResponseHeaderTimeout: 10 * time.Second,

    // ExpectContinueTimeout: 发送带 "Expect: 100-continue" 头的请求后
    // 等待服务器响应的超时;仅在显式设置请求头时生效
    ExpectContinueTimeout: 1 * time.Second,

    // DisableKeepAlives: 禁用 HTTP Keep-Alive(每次请求新建 TCP 连接)
    // 性能差但适合低频请求或调试
    DisableKeepAlives: false,

    // DisableCompression: 禁用自动 gzip 解压
    // 设为 true 可以手动处理压缩,获取原始响应体
    DisableCompression: false,

    // ForceAttemptHTTP2: 即使没有 TLS 也尝试 HTTP/2(h2c)
    // Go 1.13+ 对 https 自动启用 HTTP/2
    ForceAttemptHTTP2: true,
}

client := &http.Client{
    Transport: transport,
    Timeout:   30 * time.Second, // 整个请求的超时(含读取body)
}

性能提示

  • 多个请求复用同一个 http.Client(它是并发安全的)。
  • MaxIdleConnsPerHost 应该 >= 并发到同一 host 的请求数。
  • 不要每次请求都创建新的 http.Client

3.2 务必使用 NewRequestWithContext

// ❌ 没有超时控制
resp, err := http.Get("https://example.com")

// ✅ 带超时控制
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
req, _ := http.NewRequestWithContext(ctx, "GET", "https://example.com", nil)
resp, err := client.Do(req)
if err != nil {
    // 会捕获超时错误
}

3.3 请求重试模式

func retryableDo(client *http.Client, req *http.Request, retries int) (*http.Response, error) {
    for i := 0; i <= retries; i++ {
        resp, err := client.Do(req)
        if err == nil && resp.StatusCode < 500 {
            return resp, nil
        }
        if resp != nil {
            resp.Body.Close()
        }
        if i < retries {
            time.Sleep(time.Duration(i+1) * 100 * time.Millisecond) // 指数退避
        }
    }
    return nil, fmt.Errorf("failed after %d retries", retries)
}

3.4 断路器模式

断路器防止对故障服务的持续调用(雪崩效应)。

type CircuitBreaker struct {
    mu        sync.RWMutex
    failures  int
    lastFail  time.Time
    threshold int
    timeout   time.Duration
    state     string // "closed", "open", "half-open"
}

func (cb *CircuitBreaker) Call(fn func() (*http.Response, error)) (*http.Response, error) {
    cb.mu.RLock()
    if cb.state == "open" {
        if time.Since(cb.lastFail) > cb.timeout {
            cb.mu.RUnlock()
            cb.mu.Lock()
            cb.state = "half-open"
            cb.mu.Unlock()
        } else {
            cb.mu.RUnlock()
            return nil, errors.New("circuit breaker open")
        }
    } else {
        cb.mu.RUnlock()
    }

    resp, err := fn()
    cb.mu.Lock()
    defer cb.mu.Unlock()

    if err != nil || (resp != nil && resp.StatusCode >= 500) {
        cb.failures++
        cb.lastFail = time.Now()
        if cb.failures >= cb.threshold {
            cb.state = "open"
        }
        return resp, err
    }

    // half-open 状态下成功 → 恢复
    if cb.state == "half-open" {
        cb.state = "closed"
        cb.failures = 0
    }
    return resp, nil
}

4. 🚨 常见陷阱

陷阱 说明
忘记 resp.Body.Close() 连接不会归还池,泄漏
ListenAndServe 返回的 error 被忽略 TLS 证书错误悄悄失败
DefaultServeMux 在生产中使用 全局路由,可能暴露调试端点
ResponseWriter 在 Handler 返回后被使用 异步写响应是未定义行为
写响应后再写 Header Header 已发送,WriteHeader 再调用无效

💡 最佳实践:使用第三方路由库(如 chigorilla/mux)代替 DefaultServeMux;响应写入完成后不要保留对 http.ResponseWriter 的引用。