Go 22:HTTP 提交、查询与非法输入怎样表达
上篇给同一执行器的提交与关闭设了协议。HTTP 接入时,最容易写错的不是路由语法,而是把 POST 的响应当成后台计算完成的凭据。任务一旦被执行器接收,就归本次服务所有;handler 返回、请求 context 被取消,不能直接解释成该任务已经退出。本章在同一个累计工程里添加 HTTP 适配层,以真实 loopback 请求测试提交、查询和错误输入;完整工程及实验包、SHA-256与解包复跑说明可独立核对。
三条路由各自交付什么
HTTP 里方法与路径要一起看。当前路由是 POST /tasks、GET /tasks/{id}、GET /health,由固定 Go 1.27.1 的 http.ServeMux 方法模式注册。POST 解一个 JSON 对象,字段 text 非空;接受时返回 202 Accepted,响应体只给 {"id":"task[0]"},Location: /tasks/task[0] 指出后续查询地址。202 说明本次非阻塞提交已经进入服务拥有的执行器,不说明统计完成,更不说明客户端一定收到了完整响应。GET /tasks/{id} 返回当前 TaskStore.Lookup 的值快照,可能是 QUEUED、RUNNING、SUCCEEDED、FAILED 或 CANCELED;缺失返回 404。GET /health 回 200 只证明该 HTTP 适配层当下还能应答,不提供队列可接收或后端恢复的 readiness 承诺。
TaskStore 原先按整个 CLI 批次预建位置;HTTP 不知道接下来会提交多少项。适配层按服务内单调增长的位置生成 ID,在自己的锁下建立 QUEUED 记录、尝试向同一个 taskExecutor 非阻塞提交。如果队列满或执行器已关,立刻删除这条尚未接受的预置记录并回 503;被拒绝的 ID 可用 GET 验证确实不存在。TaskStore 仍用自己的锁保护 map 和多字段状态;适配层锁串起 ID 分配、预置和非阻塞 Submit,不能以“map 是线程安全的”代替整个接受判定。为了支持动态位置,原执行器的结果收集者按已收到的编号扩展切片;worker 上限和 channel 容量没有改变,但结果和内存记录仍随任务总数增长。这里的有界是执行/排队上界,不是进程永久内存预算。
1 | |
真实本地连接的 TestHTTPAckBeforeTaskCompletion 用闸门阻住统计:POST 已返回 202,响应体已经读完并关闭,随后按 ID 查询仍为 RUNNING、词数 0;解除闸门并优雅结束执行器后,再查到 SUCCEEDED words=3。Go 1.27.1 的 Request.Context 文档说明,入站请求 context 会在 handler 返回时取消。若把它直接传给要继续运行的任务,即使没人主动按“取消任务”,handler 返回也可能终止它。这里 Submit 用的是执行器所属的服务 context,而不是 request.Context();闸门实验展示应答与业务完成确实分开。客户端断开时具体怎样处理,以及主动取消 API,还留给 23;本次观察不能反推所有网络错误下客户端总能获知任务是否已接受。
输入错误先挡在工作队列外
POST /tasks 只接受 application/json 或其 charset=utf-8 形式。解析请求体用 http.MaxBytesReader 限制 JSON 报文总字节数 为 65536,不是只检查解出来的 text 字符数;超限回 413。json.Decoder.DisallowUnknownFields() 拒绝陌生字段,再尝试解第二次,要求只有一个完整 JSON 对象;坏 JSON、空串、纯空白、额外字段或第二个对象均回 400。其他媒体类型或与 UTF-8 不一致的 charset 回 415。错误响应体是有限的 {"error":"..."},不会把 JSON 解码器或存储内部细节直接变成客户端稳定契约。
同一个真实 loopback 服务的反向测试覆盖:GET /health 返回 200,向该路由用 POST 得到 405;非法 JSON/空白/未知字段/尾随对象返回 400;超过 65536 字节的 JSON 请求得到 413;错误媒体类型得到 415。随后让唯一 worker 停在闸门,容量 1 的队列接纳第二项,第三次正确 JSON 的提交返回 503,查询该拒绝位置为 404。放行并调用服务自己的优雅 Close 后,前两项已接受任务均可查到 SUCCEEDED。这样错误输入、过载拒绝与业务执行失败不是同一种“出错”。这些是实际的 httptest.NewServer loopback TCP 请求,而非只调用 handler 函数的伪 HTTP 测试。
一条看似合法的文本也可能在执行业务时失败。TestHTTPAcceptedTaskCanFailBusinessValidation 用原累计工程的分析器,上限 maxWords=3:提交 one two three four 得到 202 与 ID,等待 worker 结束后按 ID 查询得到 FAILED,结果词数不伪填为 4。HTTP 层的 400 管请求格式与允许字段;202 管“已接受”;FAILED 管后台计算结果。若以后想在提交前同步验证词数,那是另一份明确的接口承诺,不能静默用“收到 HTTP 202”替代实际业务成功。
静态应答、业务进展与资源所有者
HTTP 写入 ResponseWriter 本身不保证对端已完整接收,成功收到 202 也不能提供跨进程持久化。TaskStore 仍是内存 map;进程崩溃后此章没有可重启的任务记录。service.Close() 是本地测试使用的优雅业务执行器收尾,等已接受项的 worker 处理完毕;它并不调用 http.Server.Shutdown,更不能用测试服务器的 Close 推断线上连接已被优雅释放。客户端期限、断开后的身份确认、实际 HTTP 服务器生命周期及连接资源分别由 23–24 验收。特别是 /health 的 200 连“还能提交一个新任务”都不保证,更不用说全部后台工作已结束。
在 Go 1.27.1/linux-amd64、GOTOOLCHAIN=local、GOMAXPROCS=2、CGO_ENABLED=1、gcc 13.3.0 下,三项 HTTP 测试单次与重复 20 次均退出 0;两包完整 go test、本次 HTTP 路径及两包 go test -race 退出 0。无 race 报告只涉及执行过的路径。旧 CLI 的批次输出与错误参数路径也经编译后的可执行文件复跑,命令、环境、原始双流、退出码位于 writing-plans/go-language/evidence/22/。Hexo 生成和静态页面检查是另一组证据,不会替代真实 loopback 请求的协议测试。
练习:若 POST /tasks 返回 202,紧接着 GET /tasks/task[0] 返回 RUNNING,客户端应该重发原 POST 让“任务真正完成”吗?答案:不能据此重发。RUNNING 只说明该 ID 的工作尚未到终态;重发 POST 会产生另一个 ID,可能造成重复执行。应按响应给出的 Location 继续查询同一任务;如果 POST 的网络结果未知,还需要后续设计客户端提供的幂等键或其他去重协议,此章没有实现,不能靠看见 202 或主动重试宣称“恰好一次”。同理,数据库未来的一次提交与外部副作用是否发生,也要分开记账。
本章没有增加 CLI 监听端口或发布进程;真正常驻的 HTTP 服务器启动/退出、客户端取消与任务取消、连接池、真实 SQL 与重启恢复仍 NOT_RUN。Go 工具链 tag go1.27.1、镜像查询完整源码 SHA 862c888e612ac346c7c4d99c9392bdfd265f33b0 沿用基线,官方 Gitiles 直接交叉核验仍 NOT_RUN;本文依赖固定版公开 API 和原始实验,不拿网页上未锁定版本的描述填补服务协议。
参考资料
http.ServeMux、Request.PathValue、Request.Context:固定 Go 1.27.1 的go doc命令与输出见writing-plans/go-language/evidence/22/doc-*。http.MaxBytesReader、json.Decoder.DisallowUnknownFields、httptest.NewServer:版本、负例和观察边界见writing-plans/go-language/research/22.md。






