跳转至

Web 服务:从 net/http 到 context 再到一整套服务

这一章把上一章的 socket 地基盖成楼房。 HTTP 服务器骨架小得惊人: 一个路由表 + 一组处理函数 + 一行 ListenAndServe;middleware 是「返回函数的函数」, 在调用前后夹行为;context 是请求的生命周期管家——超时、取消、随请求传递的小数据。 后半章全是成品:会优雅关停的代理、带 CLI 客户端的便签 REST 服务、 以及把第 8 章推给 Prometheus 的指标再读回来的工具。 读完你会拿到「写一个生产级 Go 服务」所需的全部标准件。

1. 这一章在原书里的位置

原书第 9 章,第二部分的压轴。第 8 章对着 socket 裸编,理解了 TCP/UDP; 本章进入 99% 的 Go 后端真实所在的层:HTTP + 路由 + 中间件 + context + 可观测性。 第一部分的语言知识(goroutine、接口、defer、错误处理)在这里全部汇拢。

2. 顶层全景

  服务器    ServeMux + handler 函数 + middleware 包装(§3)
  测量      net/http/pprof(浏览器直看)+ ab 灌流 + go tool pprof(§4)
  测试      httptest:不发真服务器的 HTTP 单元测试(§5)
  生命周期  context:WithTimeout/WithCancel/WithValue(§6)
  成品 ×3   代理(§7) / 便签 REST + Cobra CLI(§8–§9) / Prometheus 回读(§10)

图说:前四块是「任何一个服务都该有的器官」,后三块是器官拼成的三个活物。

3. HTTP 服务器:middleware 一个模式打天下

http.ListenAndServe + http.NewServeMux 是全部仪式。Go 1.22 起路由支持 方法前缀:mux.HandleFunc("GET /echo", …) 与 "POST /echo" 是两条独立路由, 不用在 handler 里手写 if r.Method != "GET"1。 net/http 和并发模型焊在一起:每个请求自动一条 goroutine,第 10 章 「每连接一 goroutine」在这里连那个 go 都替你写了2。

middleware:返回 handler 的函数

func loggingMiddleware(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        fmt.Printf("[%s] %s %s\n", r.Method, r.URL.Path, r.RemoteAddr)
        next.ServeHTTP(w, r)                    // ← 把控制权交给真正的 handler
        fmt.Printf("Completed in %v\n", time.Since(start))
    }
}
// 注册时包装:mux.HandleFunc("GET /time", loggingMiddleware(handleTime))

图说:middleware 不改 handler 一行代码,只在它前后夹行为—— 日志、认证、限流、panic 恢复全是这一个形状,可无限嵌套。 这是「横切关注点」四个字在 Go 里的全部实现成本。实测:curl 打六个端点, 服务器侧逐行 [GET] /time [::1]:51440 + Completed in 513.042µs3。

服务器演示的五个端点各教一件事:/(纯文本)、/time(JSON 响应的 writeJSON 套路——设 Content-Type + json.NewEncoder(w).Encode)、 /echo?msg=…(URL 查询参数)、POST /echo(请求体 JSON 解码,坏体给 400)、 /headers(回显请求头)、/health(负载均衡器探活)4。

HTTP 客户端

服务器写好先用 curl 测,客户端课用 Go 自写。三档API: http.Get/http.Post(一步式)、http.NewRequest + http.Client.Do (能加自定义头、控超时)、io.ReadAll(resp.Body) 收身。 铁律:defer resp.Body.Close()——响应体是连接池的通行证,不关会耗尽连接5。

4. 测量:pprof 告诉你服务器在等什么

加性能观测只需要两件事:匿名 import _ "net/http/pprof"(自动在 DefaultServeMux 挂上 /debug/pprof/ 全家桶),再开第二个端口(6060)专供剖析, 别和业务抢路6。七个常驻 profile:allocs、block、goroutine、heap、mutex、 threadcreate,CPU profile 要显式开启采样会话7。

实测剧本(值得抄):服务器跑着,go tool pprof -seconds=30 http://localhost:6060/debug/pprof/profile 开始采样;另一终端 ab -n 5000000 -c 10 "…/echo?msg=test" 灌流。结果两个看点:

  Duration: 30.15s, Total samples = 42.65s (141.45%)
  ← 采样时间超过墙钟时间 = goroutine 真的在多核上并行(第 5 号调度器的可视化)

  syscall.syscall        19.31s  45.28%
  runtime.kevent          7.80s  18.29%    ← 等内核事件(kqueue)
  runtime.pthread_cond_wait 4.64s 10.88%  ← 等条件变量

