最直接可靠的方式是函数/HTTP handler入口用time.Now()记录起始时间,defer中传参调用time.Since()计算耗时;必须在goroutine或handler内部声明start变量避免共享,禁用闭包捕获外部变量,高并发下用fmt.Sprintf拼接日志替代log.Printf,并通过Prometheus HistogramVec上报以支持P95/P99等分位数统计。

直接用 time.Now() + defer 就够了,别封装过度
Go 里统计单个函数或 HTTP handler 耗时,最轻量、最可靠的方式就是两行代码:开头记时间,结尾用 defer 算差值。所有“自动埋点”“AOP 框架”“泛型封装”在绝大多数业务场景下都是冗余的,反而增加理解成本和 GC 压力。
常见错误是写成这样:
start := time.Now()<br>defer fmt.Printf("cost: %v", time.Since(start))看着没问题,但一旦 handler 里有 panic 或提前 return,
start 可能被闭包捕获为零值(尤其 Go 1.22 之前),输出 cost: 0s。
- 正确写法:把
start显式传进defer匿名函数,确保值被捕获 - 耗时单位统一用
.Milliseconds(),避免.Seconds()返回小数导致日志难对齐 - 别在
defer里调用log.Printf—— 它带锁,高并发下会成为瓶颈;改用fmt.Sprintf拼好再一次性写
HTTP 中间件里必须隔离 start 变量作用域
如果你用 Gin/Echo/GoFrame 等框架,中间件函数会被复用,start 必须声明在 handler 内部,不能提成包级变量或闭包外变量,否则多个请求会互相覆盖。
典型错误示例:
var start time.Time<br>func logger() gin.HandlerFunc {<br> return func(c *gin.Context) {<br> start = time.Now()<br> c.Next()<br> log.Printf("cost: %dms", time.Since(start).Milliseconds())<br> }<br>}这会导致并发请求共用同一个
start,耗时完全不准。
立即学习“go语言免费学习笔记(深入)”;
- 每个请求 handler 内独立声明
start := time.Now() - 如果要透传到下游逻辑(比如 DB 查询层),用
c.Request.Context()存,别靠全局变量 - 别为了“统一格式”把耗时计算抽成
func() time.Duration—— 函数调用本身就有开销,且掩盖了真实执行位置
需要聚合统计?用 prometheus.HistogramVec 替代自定义滑动窗口
想看 P95/P99 延迟、按 path/method 分桶?别自己用 container/list 维护时间戳队列,也别碰 golang.org/x/time/rate —— 它不是统计工具,是限流器。
真正生产就绪的做法是直接对接 Prometheus 生态:
httpDuration := prometheus.NewHistogramVec(<br> prometheus.HistogramOpts{<br> Name: "http_request_duration_seconds",<br> Help: "HTTP request duration in seconds",<br> Buckets: prometheus.ExponentialBuckets(0.001, 2, 12), // 1ms~2s<br> },<br> []string{"method", "path", "status"},<br>)
- 注册后,在中间件里一行上报:
httpDuration.WithLabelValues(c.Request.Method, c.Request.URL.Path, strconv.Itoa(statusCode)).Observe(time.Since(start).Seconds()) - 所有分位数、直方图聚合由 Prometheus server 自动算,你不用管窗口怎么滑、数据怎么过期
- 避免用
statsviz查单实例延迟 —— 它只展示 runtime 指标(GC、heap),不记录业务耗时
集成测试里耗时断言要加 t.Helper() 和超时控制
写集成测试验证接口响应是否在预期范围内,别只写 if cost > 100*time.Millisecond 然后 t.Fatal —— 这会让失败日志看不出是哪个测试 case 崩的。
- 在断言函数开头加
t.Helper(),让报错定位到调用处而非断言内部 - 给 HTTP 请求加 context timeout,防止网络卡住导致测试挂死:
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) - 别用
time.Sleep模拟“慢接口”做测试 —— 它不保证精度,且无法反映真实阻塞点;改用httptest.NewServer返回一个故意 delay 的 handler
真正容易被忽略的是:耗时统计本身不该成为性能瓶颈。任何在 hot path 上做 map 写入、JSON 序列化、或同步 I/O 的操作,都可能让监控反成拖累。优先走 defer + fmt.Sprintf + prometheus.HistogramVec.Observe 这条链路,它经过大量生产验证,开销可控,且指标可直接喂给告警系统。


















