ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

最后一分钟折扣变更:超时降级代码怎样才算兜住了

最后一分钟折扣变更:超时降级代码怎样才算兜住了

折扣服务在付款前超时,调用方应该拿到什么?如果只返回一个看似正常的金额,账务和排障都会失去关键信息。降级结果应当可识别,且不能把“没拿到折扣”悄悄写成业务成功。

超时是信号,回退是业务决定

Go里等待取消用ctx.Done(),context.DeadlineExceeded是错误值,不是Context的成员通道。下面把两件事拆开:上游函数负责感知取消,调用方根据已批准的规则返回带标记的回退值。

package main import ( "context" "errors" "fmt" "time" ) type Quote struct { Cents int IsFallback bool Reason string } func quote(ctx context.Context, original int, upstream func(context.Context) (int, error)) (Quote, error) { value, err := upstream(ctx) if err == nil { return Quote{Cents: value}, nil } if errors.Is(err, context.DeadlineExceeded) { return Quote{Cents: original, IsFallback: true, Reason: "discount_timeout"}, nil } return Quote{}, err } func main() { ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(-time.Second)) defer cancel() wait := func(ctx context.Context) (int, error) { <-ctx.Done() return 0, ctx.Err() } q, err := quote(ctx, 10000, wait) if err != nil || !q.IsFallback || q.Cents != 10000 { panic("fallback check") } q, err = quote(context.Background(), 10000, func(context.Context) (int, error) { return 9000, nil }) if err != nil || q.IsFallback || q.Cents != 9000 { panic("success check") } failure := errors.New("invalid discount rule") _, err = quote(context.Background(), 10000, func(context.Context) (int, error) { return 0, failure }) if !errors.Is(err, failure) { panic("business error was hidden") } fmt.Println("timeout, success and business-error checks: passed") }

别让代码替业务批准原价付款

此例假设超时后可以返回原价。真实业务可能要求阻断付款、保留订单等待重试,或明确征求用户确认。三种规则的财务后果不同,应先选定规则再写代码。

IsFallback还需要贯穿日志、指标和调用方展示,否则标记只在结构体里存在。可以统计回退比例,但不要把回退响应混入折扣成功率。重复请求涉及幂等、价格快照和支付状态,本例没有实现。

取消也需要上游配合

Context不会强行终止忽略它的函数。这里注入的上游会等待取消;真实HTTP客户端、数据库驱动和内部重试都要继续传递Context。另起goroutine然后不管泄漏,并不能制造真正的超时保障。

本例验证三条本地控制路径,没有连接折扣服务,也没有测试网络、并发或支付链路。适合先固定错误语义,再用集成测试验证整条链路。

参考

  • Go context:取消与截止时间
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进