Go标准库log包缺乏上下文传递能力,无法自动注入请求ID、函数名、耗时等关键信息,导致日志不可追踪且侵入业务逻辑;需通过middleware+context实现HTTP请求级切面,或用defer+runtime.Caller实现函数级切面,并规避锁竞争、敏感信息泄露等常见坑。

为什么直接用 log 包不够用?
Go 标准库的 log 包没有上下文传递能力,也没法自动注入请求 ID、函数名、耗时等关键信息。你写 log.Printf("start"),上线后根本分不清这条日志来自哪个 HTTP 请求、哪个 goroutine、执行了多久。日志切面本质是把“通用日志逻辑”从业务代码里抽出来,而不是每处都手写 log 调用。
真正要解决的不是“怎么打日志”,而是“怎么让日志自带上下文、可追踪、不侵入业务逻辑”。
用 middleware + context.Context 实现 HTTP 请求级切面
这是最常见也最实用的场景:每个 HTTP 请求进来,自动记录入口、耗时、错误、参数(可选)。不用改 handler 函数体,只在路由注册时 wrap 一层。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 用
http.HandlerFunc包装原始 handler,在.ServeHTTP前后插入日志逻辑 - 把请求 ID 注入
context.Context(例如用req.Context().WithValue(...)),后续所有依赖 context 的日志调用都能拿到它 - 别用全局变量存 request ID —— goroutine 不安全,且无法跨中间件传递
- 耗时统计用
time.Now()和time.Since(),别用time.AfterFunc或 goroutine 异步记 —— 可能 panic 后漏记
示例片段:
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
func LoggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
reqID := r.Header.Get("X-Request-ID")
if reqID == "" {
reqID = uuid.New().String()
}
ctx := context.WithValue(r.Context(), "req_id", reqID)
r = r.WithContext(ctx)
log.Printf("[REQ %s] %s %s", reqID, r.Method, r.URL.Path)
next.ServeHTTP(w, r)
log.Printf("[DONE %s] %v", reqID, time.Since(start))
})
}
用 defer + runtime.Caller 实现函数级切面
不是所有逻辑都在 HTTP handler 里。比如数据库查询、第三方 API 调用、定时任务,也需要单独的日志埋点。这时靠 middleware 不够,得用函数装饰器模式。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 写一个通用的
LogFunc工具函数,接收函数名、入参、出参(可选)、耗时,自动格式化输出 - 用
runtime.Caller(1)获取调用方文件和行号,比硬编码字符串更可靠 - 避免在 defer 里做耗时操作(如写磁盘、网络调用)—— defer 是同步执行的,会拖慢主流程
- 如果函数返回 error,优先 log 错误,而不是只记 “end” —— 很多 bug 就卡在 error 被忽略又没日志
示例:
func LogFunc(name string, f func()) {
start := time.Now()
_, file, line, _ := runtime.Caller(1)
log.Printf("[START %s:%d %s]", filepath.Base(file), line, name)
defer func() {
log.Printf("[END %s:%d %s] %v", filepath.Base(file), line, name, time.Since(start))
}()
f()
}
别踩这些坑
实际落地时最容易翻车的几个点:
-
log.Printf在高并发下可能锁竞争严重 —— 换成zap或zerolog,它们支持无锁写入 - 用
fmt.Sprintf拼接日志内容再传给log—— 字符串拼接本身就有开销,且易引入空指针 panic;应让日志库自己处理格式化(如zap.String("key", val)) - 把敏感字段(密码、token)直接打到日志里 —— 即使是 debug 环境也不行;加个过滤逻辑,或用结构化日志字段名区分是否脱敏
- 以为加了 middleware 就一劳永逸 —— gRPC、WebSocket、定时任务、后台 goroutine 都不在 HTTP 流程里,得各自补切面

