图说:HTTP 服务器的天性是等——时间大头在系统调用和线程睡眠, 「自己的计算」占比极小。这是给「性能优化先 profile」最生动的注脚: 优化业务逻辑前,先确认瓶颈真在业务逻辑8。

5. 测试:httptest 不发真服务器

net/http/httptest 让 handler 测试不需要端口:httptest.NewRequest 造假请求, httptest.NewRecorder 当假 ResponseWriter,直接调 handler(可以带着 middleware 一起调),断言状态码和响应体。书上把六个端点做成表格驱动测试——和 06 号拆解的 basename 测试同一形状,只是「函数」换成了「HTTP 往返」9。 测试输出里客户端地址变成 192.0.2.1:1234——文档保留段 IP,一眼认出是测试流量。

6. context:请求的生命周期管家

context 解决一个问题:一条请求触发的所有下游工作(数据库查询、外部 API、 子 goroutine),怎么一起「喊停」。三种用具:

函数 干什么 触发时机
WithCancel 返回 cancel 函数 你手动调 cancel()
WithTimeout(d) 2 秒后到点 倒计时结束
WithDeadline(t) 到指定时刻 时钟到点

使用合同:ctx.Done() 是个 channel,select 等它;拿到 cancel 必 defer cancel() (管家的打卡下班,防泄漏);ctx.Err() 告诉你死因(deadline exceeded / canceled)。 书里教练吹哨的比喻:时限一到,所有人停手,不管干到哪10。

HTTP 集成的最小演示:handler 里 ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second), 慢操作要 3 秒——ctx.Done() 先到,返回 408 Request Timeout。 真服务的对应动作:客户端断开或超时,数据库查询和后台任务随之取消11。

WithValue 是第三条腿(传请求级小数据:用户 ID、trace ID), 但书里给了重要的克制令:数据逻辑上属于应用(配置、连接池、大对象)就放结构体; 只有「跟着这次请求走」的元数据才进 context。背包 vs 随任务传阅的剪贴板12。

7. 成品一:代理服务器

代理 = 客户端和目标服务器之间的中间人:转发、可缓存、可过滤、可隐藏客户端 IP。 书上的实现因太长没贴代码,但架构描述完整,值得当设计范本13:

  • Proxy 结构封装全部状态:listener、logger、quit channel、WaitGroup;
  • NewProxy 里建一个响应 OS 信号的 context(SIGINT/SIGTERM → cancel)—— 第 8 章 signal.Notify + 本章 context 的合龙;
  • Start():Accept 循环把每条连接交给 goroutine,同时盯 context 的取消;
  • handleClient():解析 HTTP 请求、净化头、普通 HTTP 直接转发; HTTPS 走 CONNECT 隧道(CONNECT 是 HTTP 代理转加密流量的标准方法)—— 双向数据拷贝各一条 goroutine,共享一个 WithCancel:一边关,两边散;
  • Shutdown():关 listener、WaitGroup.Wait() 等所有活跃会话收场,日志确认。

实测:curl -x http://127.0.0.1:8080 https://www.mtsoukalos.eu/, 日志完整演出 accept → CONNECT 隧道 → 隧道关闭;Ctrl+C 后三行关停日志 Shutting down proxy server... / shut down gracefully. / Listener closed14。 「优雅关停 = context 取消 + WaitGroup 等待」这对组合是生产服务器的标准答案, 第 8 章的 dd(1) 用了信号,这里补全了「等完再走」的另一半。

8. 成品二:便签 REST 服务

教学版 REST:便签有 id/title/content,五个端点(POST /notes 建、 GET /notes 列(可 ?title= 过滤)、GET/PUT/DELETE /notes/{id}), 数据落盘 notes.json(loadNotes/saveNotes 同步内存 map 与文件)15。 curl 全旅程:

  POST /notes {"title":"First Note",…}  → {"id":2,…}      ← 201
  GET  /notes                            → [ {id:1,…}, {id:2,…} ]
  DELETE /notes/1                        → (空,成功)
  GET  /notes/1                          → "Note not found"     ← 404

设计要点书里明说:先设计再写码——路由约定、数据格式、错误形态, 以及把业务逻辑、网络、持久化分层(这个服务还小,三层的轮廓已经在)16。

