กระรอกดินสีฟ้าอมเขียวต้นฉบับกำลังหยุดสายพานและผูกป้ายสาเหตุเชิงภาพไว้ที่คันหยุด

Go context.WithCancelCause: บอกเหตุผลที่งานถูกยกเลิก

พี่โชว์Engineering

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