必须在鉴权中间件(如jwt解析后)将用户id注入context.context,再由日志中间件通过安全断言提取并结构化写入;禁用url或header直取、避免字符串拼接、需分片限流文件输出、并发写入须加锁。

中间件里怎么拿到用户ID并塞进日志上下文
关键不是“加字段”,而是确保请求生命周期内用户ID能稳定透传。常见错误是直接从 req.URL.Query().Get("uid") 或 req.Header.Get("X-User-ID") 取,但没做校验或 fallback,导致日志里出现空字符串或默认值。更稳妥的做法是在鉴权中间件(比如 JWT 解析后)把用户 ID 存进 context.Context,再由日志中间件读取。
实操建议:
- 用
context.WithValue(ctx, userIDKey, uid)注入,userIDKey推荐定义为私有type ctxKey string类型,避免 key 冲突 - 日志中间件中用
uid, ok := ctx.Value(userIDKey).(string)安全断言,失败时用"unknown"占位,不 panic - 别在 handler 里重复解析 token 或查 DB 拿 UID——中间件已做,复用即可
用 zap 或 logrus 实现按 UID 分文件输出
标准库 log 不支持动态 writer 切换,zap 和 logrus 才可行。核心思路是:每个 UID 对应一个独立的 io.Writer(比如带 UID 前缀的文件),再通过 hook 或自定义 encoder 把日志路由过去。但注意——UID 数量不可控,不能为每个 UID 开一个文件句柄,必须做归一化或限流。
实操建议:
- 用哈希取模控制文件数量:
shard := hash(uid) % 100,生成logs/user_042.log这类固定分片文件,避免文件爆炸 - zap 用户用
zapcore.AddSync(&fileWriter{shard: shard})包装 writer;logrus 用户注册hook := &CustomHook{shardMap: make(map[int]*os.File)} - 务必设置文件轮转(如
lumberjack.Logger),否则单个文件无限增长
日志行内怎么安全注入 UID 字段
最常踩的坑是把 UID 拼进 message 字符串里,比如 log.Printf("[%s] req /api/foo", uid),这会丢失结构化能力,也难过滤。正确做法是作为结构化字段写入。
实操建议:
- zap:统一用
logger.With(zap.String("uid", uid))包一层,后续所有Info/Error自动带上该字段 - logrus:用
log.WithField("uid", uid),注意不要每次调用都新建 logger,应在中间件里 once-per-request 构建 - UID 含敏感字符(如空格、换行)要先清理:
strings.Map(func(r rune) rune { if unicode.IsControl(r) { return -1 }; return r }, uid)
并发写同一个 UID 日志文件会不会丢日志
会,如果多个 goroutine 直接往同一个 *os.File 写,且没加锁或缓冲,大概率出现日志行错乱甚至截断。zap 默认用 WriteSyncer 封装,内部已加锁;logrus 的 FileHook 若没配 levels 分离,也可能竞争。
实操建议:
- zap 用户确认用了
zapcore.Lock包装文件 writer:zapcore.AddSync(zapcore.Lock(fileWriter)) - logrus 用户避免用
log.SetOutput全局设文件,改用Hook并确保Fire方法内对 writer 加锁 - 高吞吐场景下,优先考虑异步写入(如 zap 的
zap.WrapCore+ 队列),而不是同步锁阻塞 handler
UID 分类日志真正的难点不在“怎么加字段”,而在“怎么不让文件句柄、锁、内存分配成为瓶颈”。分片数、writer 复用策略、UID 清洗时机,这三个点漏掉任何一个,上线后都可能突然卡住。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











