Go微服务错误需显式分类处理:BusinessError含Code/Message/HTTPStatus,SystemError含TraceID/cause/操作建议;统一映射gRPC与HTTP错误;包装错误用%w但生产环境须脱敏;所有错误传播必须响应context超时与cancel。

Go 语言没有异常机制,error 是一等公民,微服务中错误必须显式传递、分类、携带上下文,否则链路追踪失效、重试逻辑混乱、日志无法定位根因。
用自定义 error 类型区分业务错误与系统错误
直接返回 errors.New 或 fmt.Errorf 会导致下游无法判断错误性质——是该重试(如网络超时)、跳过(如用户参数非法),还是告警(如数据库连接中断)。应定义两个基础类型:
-
BusinessError:含Code(如"USER_NOT_FOUND")、HTTPStatus、可透传给前端的Message -
SystemError:含TraceID、原始错误(cause)、建议操作("retry_after_1s")
示例:
type BusinessError struct {
Code string
Message string
HTTPStatus int
}
func (e *BusinessError) Error() string { return e.Message }调用方用 errors.As 判断类型,而非字符串匹配。
在 gRPC 和 HTTP 层统一错误映射
同一错误在不同协议层需转为对应语义:gRPC 要设 status.Code 和 status.Message,HTTP 要写入响应体和状态码。别在每个 handler 里重复写 switch。
- 定义全局映射表:
map[error]struct{ code codes.Code; httpStatus int } - 中间件/拦截器中统一处理:
grpc.UnaryServerInterceptor捕获返回的error,查表转成status.Error - HTTP handler 中用
http.Error或自定义响应结构,避免裸写w.WriteHeader(500)
漏掉映射会导致 gRPC 客户端收到 Unknown 状态码,HTTP 前端看到 500 却不知是参数错还是服务崩了。
立即学习“go语言免费学习笔记(深入)”;
错误链中保留原始 cause,但禁止暴露敏感信息
用 fmt.Errorf("failed to fetch user: %w", err) 包装错误可保留调用链,但生产环境必须过滤:os.Getenv("ENV") == "prod" 时清空 %w 的 message,只留 code 和 traceID。
- 日志记录时用
log.Error(err, "user service failed")(假设用 zap),它会自动展开%w链 - 序列化到响应体前,调用
err.(interface{ Unwrap() error }).Unwrap()获取最底层错误,检查是否含密码、token、路径等字段 - 所有外发错误必须经过
Sanitize()方法,哪怕只是把os.PathError的Path字段置空
Context 超时与 cancel 必须触发可终止的错误传播
微服务依赖多,一个 context.WithTimeout 被忽略,会导致 goroutine 泄漏、下游持续等待。错误必须携带 context.Canceled 或 context.DeadlineExceeded 标识。
- 调用下游前检查:
if err := ctx.Err(); err != nil { return err } - select + channel 操作必须包含
ctx.Done()分支,并返回ctx.Err() - 数据库查询用
db.QueryContext(ctx, ...)而非db.Query(...);HTTP client 用client.Do(req.WithContext(ctx))
没做这点,超时后上游已放弃,你的服务还在等 MySQL 返回,错误日志里只有 "context deadline exceeded",但根本看不出卡在哪一层。










