Go net/http

net/http 以 Handler/Request/ResponseWriter 组织 HTTP 服务端与客户端;理解默认 ServeMux、超时、Context 取消与连接复用,是 Go Web 的第一性原理。

#type / concept #status / growing #tech / dev #tech / dev / backend #resource / go

[!info] 关联笔记

Go net/http

这个概念为什么会出现

Web 与 RPC 边界最终都要回答:

  • 字节流如何变成“一次请求”
  • 业务代码如何接到方法、路径、头、体
  • 响应状态码、头、体如何写回
  • 客户端如何发起请求、复用连接、处理超时

许多语言把这些藏进框架。Go 在标准库用 net/http 给出足够完成生产服务的最小完备抽象HandlerRequestResponseWriterServeMuxServerClient。框架(Gin、Echo、chi)大多是这些接口上的糖与约定。不先吃透 net/http,中间件、超时、取消、测试与优雅停机都会变成黑盒。

[!abstract] 一句话理解 net/http 把 HTTP 建模为 Handler.ServeHTTP(ResponseWriter, *Request):服务端用 Server/Mux 分发,客户端用 Client/Transport 发请求;超时与取消通过 context 和 Server/Client 字段显式配置。

最小可运行示例

先把示例放进业务场景,再看代码:

场景:最小健康问候 API,显式 Server 超时

后端要先起一个可探活的 GET /hello:返回 JSON {"msg":"hello",...}
生产不能只用 http.ListenAndServe(":8080", mux) 裸奔——
慢客户端、慢写回会拖住连接;应持有 http.Server 并设 ReadHeaderTimeout 等边界。

本例会监听 :8080;另开终端 curl 验证。
优雅停机见 go-graceful-shutdown,本例聚焦 Handler + Server 字段。

package main

import (
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"time"
)

// hello 模拟最薄业务:探活 / 问候接口。
//
// 业务意图:GET 返回 JSON;其他方法 405(若未用 1.22 方法路由时仍可手检)。
// 教学点:func(w,r) 经 HandleFunc 变成 Handler;先写 Header 再 Encode body。
func hello(w http.ResponseWriter, r *http.Request) {
	// Go 1.22+ 注册 "GET /hello" 时方法已由 mux 过滤;
	// 保留手检便于对照旧代码与非方法 pattern。
	if r.Method != http.MethodGet {
		http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
		return
	}

	w.Header().Set("Content-Type", "application/json")
	// Encode 会 WriteHeader(200)(若尚未写状态码)。
	_ = json.NewEncoder(w).Encode(map[string]string{
		"msg":  "hello",
		"path": r.URL.Path,
	})
}

func main() {
	mux := http.NewServeMux()
	// Go 1.22+ 方法+路径;旧版本可改 mux.HandleFunc("/hello", hello) 并在内手检 Method。
	mux.HandleFunc("GET /hello", hello)

	// 显式 Server:超时是生产默认配置,不是“优化项”。
	srv := &http.Server{
		Addr:              ":8080",
		Handler:           mux,
		ReadHeaderTimeout: 5 * time.Second,  // 防 Slowloris:头读太久就断
		ReadTimeout:       10 * time.Second, // 整请求读完上限
		WriteTimeout:      10 * time.Second, // 写回上限
		IdleTimeout:       60 * time.Second, // keep-alive 空闲连接
	}

	log.Println("listening on", srv.Addr)
	// 演示阻塞 listen;真实服务常配合 Shutdown(见优雅关闭篇)。
	if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
		log.Fatal(err)
	}
	fmt.Println("bye")
}

建议运行:

go run .
# 另开终端:
curl -s localhost:8080/hello

期望响应:

{"msg":"hello","path":"/hello"}

服务端日志类似 listening on :8080

结合场景再看三个关注点

  1. 业务入口是普通函数
    helloHandlerFunc 适配,中间件/路由都围绕 Handler 组合。

  2. 显式 http.Server 带超时
    问候 API 再小,也要限制头/读/写时间,避免连接被拖死。

  3. 1.22+ pattern 含方法
    "GET /hello" 把方法匹配从业务里挪到路由表;旧代码仍常见 r.Method 手检。

核心概念与准确模型

四个角色

角色职责
http.HandlerServeHTTP(w, r):处理一次请求
http.HandlerFunc函数类型适配器,让 func(w,r) 实现 Handler
http.Request方法、URL、头、Body(io.ReadCloser)、Context()
http.ResponseWriter写头、状态码、响应体;底层常为缓冲写入
type Handler interface {
	ServeHTTP(ResponseWriter, *Request)
}

type HandlerFunc func(ResponseWriter, *Request)

func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
	f(w, r)
}

中间件、鉴权、日志包装的形状全部来自“包一层 Handler 再返回新 Handler”。见 HTTP 中间件

路由:ServeMux

