
Go 错误处理别用 log.Fatal 写审计日志
审计日志不是崩溃日志,log.Fatal 会直接终止进程,导致后续关键操作(如事务回滚、资源清理、异步上报)全部丢失。合规场景下,一次错误触发审计,但服务必须继续运行。
- 审计日志要独立采集、异步落盘或发往 SIEM 系统,和业务错误处理解耦
- 用
errors.Join或自定义错误类型封装原始错误 + 审计上下文(如用户ID、操作类型、资源路径),而不是只记err.Error() - 避免在
defer里调用log.Fatal—— 比如 HTTP handler 中 panic 恢复后又 fatal,会导致整个 server 进程退出 - 真实报错现象:
panic: runtime error: invalid memory address后没看到审计记录,就是因为进程在写日志前已退出
用 fmt.Errorf 带 %w 包装错误时,审计字段不能丢
Go 1.13+ 推荐用 %w 包装错误以便 errors.Is/errors.As 判断,但审计需要的元信息(如操作人、时间戳、IP)不会自动继承。不显式携带,审计链就断了。
- 错误包装必须手动注入审计字段:比如
fmt.Errorf("failed to update user %s: %w", userID, err),而不是fmt.Errorf("update failed: %w", err) - 如果用中间件统一加审计,注意
err可能是 nil —— 很多 handler 忘了 return 错误,导致审计日志里出现空操作记录 -
%w只保留一个底层错误;若需多错误聚合审计(如 DB 写失败 + 缓存失效),得用errors.Join,再统一附加审计上下文
审计日志字段必须结构化,别依赖 fmt.Sprint(err)
把错误转成字符串再塞进 JSON 日志,等于放弃结构化分析能力。合规审计要求字段可筛选、可关联、可溯源,而 err.Error() 是纯文本,无法提取 resource_id 或 action_type。
- 定义审计事件结构体,例如:
type AuditEvent struct { Timestamp time.Time `json:"ts"` UserID string `json:"user_id"` Action string `json:"action"` Resource string `json:"resource"` ErrMsg string `json:"err_msg,omitempty"` ErrCode string `json:"err_code,omitempty"` // 如 "DB_CONN_TIMEOUT" } - 从错误中提取码而非描述:用
errors.Is(err, sql.ErrNoRows)判断后写"NOT_FOUND",而不是记"sql: no rows in result set" - 日志输出必须用
json.Marshal,禁用fmt.Printf("%+v")—— 后者字段名大小写、嵌套格式不可控,SIEM 解析失败率高
HTTP handler 里别在 WriteHeader 后写审计日志
有些团队把审计日志写在 http.ResponseWriter.Write 之后,以为“响应发出去了才记”,结果发现大量审计缺失。问题在于:连接可能已断开、中间件已返回、甚至反向代理已超时关闭连接 —— 此时写日志容易 panic 或静默失败。
立即学习“go语言免费学习笔记(深入)”;
- 审计日志必须在 handler 主逻辑结束前、且 HTTP 状态码确定后立即生成(比如 switch status 之后,
w.WriteHeader之前) - 不要等 defer 执行 ——
defer在函数 return 后才跑,而 handler 可能因 panic 提前退出,defer 不保证执行 - 更稳的做法:用中间件统一捕获
responseWriter的状态码和字节数,在next.ServeHTTP返回后立刻写审计,不依赖 handler 内部顺序










