Server-Timing头必须通过w.Header().Set()在w.WriteHeader()或w.Write()前设置,r.Header只读无效;应使用servertiming.Middleware自动注入Timing实例,显式调用Start()/Stop()记录耗时,避免defer失效;指标需聚合后一次性写入,勿多次Set或手动拼接。

Server-Timing头必须通过w.Header()写入,不能碰r.Header
很多人试过在handler里调用r.Header.Set("Server-Timing", "..."),结果浏览器开发者工具里根本看不到这个头——因为r.Header是请求头副本,只读,改了也白改。真正生效的只有w.Header().Set("Server-Timing", "..."),而且必须在w.WriteHeader()或w.Write()之前调用。
常见错误现象:
- curl -v能看到
Server-Timing,但浏览器Network面板不显示 → 实际是curl把请求头误当响应头打印了 - 用
http.Error()返回错误后补写Server-Timing→ 已发送状态码,头写入被忽略
正确姿势是:所有Server-Timing指标必须在响应体写出前、状态码确定后(但未发出)的窗口期注入。
用servertiming.Middleware自动初始化上下文,别手动生成timing对象
手动调用servertiming.NewTiming()再塞进context容易漏掉并发安全处理,也绕过了库内置的header序列化逻辑。直接用官方中间件最稳:
立即学习“go语言免费学习笔记(深入)”;
h := servertiming.Middleware(http.HandlerFunc(yourHandler), nil)
http.ListenAndServe(":8080", h)
它会在每个请求的context.Context里注入一个*servertiming.Timing实例,后续只需从r.Context()取,无需自己管理生命周期。
关键点:
- 中间件必须包装最终handler,不能只包一层路由mux → 否则子handler拿不到timing对象
- 第二个参数传
nil即可,自定义配置极少用到,强行传非nil反而易出错 - 如果用了gorilla/mux等第三方router,确保中间件在
router.ServeHTTP()之前执行
记录耗时要用Start()/Stop()配对,避免defer在panic时失效
写defer m.Stop()看着简洁,但一旦handler中途panic,Stop()不执行,指标就丢了。更可靠的方式是显式调用:
timing := servertiming.FromContext(r.Context())
m := timing.NewMetric("db").WithDesc("MySQL query").Start()
rows, err := db.Query(...)
m.Stop() // 立即停,不依赖defer
这样即使后面发生错误或提前return,耗时也已记录。如果真要defer,得包一层:
m := timing.NewMetric("cache").Start()
defer func() { m.Stop() }()
但不如直写清晰。另外注意:
- 同一个metric name重复调用
NewMetric()会覆盖前值,不是累加 - 描述文字
WithDesc()建议用英文+空格分隔,中文可能在某些浏览器解析异常 - 指标名别含空格或特殊字符,
sql-query可以,sql query不行
浏览器只解析首个Server-Timing头,多写会被截断
HTTP规范允许一个header字段多次出现,但Chrome/Firefox实际只取第一个Server-Timing值。如果你手动拼接多个指标再w.Header().Set(),后写的会覆盖前写的。
正确做法永远只调用一次w.Header().Set(),让servertiming库内部聚合所有metric后一次性写入。库底层用fmt.Sprintf拼成类似db;dur=12.3, cache;dur=4.7;desc="Redis hit"的字符串。
容易踩的坑:
- 在多个中间件里各自调用
servertiming.FromContext(...).NewMetric(...)→ 没问题,库会自动合并 - 自己用
strings.Builder拼Server-Timing值再Set()→ 覆盖风险高,且丢失WithDesc()等元信息 - 用
Add()而非Set()→Header.Add()会产生多个同名头,浏览器无视后续项
最后提醒一句:Server-Timing只是观测手段,不能替代pprof或分布式追踪。指标精度受Go调度器影响,亚毫秒级差异不必深究。重点是识别哪段逻辑拖慢了整条链路,而不是纠结单次调用的0.3ms浮动。



















