ข้ามไปยังเนื้อหา

Context

context.Context เป็น standard interface ที่ carry:

  • Cancellation signal — goroutine สามารถตรวจสอบ ctx.Done() เพื่อรู้ว่าควรหยุดทำงาน
  • Deadline หรือ timeout ที่เป็น optional — งานถูก cancel อัตโนมัติเมื่อถึงเวลา
  • Request-scoped key/value pairs — metadata ขนาดเล็กเช่น request ID หรือ auth token

Package context แก้ปัญหาที่เป็นรูปธรรม: เมื่อ server request ถูก cancel (user ปิด browser, upstream timeout) goroutines ที่รันสำหรับ request นั้นจะรู้ได้อย่างไรว่าควรหยุด? การส่ง context.Context เป็น argument แรกให้ทุก I/O function คือคำตอบมาตรฐานของ Go

// Root contexts — ไม่มี cancellation
context.Background() // root ระดับบนสุด ใช้ใน main หรือ test setup
context.TODO() // placeholder เมื่อยังไม่แน่ใจจะใช้ context ไหน
// Derived contexts — เพิ่ม cancellation/timeout บน parent
ctx, cancel := context.WithCancel(parent)
ctx, cancel := context.WithTimeout(parent, 5*time.Second)
ctx, cancel := context.WithDeadline(parent, time.Now().Add(5*time.Second))
ctx2 := context.WithValue(parent, key, value)

Derived contexts สร้าง tree การ cancel parent จะ cancel ลูกทั้งหมดอัตโนมัติ

เมื่อเรียก context.WithCancel หรือ context.WithTimeout คุณได้รับสองค่า: derived context และ cancel function คุณ ต้อง เรียก cancel() เมื่องานเสร็จ — แม้ context จะหมดอายุตามธรรมชาติ ถ้าไม่เรียก parent context จะถือ reference ถึง child context จนกว่าจะ cancel parent ด้วย ซึ่งทำให้ leak ทรัพยากร

ใน goroutine ctx.Done() return channel ที่ถูกปิดเมื่อ context ถูก cancel หรือ timeout ใช้ select เพื่อรวมกับงานจริง:

select {
case result := <-workCh:
return result, nil
case <-ctx.Done():
return "", ctx.Err() // context.Canceled หรือ context.DeadlineExceeded
}
package main
import (
"context"
"fmt"
)
func fetchData(ctx context.Context, id int) (string, error) {
select {
case <-ctx.Done():
return "", ctx.Err()
default:
return fmt.Sprintf("data-%d", id), nil
}
}
type ctxKey string
func main() {
// WithCancel: fetch ปกติสำเร็จ
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
result, err := fetchData(ctx, 1)
if err != nil {
fmt.Println("error:", err)
} else {
fmt.Println("got:", result)
}
// Cancel แล้วลองอีกครั้ง — context เป็น done แล้ว
cancel()
result2, err2 := fetchData(ctx, 2)
if err2 != nil {
fmt.Println("cancelled:", err2)
} else {
fmt.Println("got:", result2)
}
// WithValue: แนบ request-scoped metadata
ctx3 := context.WithValue(context.Background(), ctxKey("requestID"), "abc-123")
rid := ctx3.Value(ctxKey("requestID")).(string)
fmt.Println("requestID:", rid)
}
สิ่งที่ได้ประโยชน์ต้นทุน
context.WithCancelpropagate cancellation treeต้อง call cancel() เสมอ ไม่งั้น goroutine leak
context.WithTimeoutdeadline อัตโนมัติต้อง handle context.DeadlineExceeded
context.WithValuepass value ผ่าน call stacktype unsafe, ใช้ unexported key เพื่อหลีกเลี่ยง collision
ctx.Done() channelnon-blocking check via selectต้อง check ในทุก loop iteration ที่อาจรันนาน
  • context.WithValue เหมาะสำหรับ pass business data — ใช้สำหรับ request-scoped metadata เท่านั้น (trace ID, auth token)
  • cancel() ทำให้ goroutine หยุดทันที — cancel() ส่ง signal ผ่าน channel — goroutine ต้องตรวจสอบ ctx.Done() เอง
  • context ส่ง value ได้ทุก type — context.Value() คืน any ต้องใช้ type assertion และอาจ nil

💡 ตัวอย่างจากของจริง

Kubernetes ส่ง context จาก API server → controller → goroutine ทุกตัว เพื่อ graceful shutdown

net/http ทุก HTTP request มี context — client disconnect ยกเลิก context ทำให้ handler cleanup ได้

gRPC propagate deadline ผ่าน context ข้าม service boundary อัตโนมัติ

วิธี idiomatic ที่ถูกต้องในการจัดการ cancel function จาก context.WithCancel คืออะไร?
ctx.Done() return channel อะไร และถูกปิดเมื่อไหร่?
ctx.Err() return อะไรเมื่อ deadline ของ context เกินแล้ว?
ทำไมควรใช้ unexported package-private type เป็น context.WithValue key?