บทที่ 14 · Part 4 — APIs and Boundaries

Context, Resources and Cancellation

ส่ง deadline และ cancellation ให้ถึงปลายทาง พร้อมจัดลำดับ cleanup และไม่ซ่อน lifecycle

HTTP client ปิด connection ไปแล้ว แต่ query ยังรันต่อ Worker กำลัง shutdown แต่ goroutine retry ยัง sleep อยู่ หรือ timeout ถูกสร้างใหม่ใน repository จนยาวกว่า deadline ของ request อาการเหล่านี้เกิดจาก เรามอง context.Context เป็น parameter ตาม style แทนที่จะมองเป็น ownership ของเวลาและการยกเลิก

จบบทนี้คุณจะ

  • propagate context จาก entry point ถึง network/DB call โดยไม่ตัดสาย cancellation
  • วาง timeout ที่ boundary ซึ่งรู้ budget และคืน resource ด้วย cancel/defer
  • แยก request-scoped metadata ออกจาก dependency และ business input

Cancellation ต้องต่อเป็นสายเดียว

function ที่ทำ I/O หรือรอนานควรรับ ctx context.Context เป็น parameter แรกและส่งค่าเดิมลงไป อย่าสร้าง context.Background() กลาง request path เพราะตัด cancellation และ trace correlation:

func (s *Service) ByID(ctx context.Context, id string) (Batch, error) {
    return s.store.ByID(ctx, id)
}

func (s *BatchStore) ByID(ctx context.Context, id string) (Batch, error) {
    row := s.db.QueryRowContext(ctx, queryBatch, id)
    return scanBatch(row)
}

context.Background() เหมาะที่ top-level เช่น main/test ส่วน context.TODO() เป็น placeholder ที่มองเห็นได้ว่า design ยังไม่เสร็จ ห้ามส่ง nil context

Deadline Budget อยู่กับ Caller

ผู้ที่รู้ end-to-end budget ควรกำหนด timeout แล้ว downstream ทำให้สั้นลงได้แต่ไม่ควรขยาย deadline เดิมโดยไม่ตั้งใจ:

ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
defer cancel()

batch, err := service.Submit(ctx, request)

เรียก cancel() เสมอแม้ timeout จะหมดเอง เพราะ timer และ child resources ควรถูกคืนเร็ว สำหรับ HTTP client ใช้ http.NewRequestWithContext; สำหรับ database ใช้ QueryContext, ExecContext และ BeginTx แต่ต้องจำว่า driver/upstream ต้องรองรับ cancellation ด้วย Context เป็น signal ไม่ใช่ เวทมนตร์ที่ kill remote operation ทุกระบบ

อย่าวาง timeout เดียวกันทุก call โดยไม่ดู budget ถ้า request มี 2 วินาทีและเรียก upstream 3 ตัวแบบ sequential การให้แต่ละตัว 2 วินาทีไม่ใช่ end-to-end guarantee ควรแบ่ง budget, ทำ parallel เมื่อ semantics อนุญาต หรือ fail ก่อนเมื่อเวลาเหลือน้อย

Context Values ไม่ใช่ Parameter Bag

context value เหมาะกับ metadata ที่เดินทางข้าม process/API boundary เช่น trace context, request ID หรือ authenticated principal ที่ middleware สร้าง ใช้ unexported key type หรือ typed accessor เพื่อกัน collision แต่ business input เช่น batchID, currency หรือ retry count ควรเป็น parameter/struct ปกติ เพราะ compiler และ call site ต้องมองเห็น

ไม่ควรเก็บ context ใน struct ทั่วไป เพราะ lifetime ของ struct กับ request สับสน ส่งเป็น parameter แต่ละ call ข้อยกเว้นมีน้อยและควรอธิบาย contract ตามคำแนะนำทางการเรื่อง Contexts and structs

Resource Cleanup และงานที่อยู่ข้าม Request

หลังเปิด resource สำเร็จให้วาง defer Close() ใกล้จุดเปิด แต่ตรวจ error จาก Close/Flush เมื่อ ความทนทานของ write สำคัญ ใน loop ขนาดใหญ่ไม่ควร defer ทุก iteration ใน function เดียว เพราะ resource จะค้างจน function จบ ให้แยก iteration เป็น helper หรือปิด explicit ตาม scope

context.WithoutCancel ทำให้ values เดินต่อแต่ตัด deadline/cancellation เหมาะเฉพาะงานที่ต้องอยู่ นานกว่า request และมี lifecycle ใหม่ของตัวเอง เช่น enqueue audit record ที่ bounded และ shutdown ได้ ไม่ใช่เครื่องมือแก้ handler ที่เรียก Background() แบบไม่คิด งาน durable ควรส่งเข้า queue/Temporal แทน goroutine ลอยใน process

function synchronous เป็น default ที่ composable เพราะ caller ตัดสินใจเองว่าจะเรียกใน goroutine หรือไม่ API async ต้องบอก buffer, ownership, cancellation และ error channel ให้ครบ ซึ่งเป็นเนื้อหา ของ Goroutine Ownership

Production Toolbox

Default คือ stdlib context และ API แบบ synchronous ใช้ errgroup เมื่อมีงานลูกหลายตัวที่แชร์ cancellation และต้องรวม error ใช้ WithoutCancel เฉพาะเมื่อสร้าง owner/lifetime ใหม่ที่ชัด อย่าใช้ context เป็น DI container หรือเก็บ logger/config ทั้งระบบโดยไม่มี request scope

Checklist ของ Cancellation Path

  • context เริ่มที่ entry point และเดินถึงทุก blocking call
  • ผู้รู้ end-to-end budget เป็นผู้ตั้ง deadline
  • ทุก WithCancel/WithTimeout มี owner เรียก cancel
  • ไม่มี Background() กลาง request path
  • context values มีเฉพาะ request metadata และ key ปลอด collision
  • resource ปิดใน scope ที่เปิดและรายงาน flush/close error เมื่อสำคัญ
  • งานที่ outlive request มี queue, bound, shutdown และ error owner ใหม่

อ่านเพิ่ม: context package, Go Concurrency Patterns: Context และ Go Code Review Comments — contexts