บทที่ 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