Go Context Package 2026: การยกเลิก, Timeout และคำถามสัมภาษณ์งาน

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

Go Context Package 2026: การยกเลิก, Timeout และคำถามสัมภาษณ์งาน

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 อย่างมีประสิทธิภาพ

context_interface.gogo
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, ว่างเปล่า และไม่เคยถูกยกเลิก

context_creation.gogo
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

cancellation.gogo
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 ที่ยกเลิกอัตโนมัติเมื่อหมดเวลา

timeouts.gogo
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 เพื่อหลีกเลี่ยงการชน

context_values.gogo
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 ถูกยกเลิก

utilities.gogo
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 นี้

go
// 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 ต่อไป

go
// 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

mistakes.gogo
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

เขียนโดย

Anthony Fillion-Maillet

ผู้ก่อตั้ง SharpSkill

เป็นนักพัฒนาฟูลสแตกมากว่า 10 ปี ดูแล SharpSkill และรับผิดชอบทุกสิ่งที่เผยแพร่ที่นี่

อัปเดตเมื่อ 29 สิงหาคม 2569

แชร์

บทความที่เกี่ยวข้อง