折扣服务在付款前超时,调用方应该拿到什么?如果只返回一个看似正常的金额,账务和排障都会失去关键信息。降级结果应当可识别,且不能把“没拿到折扣”悄悄写成业务成功。
超时是信号,回退是业务决定
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:取消与截止时间