9. 成品三:Cobra/Viper CLI 客户端

第 2 号拆解只给了 cobra/viper 概念,这里第一次真用。 cobra init + cobra add list/get/add/update/delete 脚手架出 cmd/ 目录; root.go 定义持久 flag --url(子命令全继承),add.go 的 RunE 里就是 上一节的 HTTP 客户端代码。关键迁移:先把 API 交互写成纯 Go 函数 (testServer.go),再原封不动塞进 Cobra 命令——HTTP 逻辑与 CLI 壳分离17:

  $ go run main.go add -t "Book Idea" -c "Write about Go middleware"
  Note created successfully!
  $ go run main.go get 1
  Error: server returned: 404 Not Found

必选项用 MarkFlagRequired 声明,缺了 Cobra 自己拦18。 (服务与客户端共用 Note 结构体与 baseURL——这是单一事实来源的小示范。)

10. 成品四:从 Prometheus 把指标读回来

第 8 章是「推出去」,本节是「读回来」:Prometheus 自己也有 HTTP API, /api/v1/query(瞬时值)与 /api/v1/query_range(区间序列), 查询语言 PromQL 就长在 URL 参数里19。

readProm.go 的教学点有两个。① 响应结构建模:Instant 与 Range 分开两个 结构体,值的类型用 []interface{}(JSON 里时间和值类型不一)—— 宁可解码时做类型断言,也不让结构体撒谎。② label 过滤的用户界面: --labels cpu=0,mode=idle 程序负责拼成 {cpu="0",mode="idle"} 接在指标名后 ——CLI 参数到查询语言的翻译层,一层就够,不让用户写 PromQL20。

配套还有一个造数程序(sampleData.go):正弦曲线模拟 CPU 昼夜起伏、 HTTP 请求随机突发,回填 7 天历史后 POST 给 Pushgateway (第 10 号说过 Pushgateway 缺席,这里补上了:不可被拉的短任务批次走推送)。 --list 列全部指标、--metric 查瞬时值、--start/--end 定区间—— 一个最小可用的 PromQL 客户端21。

11. 作者的判断与证据

判断 性质 依据
middleware 是 HTTP 横切关注点的完整答案 设计事实 + 作者立场 「just a function returning another http.HandlerFunc」3
剖析端口与业务端口分开 工程惯例(作者采用) 6060 独立服务的说明6
HTTP 服务器的时间大头在「等」 pprof 实证 141.45% 采样 + syscall/kevent 占比8
cancel 必 defer、应用数据不进 context 语言社区铁律 backpack/剪贴板对照12
优雅关停 = context 取消 + WaitGroup 等待 架构模式(作者实现) 代理的 Shutdown 设计13
先写纯函数 API 客户端再套 CLI 作者的方法论主张 「directly transferred into a Cobra-based CLI」17

12. 边界与局限

  • 路由只说标准库。路径参数(/{id} 的提取)、路由组、通配在 ServeMux 里很简陋, 书里一句 Note 把 Chi 指出去就没了——真实项目多半第三周就换路由器。
  • 并发安全没点破:便签服务的 notes map 被多 goroutine(每请求一条)共享, 没有 mutex——教学程序在并发写时会 data race,书里未提(第 3 章的 -race 能抓到)。
  • context value 的 key 冲突风险(不同库用同名字符串 key)没讲, 官方推荐私有类型 key,示例用的是裸字符串——照抄会埋雷。
  • 代理的 TLS 拦截(中间人)与 CONNECT 之外的语义没展开;HTTP/2 在代理下的 退化也没提。
  • Pushgateway 的正确用法警示(推了不清理会留僵尸指标)没讲——只演示了推。

13. 可带走的

  1. 服务器三件套:ServeMux(方法前缀路由)+ handler + ListenAndServe; 每请求自动 goroutine。
  2. middleware = 包装 handler 的函数;日志、认证、限流、恢复全是一个形状。
  3. defer resp.Body.Close();客户端要自定义头/超时用 NewRequest + Client.Do。
  4. pprof:匿名 import + 独立端口;go tool pprof -seconds=N URL + ab 灌流; 采样 >100% 是多核并发的证据,HTTP 服务器的瓶颈常在「等」。
  5. httptest:不发真服务器的 handler 测试,表格驱动照旧适用。
  6. context 三用具 + 两个合同:defer cancel()、数据分「请求级进 context、 应用级进结构体」。
  7. 优雅关停公式:signal → context cancel → 停接新客 → WaitGroup 等完在途。
  8. 写 CLI 客户端:先纯函数 API 层,再套 Cobra;必填 flag 用 MarkFlagRequired。
  9. Prometheus API 两个端点(query/query_range)+ label 过滤; CLI 替用户拼 PromQL,一次翻译一层。

