通用日志装饰器需结构化记录全链路状态,支持trace_id、参数快照、耗时、结果/错误等字段,严格保持原函数签名,提供采样与动态开关,并明确panic处理边界——仅记录后向上抛出。

用高阶函数实现通用日志装饰器来记录业务逻辑全链路状态,核心不是“包一层就完事”,而是让每条日志能串起一次完整调用上下文——包括入口、中间节点、错误位置、耗时、参数快照和返回结果。这需要结构化、可追溯、可开关的设计,而不是简单打印函数名。
保持签名一致,才能无感接入业务函数
通用日志装饰器必须严格匹配原函数的参数类型、数量和返回值类型。否则无法直接赋值给原有变量或传入已有接口。Go 中没有泛型前,常见做法是按常用签名分组定义,比如:
-
LogFunc1:接收 1 个参数、返回 1 个值(如
func(int) string) -
LogFunc2:接收 2 个参数、返回 1 个值(如
func(string, bool) error) - LogFunc0:无参无返回(适合初始化类操作)
不推荐用 reflect 动态适配所有签名——它绕过编译检查、性能差、日志字段不可控,且难以注入 trace ID 等关键上下文。
注入结构化上下文,支撑链路追踪
全链路日志不是拼字符串,而是把每次调用当作一个事件单元,带齐以下字段:
- trace_id:从 HTTP header 或 context 透传下来,确保跨函数、跨 goroutine 一致
-
func_name:用
runtime.FuncForPC(reflect.ValueOf(fn).Pointer()).Name()提取(注意仅用于开发/调试,生产建议显式传入) - args:只序列化轻量级值(如 ID、状态码),避免记录大 struct 或指针内容
-
took_ms:纳秒级计时,用
time.Since(start)计算 -
result / error:成功时记录关键返回字段(如
"user_id": 123),失败时记录err.Error()和堆栈摘要
使用 zap.Logger.With() 或 logrus.WithFields() 构造子 logger,一次性注入全部字段,避免多次 .With() 造成对象拷贝开销。
支持采样与动态开关,避免压垮服务
高频业务函数(如订单校验、库存查询)若每调用必打日志,会显著拖慢吞吐。应提供两级控制:
-
全局开关:通过配置项或环境变量控制是否启用日志装饰(如
LOG_DECORATOR_ENABLED=false) -
按函数采样:对指定函数设置采样率(如
SampleRate: 0.01表示 1% 请求打全量日志)
采样逻辑放在装饰器最外层,避免创建 logger、序列化参数等开销。未命中采样的调用直接执行原函数,零日志成本。
明确 panic 处理边界,不静默吞错
日志装饰器里加 defer recover 是危险操作。它会让上层调用方收不到 panic,HTTP handler 返回 200 却没执行成功,排查时完全无迹可寻。
正确做法是:
- 只记录 panic 发生的事实(
logger.Error("panic caught", zap.String("func", name), zap.String("panic", r.(string)))) - 记录后立即
panic(r)向上传播 - 真正的错误拦截和降级,交给顶层中间件或 panic 恢复层统一处理
这样既保留了可观测性,又不破坏错误传播语义。

















