go中需用defer+recover捕获panic并转为error返回,因panic会中断执行,而recover仅在defer函数中有效,可阻止崩溃并获取panic参数,再统一转为error实现健壮错误处理。

为什么不能直接在函数里用 log.Printf 打入参出参
因为日志位置分散、格式不统一,且无法自动捕获 panic 后的返回值或未执行完的路径。更关键的是:你没法拿到调用栈里真实的业务函数名和行号——log.Printf 里写的 r.URL.Path 或 userID 是变量名,不是字段名;而真正要审计的,是“谁在 service/user.go:105 调用了 GetUserByID,传了 id=123,返回了 &User{...}”。原生日志只记录字符串拼接结果,不结构化,也不带上下文。
用高阶函数包装 + defer 实现无侵入记录
Go 没有装饰器语法,但可以用函数组合模拟。核心思路是:把目标函数转成 func(...interface{}) (interface{}, error) 类型,再套一层 wrapper,在入口打参数、出口打返回值。注意三点:
- 必须用
defer捕获 panic,并统一返回nil, err,否则出参日志会漏掉 - 入参日志要提前
fmt.Sprintf("%v", args),避免闭包延迟求值导致指针已变 - 出参日志不能直接
fmt.Sprintf("%+v", result)—— 若result是大 struct 或含sync.Mutex,会 panic;应先做类型判断,对不可序列化类型只记reflect.TypeOf(result).String()
如何避免日志污染和性能损耗
高频函数(如数据库查询、JSON 解析)加日志容易拖慢服务。实际落地时得控制粒度:
- 只对顶层业务函数(如
HandleUserCreate、ProcessPayment)启用,不递归包裹内部工具函数 - 用
context.Context透传开关:比如ctx = context.WithValue(ctx, logKey, true),wrapper 里检查该 key 再决定是否记录 - 禁止记录敏感字段:提前定义黑名单(如
"password","token","secret"),用reflect遍历结构体字段名做过滤,而不是靠正则匹配字符串 - 出参日志默认只记录类型和地址(
&User{0xc000123456}),开启调试模式才展开字段值
call_site 和 time.Now().UTC() 是最容易错的两个点
日志字段里 call_site 不是 runtime.Caller(1) 就行——它得跳过 wrapper 层和当前匿名函数层,通常用 runtime.Caller(2);而 event_time 必须是 UTC,否则 K8s 集群里多个节点日志时间对不上,查问题时根本分不清先后。这两个字段一旦写错,整条日志就失去可追溯性:你看到 “GetUserByID 返回了 nil”,却不知道是在哪一行触发的,也不知道它发生在请求开始后第 3ms 还是第 3s。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











