go中不存在全局未捕获异常,panic不跨goroutine传播,error须显式处理;柔性降级需分层拦截panic与error,http中间件用defer-recover捕获panic并返回500,业务error靠context传递与显式判断,fallback须纯内存、无副作用且带监控。

Go 里不存在“全局未捕获异常”的概念——panic 不会跨 goroutine 传播,error 本就不该被“未捕获”。所谓“柔性降级”,本质是把本该显式处理的业务错误(error)和本该避免的运行时崩溃(panic)分层拦截,并在关键路径上注入轻量 fallback 分支。
HTTP 中间件必须包裹 recover,但仅限 panic 拦截
每个 HTTP 请求都在独立 goroutine 中执行,recover() 只能在该 goroutine 的 defer 函数中直接调用才有效。中间件是唯一能覆盖全部 handler 的位置:
- 别在
http.HandleFunc里每个函数都写一遍defer recover()—— 易漏、难维护、无法统一日志格式 -
recover()后必须立刻调用w.WriteHeader(500),否则响应状态码默认为 200,前端可能解析空响应失败 - 不要原样输出
panic堆栈:生产环境需脱敏,至少过滤掉文件路径与行号;可用runtime/debug.Stack()截取前几帧并哈希化 - recover 不等于恢复业务逻辑——panic 前已写的响应头、发的 MQ 消息、改的 DB 状态不会回滚,只能做上报和 cleanup
业务 error 不能靠 recover,得靠 context 和显式判断
95% 的“降级需求”其实来自可预期失败:超时、503、连接拒绝。这些返回的是 error,不是 panic,recover 完全无效。
- 主调用必须绑定
context.WithTimeout,例如ctx, cancel := context.WithTimeout(req.Context(), 800*time.Millisecond),而非依赖http.Client.Timeout - 降级触发条件要精确:只对
errors.Is(err, context.DeadlineExceeded)、net.ErrClosed、HTTP 状态码502/503/504做 fallback,绝不降级400或sql.ErrNoRows - fallback 函数必须无副作用:禁止调用外部服务、禁止查主库、禁止
time.Now()或rand.Intn();推荐从sync.Map读预热缓存,或返回硬编码结构体 - 别用
err == nil当降级开关——某些 SDK 超时后返回nil结果 + 非空error,需同时判结果和 err
熔断器必须按接口粒度隔离,且 timeout 不是单次超时
用 sony/gobreaker 时,常见误用是把整个服务绑到一个熔断器上,或混淆 Settings.Timeout 含义。
-
Settings.Timeout是熔断开启后保持“打开”状态的持续时间(比如 30 秒),不是单次调用超时——单次超时仍由context.WithTimeout控制 - 每个下游依赖(如
user-service/get、order-service/list)应配独立CircuitBreaker实例,Name字段需可识别、可监控 -
ReadyToTrip别设成简单计数器:用counts.ConsecutiveFailures > 5容易在低流量下误熔断;建议结合失败率(float64(counts.TotalFailures) / float64(counts.TotalRequests)) - 熔断器只 wrap 真正的业务调用点,比如
client.Do()或db.QueryRow(),别 wrap 整个http.HandlerFunc.ServeHTTP——否则客户端断连也会被计入失败
降级逻辑必须放在 service 层,而非 handler
handler 层只负责接收请求、校验参数、调用 service、返回响应。把降级塞进 handler 会导致三重问题:复用困难、熔断超时无法统一控制、备用逻辑失控。
- service 方法签名应为
func(ctx context.Context, userID string) (*User, error),由它内部判断是否降级,而非 handler 里写两套 HTTP 调用 - 降级分支必须纯内存或本地缓存:Redis 缓存失效时若 fallback 再查 Redis,就可能击穿导致全量走兜底,兜底本身反成瓶颈
- 兜底数据需提前预热:启动时加载默认 banner、热门商品列表等至
sync.Map,避免首次请求触发冷加载延迟 - 动态开关要支持运行时生效:通过原子变量或配置中心监听,避免重启才能切降级开关
最易被忽略的点是 fallback 的可观测性——它不报错,却可能掩盖真实故障。必须给每个降级分支打独立 metric(如 api_fallback_count{endpoint="user.get",reason="timeout"}),并设置阈值告警。没有监控的降级,等于把问题藏进黑盒。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











