Golang微服务如何处理错误_Golang错误传播机制

5次阅读

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

Golang微服务如何处理错误_Golang错误传播机制

go微服务中错误不能只用log.Fatalpanic兜底

微服务里一旦用log.Fatalpanic处理业务错误,会导致整个服务进程退出,gRPC/HTTP服务直接不可用,熔断、重试、链路追踪全失效。真实场景中,90%的错误是可恢复的(如下游超时、参数校验失败、数据库唯一约束冲突),该返回带状态码的响应,而不是让进程跪。

正确做法是把错误作为返回值显式传递,并在关键边界做转换:

  • HTTP handler 中将 error 转为 http.StatusXXXjsON 错误体(如 {"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.NotFound404codes.Unavailable503
  • 内部错误(如 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 版本函数(QueryRowContextDoContextSendContext),并确保下游服务也遵循同一套 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() 检查里。

text=ZqhQzanResources