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)
}🚨 陷阱:
ReadTimeout和WriteTimeout是整个请求/响应的超时,而不是单个读写操作的超时。一个慢客户端可能占用连接很长时间。
2. 中间件模式
Go 的 HTTP 中间件是函数装饰器模式的经典应用。
2.1 中间件签名
type Middleware func(http.Handler) http.Handler2.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 再调用无效 |
💡 最佳实践:使用第三方路由库(如
chi、gorilla/mux)代替DefaultServeMux;响应写入完成后不要保留对http.ResponseWriter的引用。