14. 原文地图

主题 原书章 原文位置
net/http 与并发模型 The net/http package text/94-ch09.txt:21(搜「powerful and high-level」)
middleware 模式 Creating a simple HTTP server with middleware text/95-fm-creating-a-simple-http-server-with-middleware.txt:70(搜「loggingMiddleware」) · :171(搜「GET /」)
五端点与 curl 实测 Creating a simple HTTP server with middleware text/95-fm-creating-a-simple-http-server-with-middleware.txt:201(搜「Welcome to the Go Web Service」) · :233(搜「Completed in 32.542µs」)
HTTP 客户端三档 Creating a simple HTTP server with middleware text/95-fm-creating-a-simple-http-server-with-middleware.txt:274(搜「getRequest」) · :299(搜「getWithHeaders」)
pprof 接入与七 profile Creating a simple HTTP server with middleware text/95-fm-creating-a-simple-http-server-with-middleware.txt:408(搜「pprof.Profiles()」) · :447(搜「localhost:6060」)
ab 灌流与 141.45% Creating a simple HTTP server with middleware text/95-fm-creating-a-simple-http-server-with-middleware.txt:472(搜「-seconds=30」) · :480(搜「141.45%」) · :502(搜「syscall.syscall」)
httptest 测试 Creating a simple HTTP server with middleware text/95-fm-creating-a-simple-http-server-with-middleware.txt:549(搜「TestEndpoints」) · :599(搜「192.0.2.1:1234」)
context 三用具与教练比喻 The context package text/96-fm-the-context-package.txt:20(搜「carry deadlines」) · :36(搜「doWork」) · :50(搜「WithTimeout」)
WithValue 的克制令 The context package text/96-fm-the-context-package.txt:110(搜「Why not use a structure」) · :117(搜「backpack」)
HTTP 408 演示 The context package text/96-fm-the-context-package.txt:124(搜「slowOperation」) · :135(搜「r.Context()」)
代理架构与优雅关停 Creating a proxy server text/97-fm-creating-a-proxy-server.txt:22(搜「Proxy structure」) · :29(搜「Shutdown」) · text/97-fm-creating-a-proxy-server.txt:40(搜「shared context.WithCancel」)
代理实测(CONNECT 隧道) Creating a proxy server text/98-fm-developing-a-web-service.txt:5(搜「curl -x」) · :13(搜「CONNECT //www.mtsoukalos.eu:443」)
便签服务五端点 Developing a web service text/98-fm-developing-a-web-service.txt:60(搜「POST /notes」) · :74(搜「GET /notes」) · :108(搜「curl -X POST」)
REST 设计主张 Developing a web service text/98-fm-developing-a-web-service.txt:42(搜「Carefully designing」)
纯函数客户端→Cobra Developing a web service text/98-fm-developing-a-web-service.txt:233(搜「directly transferred」) · text/99-fm-developing-a-client-for-the-web-service.txt:119(搜「var addCmd」)
Prometheus API 与 label 拼接 Reading data from Prometheus text/100-fm-reading-data-from-prometheus.txt:128(搜「listMetrics」) · :87(搜「InstantResponse」) · :222(搜「cpu=\"0\"」)
造数与 Pushgateway 回填 Reading data from Prometheus text/100-fm-reading-data-from-prometheus.txt:44(搜「backfill」) · :65(搜「backfill-days 7」)


  1. 出处:「Creating a simple HTTP server with middleware」(text/95-fm-creating-a-simple-http-server-with-middleware.txt:171,搜「GET /」;:188,搜「method-aware routing」)。GET/POST /echo 分立两条路由见第 190–191 行。 ↩

  2. 出处:「The net/http package」(text/94-ch09.txt:24,搜「integration with Go's concurrency model」)。 ↩

  3. 出处:「Creating a simple HTTP server with middleware」(text/95-fm-creating-a-simple-http-server-with-middleware.txt:70,搜「loggingMiddleware」;:40,搜「function returning another http.HandlerFunc」;:233,搜「Completed in 32.542µs」)。 ↩↩

  4. 出处:「Creating a simple HTTP server with middleware」(text/95-fm-creating-a-simple-http-server-with-middleware.txt:90,搜「handleRoot」;:127,搜「Invalid JSON body」;:200,搜「Welcome to the Go Web Service」)。writeJSON 的 Content-Type + Encode 见第 56–63 行。 ↩

  5. 出处:「Creating an HTTP client」(text/95-fm-creating-a-simple-http-server-with-middleware.txt:274,搜「getRequest」;:280,搜「defer resp.Body.Close」;:299,搜「getWithHeaders」;:375,搜「Go-http-client/1.1」)。 ↩

  6. 出处:「Profiling HTTP applications」(text/95-fm-creating-a-simple-http-server-with-middleware.txt:401,搜「anonymously」;:436,搜「port 6060」)。 ↩↩

  7. 出处:「Profiling HTTP applications」(text/95-fm-creating-a-simple-http-server-with-middleware.txt:416,搜「allocs」;:432,搜「only exists when you explicitly trigger」)。七 profile 逐一解释见第 423–431 行。 ↩

  8. 出处:「Profiling HTTP applications」(text/95-fm-creating-a-simple-http-server-with-middleware.txt:480,搜「141.45%」;:502,搜「syscall.syscall」;:518,搜「45.28%」;:526,搜「spends much of its time waiting for requests」)。 ↩↩

  9. 出处:「Testing HTTP applications」(text/95-fm-creating-a-simple-http-server-with-middleware.txt:549,搜「TestEndpoints」;:587,搜「ResponseRecorder」;:599,搜「192.0.2.1:1234」)。 ↩

  10. 出处:「The context package」(text/96-fm-the-context-package.txt:20,搜「carry deadlines」;:25,搜「WithCancel」;:36,搜「doWork」;:50,搜「WithTimeout」;:51,搜「Always call cancel」)。教练吹哨比喻见第 9–19 行。 ↩

  11. 出处:「The context package」(text/96-fm-the-context-package.txt:124,搜「slowOperation」;:135,搜「r.Context()」;:160,搜「408」)。 ↩

  12. 出处:「The context package」(text/96-fm-the-context-package.txt:110,搜「Why not use a structure」;:117,搜「backpack」)。「configuration data, database connections, or large objects should live in well-defined structs」在同段。 ↩↩

  13. 出处:「Creating a proxy server」(text/97-fm-creating-a-proxy-server.txt:22,搜「Proxy structure」;:26,搜「cancellable context」;:29,搜「Shutdown」;:31,搜「context cancellation and WaitGroup」)。 ↩↩

  14. 出处:「Creating a proxy server」(text/98-fm-developing-a-web-service.txt:5,搜「curl -x」;:13,搜「CONNECT //www.mtsoukalos.eu:443」;:18,搜「Shutting down proxy server」)。 ↩

  15. 出处:「Developing a web service」(text/98-fm-developing-a-web-service.txt:60,搜「POST /notes」;:98,搜「loadNotes() and saveNotes()」)。curl 四连实测见第 108–117 行。 ↩

  16. 出处:「Developing a web service」(text/98-fm-developing-a-web-service.txt:42,搜「Carefully designing」;:51,搜「separate concerns」)。 ↩

  17. 出处:「Developing a web service」(text/98-fm-developing-a-web-service.txt:233,搜「directly transferred」)与「Developing a client for the web service」(text/99-fm-developing-a-client-for-the-web-service.txt:13,搜「cobra init」;:119,搜「var addCmd」;:144,搜「MarkFlagRequired」)。 ↩↩

  18. 出处:「Developing a client for the web service」(text/99-fm-developing-a-client-for-the-web-service.txt:163,搜「Book Idea」;:6,搜「404 Not Found」)。 ↩

  19. 出处:「Reading data from Prometheus」(text/100-fm-reading-data-from-prometheus.txt:21,搜「communicates primarily over HTTP」;:24,搜「PromQL」)。 ↩

  20. 出处:「Reading data from Prometheus」(text/100-fm-reading-data-from-prometheus.txt:87,搜「InstantResponse」;:114,搜「two elements」;:222,搜「cpu=\"0\"」;:236,搜「without requiring the web UI」)。 ↩

  21. 出处:「Reading data from Prometheus」(text/100-fm-reading-data-from-prometheus.txt:44,搜「backfill」;:52,搜「sinusoidal」;:65,搜「backfill-days 7」;:169,搜「--list」;:195,搜「--metric」)。 ↩