Go 标准库已经具备 HTTP 服务、路由、JSON 与测试能力。先把边界做对,再决定是否引入 Gin 等框架,可以更清楚地辨认框架解决了什么。本实验使用 Go 1.22 引入的方法路由模式,因此需要 Go 1.22 或更高版本;只依赖标准库。
下载和运行
下载 http-lab.zip,解压到新目录后运行:
go test ./...
go run .
服务只监听 127.0.0.1:8081。请求示例:
curl -i -X POST http://127.0.0.1:8081/echo -H "Content-Type: application/json" --data '{"name":"Cinna"}'
Windows PowerShell 若将 curl 映射为别的命令,请使用 curl.exe。合法请求返回 201 与 {"name":"Cinna"};这只是用于观察协议边界的回显接口,不会创建持久资源。
一次 Decode 不够
JSON 解码器第一次成功,只证明读到了第一个合法值。{"name":"A"}{"name":"B"} 可以先成功解码 A,因此要继续解码并要求 EOF。未知字段、体积、名称长度也要各自校验。
r.Body = http.MaxBytesReader(w, r.Body, 1024)
dec := json.NewDecoder(r.Body)
dec.DisallowUnknownFields()
var input struct { Name string `json:"name"` }
if err := dec.Decode(&input); err != nil {
// 区分 MaxBytesError 与其他格式错误后返回。
return
}
var extra any
if err := dec.Decode(&extra); err != io.EOF {
// 第二个值或非法尾随内容都拒绝。
return
}
这里只摘出解码流程,压缩包中的 main.go 包含完整错误响应。Content-Type 使用 mime.ParseMediaType 解析,可以接受 application/json; charset=utf-8,而不是用脆弱的字符串全等比较。名称限制按 Unicode rune 数量计算,避免把每个中文字符当作多个字节。
方法路由与响应顺序
mux.HandleFunc("POST /echo", handler) 限定方法。其他方法由 ServeMux 返回 405 与 Allow 信息,不必在每个函数里再散落一份判断。写 JSON 前先设置 Content-Type,再写状态码,最后写 body;第一次写 body 后再改状态码已经太晚。
真实创建接口应返回能定位新资源的 Location。回显实验没有存储,所以不伪造资源 URL。项目中的异步任务接口则会返回状态查询地址,见 Go 任务服务。
超时需要区分不同阶段
Server 配置 ReadHeaderTimeout、ReadTimeout、WriteTimeout 和 IdleTimeout,限制慢连接占用。HTTP 客户端、数据库和业务处理仍需自己的 deadline,服务端 WriteTimeout 不会自动取消所有业务工作。长连接与流式接口也不能直接套用短 JSON 请求的全部时间参数。
停止服务时使用 Shutdown 拒绝新连接并等待在途请求,在总等待到期后有明确退出策略。生产环境还要检查反向代理超时、请求体限制和真正的信号传播;本实验没有部署代理或测试公网流量。
测试观察什么
测试通过 httptest 在进程内执行 handler,覆盖正常请求、未知字段、尾随 JSON、超大请求、不支持的媒体类型和错误方法。它验证应用路由与序列化,不证明 TLS、真实连接超时和代理行为。
练习:增加 GET /echo/{name},先明确 URL 解码和路径中的斜线处理,再写测试。接着给 POST 增加下游调用,使用 r.Context 传播取消,并验证客户端断开后的工作是否结束。
面试自查
- 为什么限制 Content-Length 还不够?分块传输时应在哪一层限制实际读取量?
- Handler 超时、写超时和下游 context 取消分别保护什么?
- 为什么 httptest 的测试不能代替真实网络与浏览器验证?