logrus需手动提取context.value并注入日志字段,必须检查nil和类型断言,避免panic;zap通过middleware预提字段复用;slog可自定义handler在handle时读取context注入属性;禁止直接记录context接口。

logrus 里怎么把 context.Value 带进日志字段
logrus 本身不感知 context.Context,必须手动提取并注入字段。常见错误是直接传 ctx 进 WithField 或想当然调用 ctx.Value() 却没做类型断言和空值检查。
正确做法是封装一个带上下文的日志函数,例如:
func LogWithCtx(log *logrus.Entry, ctx context.Context) *logrus.Entry {
if ctx == nil {
return log
}
if reqID := ctx.Value("request_id"); reqID != nil {
if s, ok := reqID.(string); ok {
log = log.WithField("request_id", s)
}
}
if userID := ctx.Value("user_id"); userID != nil {
if i, ok := userID.(int64); ok {
log = log.WithField("user_id", i)
}
}
return log
}
- 务必检查
ctx.Value()返回值是否为nil,否则 panic - 类型断言失败时不要静默丢弃,至少打个 warn 或 fallback 到空字符串
- 避免在日志函数里调用
ctx.Deadline()或ctx.Err()—— 这些属于控制流,不是日志元数据
zap 日志中如何让 context.WithValue 的键自动转成 fields
zap 没有内置 context 支持,但可以借助 zapcore.Core 或中间件式包装器实现字段透传。更轻量的做法是定义统一的 context key 类型(比如 type ctxKey string),再配合 zap.Stringer 接口或预定义提取函数。
推荐在 HTTP middleware 中一次性提取并挂载到 zap.Logger 上:
func WithContextFields(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
fields := []zap.Field{
zap.String("request_id", getReqID(ctx)),
zap.Int64("user_id", getUserID(ctx)),
}
logger := zap.L().With(fields...)
r = r.WithContext(context.WithValue(ctx, loggerKey, logger))
next.ServeHTTP(w, r)
})
}
- 不要在每次
logger.Info()前都重复调用ctx.Value()—— 提取一次,复用字段数组 - zap 的
With()返回新 logger,开销低,适合 per-request 绑定 - 避免用
interface{}当 key,会导致 map 查找失效;固定用自定义类型如type loggerKey struct{}
为什么不能直接把 context.Context 当字段塞进日志
因为 context.Context 是接口类型,直接传给 WithField("ctx", ctx) 会触发 fmt.Sprintf("%+v", ctx),输出类似 &{0xc000123456 ...} 的内存地址和未导出字段,既无业务意义又可能泄露内部结构。
- logrus 默认对非基础类型调用
fmt.Sprint,zap 则直接拒绝非基本/可序列化类型(报错unknown field type) - 某些 context 实现(如
valueCtx)嵌套深,暴力打印可能栈溢出 - 真正需要记录的是业务语义字段(如 trace_id、tenant_id),不是 context 的生命周期状态
用 slog(Go 1.21+)时 context 标签怎么透传更自然
slog 支持 Handler 层级的属性注入,比 logrus/zap 更贴近 context 模型。关键不是“往日志里塞 context”,而是让 handler 在处理每条记录时主动读取当前 goroutine 的 context。
示例:自定义 slog.Handler,在 Handle() 方法里提取:
type ContextHandler struct {
slog.Handler
}
func (h ContextHandler) Handle(ctx context.Context, r slog.Record) error {
if reqID := ctx.Value("request_id"); reqID != nil {
if s, ok := reqID.(string); ok {
r.AddAttrs(slog.String("request_id", s))
}
}
return h.Handler.Handle(ctx, r)
}
- slog 的 context 是 handler 执行时传入的,不是 record 自带的 —— 所以必须确保调用
slog.InfoContext(ctx, ...),而非slog.Info() - handler 里不能修改
r的原始时间、level 等字段,但AddAttrs是安全的 - 如果用了
slog.With()创建子 logger,它的 attrs 会和 handler 注入的合并,注意字段名冲突
var RequestIDKey = struct{}{} 并导出,是最小意外成本的选择。golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











