Go net/http
net/http 以 Handler/Request/ResponseWriter 组织 HTTP 服务端与客户端;理解默认 ServeMux、超时、Context 取消与连接复用,是 Go Web 的第一性原理。
[!info] 关联笔记
Go net/http
这个概念为什么会出现
Web 与 RPC 边界最终都要回答:
- 字节流如何变成“一次请求”
- 业务代码如何接到方法、路径、头、体
- 响应状态码、头、体如何写回
- 客户端如何发起请求、复用连接、处理超时
许多语言把这些藏进框架。Go 在标准库用 net/http 给出足够完成生产服务的最小完备抽象:Handler、Request、ResponseWriter、ServeMux、Server、Client。框架(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。
结合场景再看三个关注点
-
业务入口是普通函数
hello经HandlerFunc适配,中间件/路由都围绕Handler组合。 -
显式
http.Server带超时
问候 API 再小,也要限制头/读/写时间,避免连接被拖死。 -
1.22+ pattern 含方法
"GET /hello"把方法匹配从业务里挪到路由表;旧代码仍常见r.Method手检。
核心概念与准确模型
四个角色
| 角色 | 职责 |
|---|---|
http.Handler | ServeHTTP(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 按威胁模型配置
}
要点:
ListenAndServe阻塞;优雅关闭用Shutdown(ctx)(见 优雅关闭)。- 超时字段语义不同:读头、读体、写响应、空闲 keep-alive 分别控制。
- 每个连接的请求在独立 goroutine 中调用
Handler;Handler 必须并发安全。
请求体与响应写出顺序
- 先
Header().Set,再WriteHeader(code),再Write体。 - 第一次
Write若未显式WriteHeader,会隐式200。 - 写过 header 后再改状态码无效。
Body必须读完或关闭,否则影响连接复用(客户端尤甚)。
客户端:http.Client 与 Transport
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()
规则:
- 不要每次请求
http.Client{}新建且丢弃——应复用 Client/Transport 以复用连接池。 http.Get/http.DefaultClient默认无整体超时,生产慎用。- 取消与截止时间优先挂在
Request.Context()。 - 必须
CloseBody(或io.Copy(io.Discard, body)后关闭)以回收连接。
Context 与取消
ctx := r.Context() // 服务端:客户端断开或 Server 取消时 Done
// 下游
req, _ := http.NewRequestWithContext(ctx, ...)
服务端 Handler 应尊重 r.Context():客户端取消后停止重活、释放资源。这与 context、优雅关闭 同一条取消链。
测试:httptest 与 httptest.Server
req := httptest.NewRequest(http.MethodGet, "/hello", nil)
rr := httptest.NewRecorder()
hello(rr, req)
if rr.Code != http.StatusOK { /* ... */ }
集成级可用 httptest.NewServer(handler) 拿真实 URL 做 Client 往返。
设计动机
- 接口小、可组合
一个方法的Handler让中间件、路由、文件服务、反向代理都能同一形状嵌套。 - 标准库即生产可用
小服务可以零框架上线;框架成为可选加速器而非必选项。 - 显式胜过隐式
超时、TLS、BaseContext、ConnContext 都是字段/钩子,而不是全局魔法。 - 与
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 约定的组织方式 |
| JSON | Body 编解码;契约在序列化边界 |
| context | 取消与超时的传播载体 |
| 优雅关闭 | Server.Shutdown 与在途请求收尾 |
| gRPC / 其他协议 | 另一套栈;HTTP 常作健康检查与网关边车 |
工程实践
- 显式
Server+ 显式ServeMux,配置与威胁模型匹配的超时。 - Handler 保持薄:解析 → 调 service → 写响应;业务进 service 层(分层)。
- 错误响应统一:状态码映射、问题详情/JSON 错误体、日志与对外文案分离。
- 日志与 request id:中间件注入,下游从 context 取。
- 客户端:进程级共享 Client;按依赖设置独立超时与重试策略(注意非幂等)。
- TLS:生产终止于反代或直接
ListenAndServeTLS;证书轮换与 HTTP/2 一并验证。 - 测试金字塔:
httptest单测 Handler;少量httptest.Server测中间件链;契约测状态码与 JSON shape。 - 安全基线:限制 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:方法限制
对 /hello 发 POST,确认 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 契约;优雅关闭 完成生命周期。
自测题
概念题
Handler与HandlerFunc的关系是什么?- 为什么生产环境不建议依赖
http.DefaultClient? ReadHeaderTimeout主要防什么问题?
代码推理题
http.HandleFunc("/a", fa)
go http.ListenAndServe(":8080", nil)
go http.ListenAndServe(":8081", nil)
两个端口的路由表是否独立?为什么?
工程思考题
API 网关后的服务需要 60s 长上传,同时要防慢速攻击。Server 超时字段如何分工配置?
参考答案
展开
HandlerFunc是函数类型,其ServeHTTP方法调用函数本身,从而满足Handler接口。- 默认无整体超时,故障依赖可导致 goroutine/连接泄漏式阻塞。
- 恶意客户端缓慢发送 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)