Go Context Package 2026: การยกเลิก, Timeout และคำถามสัมภาษณ์งาน
เชี่ยวชาญ package context ของ Go สำหรับการยกเลิก, timeout และ request-scoped values คู่มือฉบับสมบูรณ์พร้อมตัวอย่างโค้ดและคำถามสัมภาษณ์

Package context ของ Go มอบกลไกมาตรฐานสำหรับการส่งต่อ deadline, สัญญาณยกเลิก และ request-scoped values ข้ามขอบเขต API ทุกแอปพลิเคชัน Go ระดับ production ใช้ context: HTTP handler, การ query database, การเรียก gRPC และ background worker ล้วนพึ่งพามันสำหรับ graceful shutdown และการจัดการ timeout
Context เป็นหนึ่งในหัวข้อที่ถูกถามบ่อยที่สุดในการสัมภาษณ์ Go ผู้สัมภาษณ์คาดหวังให้ผู้สมัครอธิบาย context propagation, แสดงการจัดการการยกเลิกที่ถูกต้อง และหลีกเลี่ยงข้อผิดพลาดทั่วไปเช่นการเก็บ context ใน struct
Interface Context และสี่ Method ของมัน
Interface Context กำหนดสี่ method ที่ทุก implementation ของ context ต้องตอบสนอง การทำความเข้าใจ method เหล่านี้เป็นรากฐานสำหรับการใช้ context อย่างมีประสิทธิภาพ
type Context interface {
// Deadline returns the time when work should be canceled
Deadline() (deadline time.Time, ok bool)
// Done returns a channel that closes when the context is canceled
Done() <-chan struct{}
// Err returns the reason why Done was closed
Err() error
// Value returns the value associated with key, or nil
Value(key any) any
}Done() คืนค่า channel แบบรับอย่างเดียว เมื่อ channel นี้ปิด ทุก goroutine ที่กำลังฟังมันจะได้รับสัญญาณทันที Pattern <-ctx.Done() จะ block จนกว่าจะเกิดการยกเลิก Err() อธิบายเหตุผล: อาจเป็น context.Canceled เมื่อถูกยกเลิกอย่างชัดเจน หรือ context.DeadlineExceeded เมื่อ timeout หรือ deadline ผ่านไปแล้ว
การสร้าง Context ด้วย Background และ TODO
สองฟังก์ชันสร้าง root context: context.Background() และ context.TODO() ทั้งคู่คืนค่า context ที่ไม่เป็น nil, ว่างเปล่า และไม่เคยถูกยกเลิก
package main
import (
"context"
"log"
"net/http"
)
func main() {
// Background: the root context for your application
ctx := context.Background()
// Use it as parent for derived contexts
server := &http.Server{Addr: ":8080"}
// TODO: placeholder when context source is unclear
// Static analysis tools can flag context.TODO() for review
processLegacyData(context.TODO())
}
func processLegacyData(ctx context.Context) {
// Context parameter enables future cancellation support
log.Println("Processing data...")
}Background() ทำหน้าที่เป็น parent สำหรับ incoming request, ฟังก์ชัน main และโค้ดเริ่มต้น TODO() ทำหน้าที่เป็น placeholder ระหว่างการ refactor เมื่อแหล่งที่มาของ context ที่ถูกต้องยังไม่ถูกกำหนด เครื่องมือวิเคราะห์แบบ static สามารถทำเครื่องหมายการใช้ TODO() เพื่อตรวจสอบในภายหลัง
การยกเลิกด้วย WithCancel และ WithCancelCause
การยกเลิกแบบ manual ช่วยให้ goroutine แม่ส่งสัญญาณไปยัง goroutine ลูกว่างานควรหยุด Pattern นี้ปรากฏใน worker pool, background task และการ implement graceful shutdown
package main
import (
"context"
"errors"
"fmt"
"time"
)
func worker(ctx context.Context, id int, results chan<- int) {
for {
select {
case <-ctx.Done():
// Check why cancellation occurred
if cause := context.Cause(ctx); cause != nil {
fmt.Printf("Worker %d stopped: %v\n", id, cause)
}
return
default:
// Simulate work
time.Sleep(100 * time.Millisecond)
results <- id * 10
}
}
}
func main() {
// WithCancelCause provides error context
ctx, cancel := context.WithCancelCause(context.Background())
results := make(chan int, 10)
// Start 3 workers
for i := 1; i <= 3; i++ {
go worker(ctx, i, results)
}
// Collect some results
for i := 0; i < 5; i++ {
fmt.Println("Result:", <-results)
}
// Cancel with a specific reason
cancel(errors.New("shutdown requested by user"))
// context.Cause retrieves the cancellation reason
time.Sleep(50 * time.Millisecond)
fmt.Println("Cause:", context.Cause(ctx))
}WithCancelCause ซึ่งเปิดตัวใน Go 1.20 ให้ข้อมูล error ที่ละเอียดกว่า WithCancel ธรรมดา ฟังก์ชัน context.Cause() ดึง error ที่ส่งไปยังฟังก์ชัน cancel ทำให้การ debug ง่ายขึ้นในระบบที่ซับซ้อน
พร้อมที่จะพิชิตการสัมภาษณ์ Go แล้วหรือยังครับ?
ฝึกฝนด้วยตัวจำลองแบบโต้ตอบ, flashcards และแบบทดสอบเทคนิคครับ
Timeout และ Deadline สำหรับการจัดการ Request
HTTP handler, การเรียก database และ request API ภายนอกควรมี timeout เสมอ Package context มี WithTimeout และ WithDeadline เพื่อจุดประสงค์นี้ ทั้งคู่สร้าง context ที่ยกเลิกอัตโนมัติเมื่อหมดเวลา
package main
import (
"context"
"fmt"
"time"
)
// fetchFromAPI simulates an HTTP call
func fetchFromAPI(ctx context.Context, endpoint string) (string, error) {
// Simulate variable response time
responseTime := time.Duration(100+endpoint[0]%150) * time.Millisecond
select {
case <-time.After(responseTime):
return fmt.Sprintf("Response from %s", endpoint), nil
case <-ctx.Done():
return "", ctx.Err()
}
}
func main() {
// WithTimeout: relative duration
ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
defer cancel() // Always call cancel to release resources
result, err := fetchFromAPI(ctx, "api.example.com/users")
if err != nil {
if err == context.DeadlineExceeded {
fmt.Println("Request timed out")
} else {
fmt.Println("Request canceled")
}
return
}
fmt.Println(result)
// WithDeadline: absolute time
deadline := time.Now().Add(500 * time.Millisecond)
ctx2, cancel2 := context.WithDeadline(context.Background(), deadline)
defer cancel2()
result2, _ := fetchFromAPI(ctx2, "api.example.com/orders")
fmt.Println(result2)
}Pattern defer cancel() รับประกันว่า resource จะถูกปล่อยแม้เมื่อ operation เสร็จก่อน timeout การข้าม call นี้ทำให้เกิด resource leak: goroutine timer ของ context ยังคงทำงานจนกว่า timeout จะหมดอายุ
Request-Scoped Values ด้วย WithValue
WithValue แนบข้อมูล request-scoped เข้ากับ context Use case ทั่วไปรวมถึง request ID, token การยืนยันตัวตน และ tracing span เอกสารอย่างเป็นทางการ เน้นการใช้ type ที่กำหนดเองเป็น key เพื่อหลีกเลี่ยงการชน
package main
import (
"context"
"fmt"
"log"
"net/http"
"time"
)
// Define custom key types to avoid collisions
type contextKey string
const (
requestIDKey contextKey = "requestID"
userIDKey contextKey = "userID"
)
// middleware adds request ID to context
func requestIDMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requestID := r.Header.Get("X-Request-ID")
if requestID == "" {
requestID = generateRequestID()
}
// Create new context with request ID
ctx := context.WithValue(r.Context(), requestIDKey, requestID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
// handler retrieves values from context
func handler(w http.ResponseWriter, r *http.Request) {
requestID, ok := r.Context().Value(requestIDKey).(string)
if !ok {
requestID = "unknown"
}
log.Printf("[%s] Processing request", requestID)
fmt.Fprintf(w, "Request ID: %s", requestID)
}
func generateRequestID() string {
return fmt.Sprintf("req-%d", time.Now().UnixNano())
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", handler)
http.ListenAndServe(":8080", requestIDMiddleware(mux))
}Context value สร้างห่วงโซ่ที่ไม่เปลี่ยนแปลง: การเรียก WithValue แต่ละครั้งสร้าง context ใหม่ที่ห่อหุ้ม parent การค้นหาจะเดินทางผ่านห่วงโซ่นี้ ทำให้ value ที่ซ้อนลึกเข้าถึงช้าลง เก็บเฉพาะข้อมูล request-scoped ไม่ใช่การกำหนดค่าแอปพลิเคชันหรือพารามิเตอร์ทางเลือก
Utility WithoutCancel และ AfterFunc
Go 1.21 เพิ่ม WithoutCancel สำหรับ operation ที่ต้องเสร็จสมบูรณ์โดยไม่คำนึงถึงการยกเลิก parent เช่น cleanup task AfterFunc กำหนดเวลา callback เมื่อ context ถูกยกเลิก
package main
import (
"context"
"fmt"
"log"
"time"
)
func saveAuditLog(ctx context.Context, message string) error {
// Use WithoutCancel: audit logs must complete even if request canceled
cleanCtx := context.WithoutCancel(ctx)
// Simulate database write
select {
case <-time.After(50 * time.Millisecond):
log.Printf("Audit: %s", message)
return nil
case <-cleanCtx.Done():
// This branch never executes: WithoutCancel context is never canceled
return cleanCtx.Err()
}
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
// AfterFunc: register cleanup when context ends
stop := context.AfterFunc(ctx, func() {
fmt.Println("Context ended, running cleanup")
})
// Perform operation
time.Sleep(50 * time.Millisecond)
// Cancel stops the AfterFunc from running if called before context ends
if stop() {
fmt.Println("Cleanup prevented")
}
cancel()
// Audit log completes despite parent cancellation
saveAuditLog(ctx, "Operation completed")
}WithoutCancel รักษา value จาก context parent แต่ละเลยสัญญาณยกเลิกของมัน Pattern นี้เหมาะสำหรับ logging, การส่ง metric และ database commit ที่ต้องสำเร็จโดยไม่คำนึงถึงผลลัพธ์ของ request
คำถามและคำตอบสัมภาษณ์
การเตรียมตัวสำหรับการสัมภาษณ์เทคนิค Go ต้องการความเข้าใจเกี่ยวกับภายใน context และ best practice คำถามเหล่านี้ปรากฏบ่อยในการสัมภาษณ์ที่บริษัทที่ใช้ Go สำหรับบริการ backend สำหรับการเตรียมสัมภาษณ์ Go เพิ่มเติม ดูโมดูล คำถามสัมภาษณ์ Context Package
ทำไม context ควรเป็นพารามิเตอร์แรก?
ทีม Go กำหนดข้อตกลงนี้เพื่อทำให้ context propagation ชัดเจนและสอดคล้อง การวาง context ไว้ตำแหน่งแรกส่งสัญญาณให้ผู้อ่านรู้ว่าฟังก์ชันเคารพการยกเลิก Library มาตรฐานปฏิบัติตาม pattern นี้: http.Request.Context(), database/sql.QueryContext() และ grpc.UnaryInterceptor ล้วนคาดหวัง context เป็นอาร์กิวเมนต์แรก
เกิดอะไรขึ้นถ้าเก็บ context ใน struct?
การเก็บ context ละเมิดโมเดล lifecycle ของ request struct อาจมีอายุยืนกว่า request ที่สร้างมัน ทำให้ operation ใช้สัญญาณยกเลิกที่ล้าสมัยหรือพลาด deadline ใหม่ เอกสาร context เตือนอย่างชัดเจนต่อ pattern นี้
// BAD: context stored in struct
type Service struct {
ctx context.Context // Never do this
}
// GOOD: pass context to each method
type Service struct{}
func (s *Service) Process(ctx context.Context, data []byte) error {
// Context flows through the call chain
return s.save(ctx, data)
}จะจัดการ context ใน goroutine อย่างไร?
Goroutine ต้องตรวจสอบ ctx.Done() ในลูปหลักหรือ statement select ของมัน การละเลย channel done สร้างการรั่วไหลของ goroutine: ฟังก์ชัน parent return แต่ goroutine ยังคงใช้ resource ต่อไป
// Correct pattern for long-running goroutines
func processStream(ctx context.Context, stream <-chan Data) {
for {
select {
case <-ctx.Done():
log.Println("Shutting down processor")
return
case data, ok := <-stream:
if !ok {
return
}
handle(data)
}
}
}เมื่อไหร่ควรใช้ context.TODO()?
ใช้ TODO() ระหว่างการ refactoring ทีละขั้นเมื่อเพิ่มการสนับสนุน context ให้กับโค้ด legacy มันทำเครื่องหมายตำแหน่งที่ต้องการ context propagation ที่เหมาะสม โค้ด production ในที่สุดควรแทนที่การเรียก TODO() ทั้งหมดด้วย context จริงจาก request handler หรือการเริ่มต้นแอปพลิเคชัน
ข้อผิดพลาดทั่วไปที่ควรหลีกเลี่ยง
การใช้ context ผิดทำให้เกิด bug ที่ละเอียดอ่อนซึ่งปรากฏขึ้นภายใต้ภาระหนักหรือระหว่างลำดับ shutdown การจดจำ pattern เหล่านี้ป้องกันเหตุการณ์ใน production
package main
import (
"context"
"time"
)
// MISTAKE 1: Ignoring context cancellation
func badWorker(ctx context.Context) {
for {
// Missing select on ctx.Done()
doExpensiveWork() // Never stops when context canceled
}
}
// MISTAKE 2: Not calling cancel
func leakyTimeout() {
ctx, _ := context.WithTimeout(context.Background(), time.Second)
// Timer goroutine leaks until timeout expires
_ = ctx
}
// MISTAKE 3: Using string keys for values
func collisionProne(ctx context.Context) context.Context {
// Different packages might use same string key
return context.WithValue(ctx, "userID", 123) // Bad
}
// CORRECT versions
func goodWorker(ctx context.Context) {
for {
select {
case <-ctx.Done():
return
default:
doExpensiveWork()
}
}
}
func properTimeout() {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel() // Always release resources
_ = ctx
}
type userIDKey struct{}
func collisionSafe(ctx context.Context) context.Context {
return context.WithValue(ctx, userIDKey{}, 123) // Good
}
func doExpensiveWork() {}เริ่มฝึกซ้อมเลย!
ทดสอบความรู้ของคุณด้วยตัวจำลองสัมภาษณ์และแบบทดสอบเทคนิคครับ
ประเด็นสำคัญสำหรับการใช้ Go Context ใน Production
- ส่ง context เป็นพารามิเตอร์แรกไปยังฟังก์ชัน อย่าเก็บมันใน struct
- defer
cancel()เสมอหลังจากWithTimeout,WithDeadlineหรือWithCancelเพื่อป้องกัน resource leak - ใช้
WithCancelCauseและcontext.Cause()เพื่อการ debug error ที่ดีขึ้นในห่วงโซ่การยกเลิกที่ซับซ้อน - ตรวจสอบ
<-ctx.Done()ในลูป goroutine เพื่อเปิดใช้งาน graceful shutdown และป้องกันการรั่วไหล - ใช้ type key ที่กำหนดเองสำหรับ
WithValueเพื่อหลีกเลี่ยงการชนระหว่าง package - ใช้
WithoutCancelสำหรับ operation cleanup ที่ต้องเสร็จสมบูรณ์โดยไม่คำนึงถึงการยกเลิก parent - เลือก
context.Background()สำหรับการเริ่มต้นแอปพลิเคชัน และcontext.TODO()เฉพาะระหว่างการ refactoring
คุณหาบั๊กใน Go เจอไหม
โค้ดจริงหนึ่งชิ้น บั๊กที่ซ่อนอยู่หนึ่งจุด วันละหนึ่งครั้ง ลองได้โดยไม่ต้องมีบัญชี

เขียนโดย
Anthony Fillion-Mailletผู้ก่อตั้ง SharpSkill
เป็นนักพัฒนาฟูลสแตกมากว่า 10 ปี ดูแล SharpSkill และรับผิดชอบทุกสิ่งที่เผยแพร่ที่นี่
อัปเดตเมื่อ 29 สิงหาคม 2569
แชร์
บทความที่เกี่ยวข้อง

Profiling และ Benchmarking Go 2026: pprof, trace และคำถามสัมภาษณ์
คู่มือ profiling Go ด้วย pprof และ runtime/trace อย่างครอบคลุม เทคนิควิเคราะห์ CPU, หน่วยความจำ และ goroutine เพื่อเพิ่มประสิทธิภาพและเตรียมตัวสัมภาษณ์

Interface Go ขั้นสูงในปี 2026: การประกอบ Type Assertion และคำถามสัมภาษณ์
คู่มือเชิงลึกเกี่ยวกับ interface Go สมัยใหม่: การประกอบ interface, type assertion ที่ปลอดภัย, generic แบบอ้างอิงตนเอง และคำถามสัมภาษณ์ที่พบบ่อยสำหรับนักพัฒนา Go

Go Testing ในปี 2026: Unit Test, Mock และคำถามสัมภาษณ์ทางเทคนิค
เชี่ยวชาญ Go testing ด้วย table-driven test, mock และ pattern ที่ผู้สัมภาษณ์คาดหวัง ตัวอย่างการใช้งานจริงกับ testify และ gomock