Go context.WithCancelCause: บอกเหตุผลที่งานถูกยกเลิก
ctx.Err() บอกได้ว่างานหยุดเพราะ canceled หรือ deadline แต่ในระบบจริงเรามักอยากรู้ว่าใครยกเลิกและเพราะอะไร เช่น deployment ปิดตัว, upstream quota เต็ม หรือ validation ล้มเหลวครับ context.WithCancelCause เพิ่มสาเหตุไว้ใน cancellation tree โดย consumer อ่านผ่าน context.Cause
WHY: การหยุดเหมือนกัน แต่การรับมือไม่เหมือนกัน
ลองนึกถึงรถไฟหยุดที่สถานี ไฟสีแดงบอกเพียงว่า “หยุด” แต่ป้ายสาเหตุบอกว่าควรรอ เปลี่ยนขบวน หรืออพยพ ctx.Err() คือไฟแดง ส่วน cause คือป้ายที่ช่วย log, metric และ retry policy ตัดสินใจได้โดยไม่แปะ state global เพิ่ม
package main
import (
"context"
"errors"
"testing"
)
var ErrDeployShutdown = errors.New("deployment is shutting down")
func waitForStop(ctx context.Context) error {
<-ctx.Done()
return context.Cause(ctx)
}
func TestWaitForStopReturnsCause(t *testing.T) {
ctx, cancel := context.WithCancelCause(context.Background())
cancel(ErrDeployShutdown)
if err := waitForStop(ctx); !errors.Is(err, ErrDeployShutdown) {
t.Fatalf("got %v, want shutdown cause", err)
}
if !errors.Is(ctx.Err(), context.Canceled) {
t.Fatalf("ctx.Err() = %v, want context.Canceled", ctx.Err())
}
}
func TestNilCauseFallsBackToCanceled(t *testing.T) {
ctx, cancel := context.WithCancelCause(context.Background())
cancel(nil)
if !errors.Is(context.Cause(ctx), context.Canceled) {
t.Fatalf("cause = %v, want context.Canceled", context.Cause(ctx))
}
}
HOW: cause แรกที่มีผลชนะ
Cancellation ไหลจาก parent ลง child สาเหตุที่เกิดก่อนและยกเลิก context นั้นจะเป็นสิ่งที่ลูกเห็น จึงไม่ควรใช้ cause เป็น event log ที่สะสมหลายเหตุการณ์ ให้เก็บ event แยกถ้าต้องรู้ timeline และเรียก cancel function เสมอเพื่อคืน timer/resource แม้งานจบตามปกติ
Cause อาจมีข้อมูลภายใน ห้ามส่ง error ทั้งก้อนไป client โดยตรง แปลงเป็น code ที่ปลอดภัยสำหรับ API ส่วน log ภายในใช้ errors.Is/As จัดหมวด ไม่เทียบข้อความ error ซึ่งเปลี่ยนง่าย
เลือกเมื่อไหร่
- ใช้
WithCancelเมื่อผู้รับต้องรู้เพียงหยุดหรือไม่หยุด - ใช้
WithCancelCauseเมื่อ operation ต้องจำแนกสาเหตุเพื่อ metric, retry หรือ audit - ใช้
WithTimeoutCauseเมื่อ deadline มีความหมายเฉพาะที่ควรรักษา - อย่าใส่ request payload, token หรือ secret ลงใน error cause
Recap
- อ่านสถานะทั่วไปจาก
ctx.Err()และเหตุผลจากcontext.Cause() - ตรวจ cause ด้วย
errors.Is/As - เรียก cancel เพื่อคืน resource ทุกเส้นทาง
- sanitize ก่อนแสดงสาเหตุข้าม trust boundary
- ไม่ใช้ cause แทน event history