fmt.Printf("%+v") 不适合生产日志,因其输出未导出字段、指针地址、函数值等不安全内容,遇循环引用会 panic,且无敏感字段过滤、自定义格式和深度限制。

为什么 fmt.Printf("%+v") 不适合生产日志
它会输出全部字段,包括未导出字段(首字母小写)、指针地址、函数值、channel 等,既不安全也不可读。更严重的是,遇到循环引用结构体时直接 panic:runtime: goroutine stack exceeds 1000000000-byte limit。
真正需要的是:只打印导出字段 + 忽略敏感字段 + 支持自定义格式(如时间转 ISO8601)+ 容错不 panic。
- 用
reflect.ValueOf(v).Kind() == reflect.Ptr先解指针,避免日志里全是&{...} - 对
time.Time、json.RawMessage等常见类型做特判,否则默认 String() 输出冗长或不可读 - 必须加递归深度限制(比如 5 层),否则嵌套 map/slice/struct 深了就栈溢出
如何用 reflect.StructField 过滤和重命名字段
反射拿到的 StructField 包含 Name、Type、Tag,关键在解析 struct tag —— 比如 json:"user_id,omitempty" 或自定义 log:"id,redact"。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 优先检查
field.Tag.Get("log"),没设才 fallback 到jsontag;这样业务层可独立控制日志行为,不污染序列化逻辑 - 支持
log:"-"完全忽略字段,log:",redact"替换为"***",比全局黑名单更灵活 -
field.Anonymous为 true 时,要展开内嵌结构体字段,但需防止名字冲突(比如两个内嵌 struct 都有ID),建议加前缀:user.ID、order.ID
reflect.Value 取值时最常踩的三个坑
不是所有 reflect.Value 都能直接 .Interface() —— 比如未导出字段返回零值,nil interface{} 会 panic,unsafe.Pointer 类型直接崩溃。
- 取值前必加
if !v.IsValid() || !v.CanInterface(),尤其处理 map value 或 slice 元素时容易 invalid - 对
interface{}类型,先v.Elem()再判断是否为 nil,否则v.Interface()可能返回nil但实际是*int(nil) - 时间字段别直接
v.Interface().(time.Time),用v.Convert(reflect.TypeOf(time.Time{})).Interface().(time.Time)更安全,避免 interface 底层类型不匹配
性能敏感场景下要不要缓存 reflect.Type
要。每次 reflect.TypeOf(v) 都触发 runtime 类型查找,实测在 QPS 万级日志中,缓存后 CPU 占用下降 40%+。
正确做法:
- 用
sync.Map缓存reflect.Type→ 字段信息映射,key 用t.String()(不是t.Name(),后者对泛型或匿名 struct 返回空) - 缓存内容至少包含:导出字段列表、每个字段的 log tag 解析结果、是否含 time/json.RawMessage 等特殊类型
- 不缓存
reflect.Value,它绑定具体实例,无法复用;只缓存类型层面的元信息
最易被忽略的是:泛型结构体(如 Result[T])每次实例化都是新 reflect.Type,缓存 key 必须包含完整类型字符串,否则漏缓存。











