在Golang的Web开发中,对HTTP接口做基础的请求统计与监控并不复杂。我们可以利用标准库net/http的中间件模式,在请求进入业务逻辑前后插入计数和计时逻辑,从而低成本地掌握服务运行状况。

为什么需要轻量级的请求统计
很多团队在业务初期并不会接入完整的APM系统,因为部署成本高、学习曲线陡。但当接口出现偶发超时或量突增时,如果没有最基础的请求计数和耗时分布,排查只能靠日志大海捞针。轻量级统计模块直接嵌入应用进程,不依赖外部存储,能在不改动业务代码的前提下输出关键指标。
这类统计通常关注三个维度:总请求量、按状态码分类的失败量、以及响应耗时分位。在Golang中,由于goroutine并发特性,统计变量必须考虑竞态。使用sync.Map或原子操作能避免锁竞争过重,也降低了出错概率。
基于中间件的统计实现
中间件本质上是一个接收http.Handler并返回新http.Handler的函数。我们在内部包装原有ServeHTTP,在调用前后记录时间并累加计数。下面示例使用sync.Map按路径存放该路径的统计数据。
package main
import (
"net/http"
"sync"
"time"
)
type stat struct {
total int64
success int64
fail int64
sumMs int64
}
var stats sync.Map
func Monitor(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
sw := &statusWriter{ResponseWriter: w, status: 200}
next.ServeHTTP(sw, r)
cost := time.Since(start).Milliseconds()
key := r.Method + " " + r.URL.Path
val, _ := stats.LoadOrStore(key, &stat{})
s := val.(*stat)
s.total++
s.sumMs += cost
if sw.status >= 200 && sw.status < 400 {
s.success++
} else {
s.fail++
}
})
}
type statusWriter struct {
http.ResponseWriter
status int
}
func (sw *statusWriter) WriteHeader(code int) {
sw.status = code
sw.ResponseWriter.WriteHeader(code)
}
上面的代码定义了一个statusWriter来捕获响应状态码,因为原生ResponseWriter不会暴露状态码。中间件在next.ServeHTTP执行后,根据状态码归类成功或失败,并累加耗时。由于stats是sync.Map,不同路径之间互不阻塞,适合多路由场景。
这种写法的优点是零外部依赖、易于理解。缺点是进程重启后数据清零,且所有数据在内存中,若路径过多可能占用较大空间。对于监控需求不复杂的内部服务,这通常是可接受的权衡。
暴露监控指标的接口
统计值存放在内存中后,还需要一个接口让运维或脚本定时拉取。我们可以注册一个独立的HTTP handler,将sync.Map内容序列化为JSON返回。下面示例简单遍历输出。
func MetricsHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte("{"))
first := true
stats.Range(func(k, v interface{}) bool {
if !first {
w.Write([]byte(","))
}
first = false
s := v.(*stat)
avg := int64(0)
if s.total > 0 {
avg = s.sumMs / s.total
}
w.Write([]byte(""" + k.(string) + "":{"))
w.Write([]byte(""total":" + itoa(s.total) + ","))
w.Write([]byte(""success":" + itoa(s.success) + ","))
w.Write([]byte(""fail":" + itoa(s.fail) + ","))
w.Write([]byte(""avgMs":" + itoa(avg) + "}"))
return true
})
w.Write([]byte("}"))
}
func itoa(i int64) string {
return strconv.FormatInt(i, 10)
}
该接口直接拼装JSON字符串,避免引入encoding/json的依赖,也方便在浏览器中查看。实际项目中可加上基础鉴权,防止指标接口被公开访问。配合定时脚本或Prometheus的textfile收集器,就能实现简单看板。
如果路径极多,Range遍历可能短暂影响性能,可在单独goroutine中定期快照到局部变量,再供接口读取,从而减少锁占用时间。这也体现了Go并发编程中读写分离的常见思路。
避免常见误区
一个容易混淆的概念是,使用defer在函数退出时记录统计是否安全。如果直接在handler内defer,只能捕获到handler返回的时刻,但无法拿到最终状态码,因为WriteHeader可能在业务任意位置调用。因此必须包装ResponseWriter,如前面statusWriter所示。
注意:不要使用普通map加互斥锁来存所有路径统计,高并发下锁竞争会让请求延迟明显上升。sync.Map在读写多写少场景表现更好。
另一个误区是统计粒度太细导致内存膨胀。比如把每个带不同ID的URL都当作独立key,几天后可能积累几十万条目。建议对路径做模板化,例如将/user/123统一成/user/{id},再存入统计表。
小结与扩展思路
通过中间件加内存计数器,我们仅用几十行代码就完成了Golang服务的请求量、成功率和耗时监控。该方案易嵌入、易调试,适合不想引入复杂组件的项目。若后续需要持久化或告警,可将快照推送到远程时序库,或改用Prometheus客户端暴露格式,实现平滑升级。
在真实生产中,还可以增加按响应状态码细分、慢请求采样、以及滑动窗口限流等能力。核心思路不变:在请求生命周期的关键节点插入观测代码,用并发安全的数据结构汇总,再以独立通道暴露结果。