mux := http.NewServeMux()
mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))
mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
	w.WriteHeader(http.StatusOK)
})

Go 1.22 起模式增强(官方文档):

  • "GET /items/{id}":方法匹配 + 路径变量
  • r.PathValue("id") 取变量
  • 更明确的最长匹配与尾部斜杠行为

旧习惯 http.Handle / http.HandleFunc 注册到默认 DefaultServeMux。可运行,但测试隔离与多 Server 场景更推荐自建 Mux

服务端:http.Server

srv := &http.Server{
	Addr:              ":8080",
	Handler:           mux,
	ReadHeaderTimeout: 5 * time.Second, // 防 Slowloris 的基础项
	// ReadTimeout / WriteTimeout / IdleTimeout 按威胁模型配置
}

要点:

  1. ListenAndServe 阻塞;优雅关闭用 Shutdown(ctx)(见 优雅关闭)。
  2. 超时字段语义不同:读头、读体、写响应、空闲 keep-alive 分别控制。
  3. 每个连接的请求在独立 goroutine 中调用 Handler;Handler 必须并发安全。

请求体与响应写出顺序

  • Header().Set,再 WriteHeader(code),再 Write 体。
  • 第一次 Write 若未显式 WriteHeader,会隐式 200
  • 写过 header 后再改状态码无效。
  • Body 必须读完或关闭,否则影响连接复用(客户端尤甚)。

客户端:http.ClientTransport

client := &http.Client{
	Timeout: 10 * time.Second, // 含连接、重定向、读体的总上限(粗粒度)
	Transport: &http.Transport{
		Proxy:                 http.ProxyFromEnvironment,
		MaxIdleConns:          100,
		IdleConnTimeout:       90 * time.Second,
		TLSHandshakeTimeout:   10 * time.Second,
		ExpectContinueTimeout: 1 * time.Second,
	},
}

req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
	return err
}
res, err := client.Do(req)
if err != nil {
	return err
}
defer res.Body.Close()

规则:

  1. 不要每次请求 http.Client{} 新建且丢弃——应复用 Client/Transport 以复用连接池。
  2. http.Get / http.DefaultClient 默认无整体超时,生产慎用。
  3. 取消与截止时间优先挂在 Request.Context()
  4. 必须 Close Body(或 io.Copy(io.Discard, body) 后关闭)以回收连接。

Context 与取消

ctx := r.Context() // 服务端:客户端断开或 Server 取消时 Done
// 下游
req, _ := http.NewRequestWithContext(ctx, ...)

服务端 Handler 应尊重 r.Context():客户端取消后停止重活、释放资源。这与 context优雅关闭 同一条取消链。

测试:httptesthttptest.Server

req := httptest.NewRequest(http.MethodGet, "/hello", nil)
rr := httptest.NewRecorder()
hello(rr, req)
if rr.Code != http.StatusOK { /* ... */ }

集成级可用 httptest.NewServer(handler) 拿真实 URL 做 Client 往返。

设计动机

  1. 接口小、可组合
    一个方法的 Handler 让中间件、路由、文件服务、反向代理都能同一形状嵌套。
  2. 标准库即生产可用
    小服务可以零框架上线;框架成为可选加速器而非必选项。
  3. 显式胜过隐式
    超时、TLS、BaseContext、ConnContext 都是字段/钩子,而不是全局魔法。
  4. io 生态统一
    Body 是 Reader,Handler 可流式处理大文件与 SSE。

边界情况与反直觉行为

1. ListenAndServe(":8080", nil) 用的是 DefaultServeMux

全局注册易在测试与多包 init 中“串味”。偏好显式 Mux。

2. 写响应不是事务

可能已经写出部分 body 后下游失败;状态码无法回滚。流式 API 要提前规划错误策略。

3. http.Error 与已写 header

http.Error 会写纯文本错误体;若你已开始写 JSON,混用会破坏协议。

4. 服务端 WriteTimeout 与流式/长连接

SSE、WebSocket 升级、大文件下载与严格 WriteTimeout 可能冲突,需要单独设计。

5. 重定向与 Client

http.Client 默认跟随重定向;敏感头在跨域重定向时的处理有规则,自动化客户端要知情。

6. HTTP/2

HTTPS 下 Go 可自动协商 HTTP/2;h2c 与自定义 Transport 需额外配置。行为依赖 TLS 与版本。

常见误区

[!warning] 常见误区:生产使用无超时 DefaultClient 错误:http.Get(url) 在下游挂死时永久阻塞。
正确:自建带 Timeout 的 Client,或为每个请求设 context 截止时间。

[!warning] 常见误区:忽略 ReadHeaderTimeout 错误:只配业务超时,慢速攻击拖垮连接。
正确:至少为 Server 设置 ReadHeaderTimeout

