Go error不可跨服务传播,需用结构化ErrorResponse;勿用fmt.Errorf包装远程错误;日志须用.Err(err)保留链路;自定义错误必须实现Unwrap()和Error()以支持errors.Is/As判定。

Go 的 error 类型不适合跨服务传播
微服务间通信依赖序列化(如 JSON、Protobuf),而 Go 原生 error 是接口类型,无法直接编码。常见错误是把 err 直接塞进 HTTP 响应体或 gRPC 返回值,结果得到空对象或 panic。
- HTTP 场景下,
json.Marshal(err)总是返回null,因为error接口没有导出字段 - gRPC 中若在
proto定义外硬塞err.Error(),会丢失堆栈、码、上下文等关键信息 - 正确做法:统一用结构化错误响应,例如定义
ErrorResponse{Code: "INVALID_INPUT", Message: "...", Details: map[string]interface{}{}}
不要用 fmt.Errorf 包装远程调用错误
对下游服务的 HTTP/gRPC 调用失败时,原错误往往已含状态码、超时标识、重试建议等语义。用 fmt.Errorf("failed to call X: %w", err) 会抹掉这些信息,只剩字符串描述。
-
errors.Is(err, context.DeadlineExceeded)在包装后失效 ——%w不传递底层类型断言能力 - 推荐用专用错误转换函数,例如:
func ToServiceError(err error) *ServiceError { if errors.Is(err, context.DeadlineExceeded) { return &ServiceError{Code: "TIMEOUT", HTTPStatus: 408} } if httpErr, ok := err.(*url.Error); ok && httpErr.Err != nil { return &ServiceError{Code: "CONNECTION_FAILED", Cause: httpErr.Err} } return &ServiceError{Code: "UNKNOWN", Raw: err} } - 所有出站请求的错误必须经过此层转换,确保上游能做策略判断(比如只对
TIMEOUT重试)
日志中的错误不能只打 err.Error()
微服务排查依赖链路追踪和集中日志。仅记录 err.Error() 会让问题定位退化成“猜谜”——没有堆栈、没有调用路径、没有原始错误类型。
- 使用
log.With().Err(err).Msg("failed to process order")(如 zerolog/zap),它会自动展开Unwrap()链并保留字段 - 避免
log.Printf("error: %v", err)—— 它忽略所有额外字段,且不格式化嵌套错误 - 对关键错误(如支付失败),强制附加 trace ID 和 span ID:
logger.Error().Str("trace_id", traceID).Str("span_id", spanID).Err(err).Msg("payment declined")
自定义错误类型必须实现 Unwrap() 和 Error()
Go 1.13 引入的错误链机制依赖 Unwrap(),但很多团队写的 ServiceError 只实现了 Error(),导致 errors.Is() 和 errors.As() 失效。
立即学习“go语言免费学习笔记(深入)”;
- 必须返回非 nil 的
error:如果当前错误封装了底层错误,Unwrap()应返回它;否则返回nil - 不要在
Error()中拼接Unwrap().Error()—— 这会导致重复打印,且破坏错误链遍历 - 示例:
type ServiceError struct { Code string Message string Cause error } func (e *ServiceError) Error() string { return e.Message } func (e *ServiceError) Unwrap() error { return e.Cause }
errors.Is(err, MyTimeout) 成立,否则熔断、重试、告警策略全都会失效。










