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 的测试不能代替真实网络与浏览器验证?

参考资料