Go中error必须显式检查,禁用panic兜底;审计日志需结构化、异步写入,关键字段如user_id、action、err_code、trace_id缺一不可,且须在请求入口注入并全程透传。

Go里error值必须显式检查,不能靠panic兜底
很多从其他语言转过来的开发者习惯性依赖recover+panic做错误兜底,结果在HTTP handler里一panic,中间件虽能recover,但后续资源清理(比如DB事务回滚、文件句柄关闭)全丢了。更糟的是,如果recover后又调了log.Fatal,整个进程直接退出,审计日志根本写不出去。
真实问题现象:panic: runtime error: invalid memory address之后没看到审计记录,就是因为log.Fatal在defer里执行,进程终止早于日志落盘。
- 所有可能返回
error的函数调用后,必须跟if err != nil判断 - HTTP handler中禁止用
log.Fatal或os.Exit,哪怕是在recover之后 - 事务类操作(如
tx.Commit())的错误也得检查——很多人只检查db.Begin(),忘了commit也可能失败
fmt.Errorf带%w包装时,审计上下文必须手动注入
fmt.Errorf("failed to update user: %w", err)看起来很规范,但%w只传递底层错误,不带任何业务上下文。审计日志里只剩“failed to update user”,查不到是哪个用户、哪台机器、什么时间触发的。
合规审计要求字段可筛选、可关联,而纯字符串日志等于放弃溯源能力。
立即学习“go语言免费学习笔记(深入)”;
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
- 包装时显式拼入关键字段:
fmt.Errorf("failed to update user %s (ip: %s): %w", userID, ip, err) - 若需多错误聚合(如DB写失败+缓存失效),用
errors.Join(err1, err2),再统一加审计字段 - 别把
err.Error()直接塞进JSON日志字段——结构化日志要的是ErrCode和Resource这类可提取字段,不是一整段字符串
审计日志必须异步、独立采集,和业务错误处理解耦
常见错误是把审计日志写在HTTP handler末尾,或者放在defer里等response写完再记。结果连接已断开、中间件提前返回,大量审计缺失。
更隐蔽的问题是:有些handler忘了return,导致err为nil,审计日志里出现空操作记录,完全失去追踪价值。
- 审计日志走独立channel + goroutine,不阻塞主流程
- 字段必须结构化,例如
{ "user_id": "u123", "action": "update_profile", "err_code": "DB_CONN_TIMEOUT" } - 用
errors.Is(err, sql.ErrNoRows)判断后写"NOT_FOUND",而不是记"sql: no rows in result set" - 输出必须用
json.Marshal,禁用fmt.Printf("%+v")——后者字段名大小写不可控,SIEM系统解析失败率高
日志库选型:slog够用就别引入zap/logrus
Go 1.21+的slog已支持结构化日志、JSON输出、自定义handler,对中小项目完全够用。硬上zap反而增加维护成本,尤其当团队还没统一日志字段规范时。
但要注意slog默认不带trace ID,而分布式追踪必须每个请求有唯一标识。这点容易被忽略,导致日志无法跨服务串联。
- 用
slog.With("trace_id", traceID)在入口处注入,避免每处都手动传 - 若已有
logrus或zap,优先复用其WithField/With方法注入trace ID和request ID - 别用
log.Println打业务日志——它没级别、没结构、没法过滤,连WARN都分不出来
return,导致审计链从源头就断了。

