[!warning] 常见误区:不关闭 Response Body 错误:泄漏连接,池耗尽后“假死”。
正确:defer res.Body.Close(),并读尽或有意丢弃。

[!warning] 常见误区:在 Handler 里用全局可变状态无同步 错误:多请求并发写 map。
正确:不可变配置 + 互斥/channel/请求内局部状态。

[!warning] 常见误区:把框架当唯一 HTTP 知识 错误:只会 c.JSON,不懂 ResponseWriter
正确:先会标准库,再映射框架生命周期。

与相邻概念对比

概念差异
中间件对 Handler 的装饰器链,不是路由本身
路由模式路径参数、子路由、REST 约定的组织方式
JSONBody 编解码;契约在序列化边界
context取消与超时的传播载体
优雅关闭Server.Shutdown 与在途请求收尾
gRPC / 其他协议另一套栈;HTTP 常作健康检查与网关边车

工程实践

  1. 显式 Server + 显式 ServeMux,配置与威胁模型匹配的超时。
  2. Handler 保持薄:解析 → 调 service → 写响应;业务进 service 层(分层)。
  3. 错误响应统一:状态码映射、问题详情/JSON 错误体、日志与对外文案分离。
  4. 日志与 request id:中间件注入,下游从 context 取。
  5. 客户端:进程级共享 Client;按依赖设置独立超时与重试策略(注意非幂等)。
  6. TLS:生产终止于反代或直接 ListenAndServeTLS;证书轮换与 HTTP/2 一并验证。
  7. 测试金字塔httptest 单测 Handler;少量 httptest.Server 测中间件链;契约测状态码与 JSON shape。
  8. 安全基线:限制 Body 大小(http.MaxBytesReader)、校验 Content-Type、防路径穿越的文件服务配置。
func withMaxBody(n int64, next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		r.Body = http.MaxBytesReader(w, r.Body, n)
		next.ServeHTTP(w, r)
	})
}

可验证实验

实验 1:最小 Server

运行上文示例,curl -i http://127.0.x.x:8080/hello,观察状态码与 JSON。

实验 2:方法限制

/helloPOST,确认 405(若用 Go 1.22 方法模式或手写检查)。

实验 3:客户端超时

对故意 time.Sleep 的 handler 用 Client{Timeout: 100 * time.Millisecond},确认返回错误。

实验 4:Context 取消

在 Handler 中 select 等待 r.Context().Done(),用 curl 中途断开,观察服务端退出等待。

实验 5:Body 关闭与复用

用共享 Client 连续请求,对比忘记 Close Body 时连接行为(可用 net/http/httptrace 观察)。

本节总结

  • 本质:HTTP 在 Go 里是 Handler 接口协作,不是框架魔法。
  • 服务端:显式 Server/Mux、超时、并发安全 Handler。
  • 客户端:复用 Client/Transport、设超时、关 Body、用 context。
  • 下一步中间件 组合横切能力;JSON 固化 API 契约;优雅关闭 完成生命周期。

自测题

概念题

  1. HandlerHandlerFunc 的关系是什么?
  2. 为什么生产环境不建议依赖 http.DefaultClient
  3. ReadHeaderTimeout 主要防什么问题?

代码推理题

http.HandleFunc("/a", fa)
go http.ListenAndServe(":8080", nil)
go http.ListenAndServe(":8081", nil)

两个端口的路由表是否独立?为什么?

工程思考题

API 网关后的服务需要 60s 长上传,同时要防慢速攻击。Server 超时字段如何分工配置?

参考答案

展开
  1. HandlerFunc 是函数类型,其 ServeHTTP 方法调用函数本身,从而满足 Handler 接口。
  2. 默认无整体超时,故障依赖可导致 goroutine/连接泄漏式阻塞。
  3. 恶意客户端缓慢发送 headers 占用连接的 Slowloris 类问题。
    代码题:不独立,都挂到 DefaultServeMux 同一路由表。
    工程题:ReadHeaderTimeout 仍短;ReadTimeout 覆盖完整读入预算;WriteTimeout 按响应特征;必要时对上传路由单独 Server 或中间件控制绝对截止时间,并在反代层同步超时。

延伸阅读与资料来源

资料类型支撑
Package net/http标准库文档Server/Client/Mux/Handler
Package net/http/httptest标准库文档Handler 测试
Go 1.22 ServeMux enhancements官方博客方法路由与路径模式
Writing Web Applications官方文章入门 Server
Transport and RoundTripper文档连接池与代理

笔记元信息

  • 建议文件名:go-nethttp.md
  • 所属阶段:Web/后端基础
  • 学习顺序:标准库 HTTP 第一篇
  • 建议下一篇:Go HTTP 中间件Go JSON 与序列化
  • 本篇状态:已深化(结构完整;对齐 Go 1.22+ ServeMux)
创建于 2026/6/20 更新于 2026/7/15