微服务中错误应显式返回并分层处理:HTTP/gRPC边界转状态码,跨服务用status.Code或errors.Is判断,包装错误必用%w保留链,禁用log.Fatal/panic避免进程退出。

Go微服务中错误不能只用log.Fatal或panic兜底
微服务里一旦用log.Fatal或panic处理业务错误,会导致整个服务进程退出,gRPC/HTTP服务直接不可用,熔断、重试、链路追踪全失效。真实场景中,90%的错误是可恢复的(如下游超时、参数校验失败、数据库唯一约束冲突),该返回带状态码的响应,而不是让进程跪。
正确做法是把错误作为返回值显式传递,并在关键边界做转换:
- HTTP handler 中将
error转为http.StatusXXX和 JSON 错误体(如{"code": 400, "message": "invalid user_id"}) - gRPC server 中将
error转为status.Error(codes.Code, msg),确保codes.InvalidArgument/codes.NotFound等语义准确 - 跨服务调用时,用
errors.Is(err, xxxErr)判断是否为预期错误,避免把网络超时当成业务逻辑失败
用fmt.Errorf包装错误时必须加%w才能保留原始错误链
Go 1.13 引入的错误包装(wrap)机制不是摆设。如果写成 fmt.Errorf("failed to save user: %v", err),原始错误就丢了——你再也无法用 errors.Is(err, sql.ErrNoRows) 或 errors.As(err, &pgErr) 做类型判断或提取细节。
必须用 %w:
立即学习“go语言免费学习笔记(深入)”;
if err != nil {
return fmt.Errorf("failed to save user: %w", err) // ✅ 可追溯
}
常见陷阱:
- 日志打印时误用
%w:log 库不支持%w,会 panic;该用%v或%+v(配合github.com/pkg/errors或 Go 1.20+ 的errors.Join) - 中间件/拦截器里二次包装但漏掉
%w,导致最外层 error 链断裂 - HTTP handler 中直接
return err而没做 status 映射,客户端收到 500 却不知道是参数错还是 DB 错
微服务间错误传播要靠status.Code而非原始 error 字符串
两个 Go 服务 A → B,A 调用 B 的 gRPC 接口,B 返回了 status.Error(codes.PermissionDenied, "user not authorized")。A 如果只看 err.Error() 去字符串匹配 "not authorized",就彻底耦合了错误文案,B 一改提示语,A 就失效。
正确方式永远是:
if status.Code(err) == codes.PermissionDenied {
// 触发降级或跳转登录页
}
同理,HTTP 微服务也应约定错误码字段(如 JSON 中的 code 字段),并用结构体解析,而非正则或 strings.Contains。
注意点:
- 不要在 error 字符串里塞敏感信息(如 SQL 语句、用户邮箱),gRPC/HTTP 响应可能被前端或网关直接透出
- 自定义错误码需与 HTTP 状态码对齐(如
codes.NotFound→404,codes.Unavailable→503) - 内部错误(如
codes.Internal)必须打日志并带上 traceID,但响应体要脱敏,返回通用提示
Context 超时和取消本身就是错误,别忽略context.DeadlineExceeded
微服务链路中,80% 的“慢请求”实际是上游已放弃,但下游还在干耗资源。如果 handler 里没检查 ctx.Err(),就会继续查 DB、调第三方 API,浪费 CPU 和连接池。
必须在每个可能阻塞的操作前检查上下文:
select {
case <-ctx.Done():
return ctx.Err() // ✅ 提前返回
default:
}
// 再执行 db.QueryRowContext(ctx, ...)
更安全的做法是所有 I/O 操作都带 Context 版本函数(QueryRowContext、DoContext、SendContext),并确保下游服务也遵循同一套 timeout 传递规则。
容易被忽略的点:
- 数据库连接池满、Redis 连接卡住时,
ctx.Err()可能晚于实际超时,需配合 client 级 timeout(如redis.Options.DialTimeout) - HTTP 客户端默认不继承父 context,必须显式传
req = req.WithContext(ctx) - goroutine 泄漏常源于忘了 select + ctx.Done(),尤其在 for-select 循环里
if err != nil { return err } 就完事;它要求你在每层明确:这个错误该不该被上层感知?要不要改变语义?要不要记录 trace?要不要触发告警?这些决策点藏在每一处 %w、每一次 status.Code 判断、每一个 ctx.Err() 检查里。










