必须逐层用%w包装错误、各模块自定义错误码、统一转换出站错误、结构化日志递归展开错误链。跨包调用需在dal层返回原始错误,service层用fmt.errorf("动作:%w")包装,handler层再加协议上下文;错误码按模块隔离定义clienterror类型及包级变量;grpc/http等出站错误须经toserviceerror统一映射为*serviceerror;日志须用zerolog/zap的.err(err)方法而非%v,确保unwrap链、code、traceid、span_id完整输出。

跨包调用时 error 丢失上下文,怎么补?
Go 没有异常栈,err 传出来就是个值,不包装就只剩一句字符串。你看到日志里只有一行 "query user from db",根本不知道是哪个 handler、哪次请求、第几层调用出的问题。
必须在每一层向上返回前用 %w 包装:
- 底层(如
dal/)只返回原始错误,比如sql.ErrNoRows或redis.Nil - 中间层(如
service/)用fmt.Errorf("failed to get user %d: %w", id, err)加业务动作和参数 - 最上层(如
handler/)再套一次,比如fmt.Errorf("HTTP GET /user/%d failed: %w", id, err)
别用 fmt.Sprintf 拼接,它会切断错误链;也别裸传 err,那等于把调用路径抹掉。
模块间共享错误码,为什么不能全局定义一个 errors.go?
因为一加新模块就得改这个文件,多人协作时容易冲突;而且所有模块共用同一段 iota,新增错误码可能踩到别人预留的区间,客户端解析直接错乱。
正确做法是每个模块自己管自己的错误码:
- 在
clienthandler/目录下定义ClientError类型,带Code int和Message string - 用包级变量声明具体错误:
ErrInvalidClientID = &ClientError{Code: 1001, Message: "client ID is invalid"} - 对外只暴露构造函数或变量,不暴露内部结构体字段
这样新增模块只需加个目录、写自己的错误定义,完全无耦合,也不怕重号。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
微服务里模块通信,gRPC 和 HTTP 调用的错误怎么统一处理?
不同协议返回的错误类型五花八门:status.Error、*url.Error、net.OpError……直接透传给上层,handler 得写一堆类型断言,维护成本爆炸。
必须设一道转换层,比如叫 ToServiceError:
- 输入任意
error,输出统一的*ServiceError结构体 - 对
context.DeadlineExceeded映射为Code: "TIMEOUT",方便上游做重试决策 - 对
*url.Error提取底层err.Err再判断是否是连接超时或 DNS 失败 - 所有出站请求(HTTP client、gRPC client、DB driver)都过这层
漏掉任何一个出口,错误就“脱管”,日志里只剩 "rpc error: code = Unknown desc = ..." 这种废信息。
log 打 error 为什么不能只写 log.Printf("err: %v", err)?
因为 %v 只调 Error() 方法,丢掉所有包装信息、原始类型、Unwrap() 链。你查日志时看不到堆栈、看不出是不是 sql.ErrNoRows、也找不到 trace_id。
要用结构化日志库(如 zerolog 或 zap)的 .Err(err) 方法:
- 它会自动递归展开
Unwrap(),打出完整错误链 - 保留字段如
Code、TraceID、ReqID,不用手动塞 map - 关键错误必须显式附加 span ID:
logger.Err(err).Str("span_id", span.SpanContext().SpanID().String()).Msg(...)
真正难的不是写代码,而是让每个模块在返回 error、记录 error、转换 error 时,都遵守同一套链路规则——少一个环节,整条调用链的可观测性就断掉。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










