Context #

Package context adalah mekanisme fundamental Go untuk mengelola lifecycle sebuah operasi — membatalkannya saat tidak lagi diperlukan, memberikan batas waktu agar tidak berjalan selamanya, dan meneruskan nilai-nilai seperti request ID atau informasi pengguna melintasi batas fungsi dan goroutine. Setiap kali kamu memanggil database, membuat HTTP request ke layanan lain, atau menjalankan operasi yang memakan waktu, selalu ada pertanyaan: “Apa yang terjadi jika pengguna membatalkan request di tengah jalan?” atau “Apa yang terjadi jika operasi ini tidak selesai dalam 5 detik?” — jawabannya adalah context. Memahami context dengan baik adalah prasyarat untuk menulis kode Go yang robust, karena hampir semua library dan package standard Go yang melakukan I/O menerima context.Context sebagai parameter pertama.

Gambaran Besar Package context #

flowchart TD
    Root["context.Background()\natau context.TODO()"] --> WC["context.WithCancel(parent)"]
    Root --> WT["context.WithTimeout(parent, duration)"]
    Root --> WD["context.WithDeadline(parent, time)"]
    Root --> WV["context.WithValue(parent, key, val)"]

    WC --> C1["ctx — context turunan"]
    WC --> C2["cancel() — fungsi pembatal"]

    WT --> T1["ctx — auto-cancel setelah duration"]
    WT --> T2["cancel() — cancel lebih awal"]

    WD --> D1["ctx — auto-cancel saat deadline"]
    WD --> D2["cancel() — cancel lebih awal"]

    WV --> V1["ctx — membawa nilai key-val"]

    C1 --> Done["ctx.Done() <-chan struct{}\nsignal cancellation"]
    T1 --> Done
    D1 --> Done

    Done --> Err["ctx.Err()\ncontext.Canceled\natau context.DeadlineExceeded"]

    V1 --> Val["ctx.Value(key)\nmengambil nilai"]

    style Root fill:#4f86c6,color:#fff
    style WC fill:#e8f5e9
    style WT fill:#e3f2fd
    style WD fill:#fff3e0
    style WV fill:#f3e5f5

Empat Fungsi Dasar #

Context di Go selalu dimulai dari root (Background atau TODO) dan diperluas dengan With* functions yang membuat context baru sebagai turunan dari parent.

package main

import (
    "context"
    "fmt"
)

func main() {
    // context.Background — root context, tidak pernah di-cancel
    // Gunakan sebagai titik awal di main(), test, dan inisialisasi
    ctx := context.Background()

    // context.TODO — placeholder saat belum tahu context yang tepat
    // Gunakan saat refactoring kode lama yang belum pakai context
    ctx2 := context.TODO()

    _ = ctx
    _ = ctx2

    // Periksa apakah context sudah di-cancel
    select {
    case <-ctx.Done():
        fmt.Println("context di-cancel:", ctx.Err())
    default:
        fmt.Println("context masih aktif")
    }
}

WithCancel — Pembatalan Manual #

context.WithCancel membuat context baru yang bisa dibatalkan secara manual dengan memanggil fungsi cancel. Saat cancel dipanggil, ctx.Done() channel ditutup dan semua operasi yang mendengarkan channel ini bisa berhenti.

import (
    "context"
    "fmt"
    "time"
)

func operasiLama(ctx context.Context, nama string) error {
    select {
    case <-time.After(3 * time.Second):
        fmt.Printf("%s selesai\n", nama)
        return nil
    case <-ctx.Done():
        fmt.Printf("%s dibatalkan: %v\n", nama, ctx.Err())
        return ctx.Err()
    }
}

func main() {
    ctx, cancel := context.WithCancel(context.Background())

    // Jalankan beberapa goroutine yang menggunakan context yang sama
    go operasiLama(ctx, "goroutine-1")
    go operasiLama(ctx, "goroutine-2")
    go operasiLama(ctx, "goroutine-3")

    // Batalkan semua setelah 1 detik
    time.Sleep(time.Second)
    cancel() // satu panggilan membatalkan semua goroutine yang mendengarkan ctx

    time.Sleep(100 * time.Millisecond) // tunggu goroutine selesai bersih
    fmt.Println("semua goroutine selesai")
}
// Output:
// goroutine-1 dibatalkan: context canceled
// goroutine-2 dibatalkan: context canceled
// goroutine-3 dibatalkan: context canceled
// semua goroutine selesai

Propagasi Cancellation #

Saat parent context di-cancel, semua child context ikut di-cancel secara otomatis:

flowchart TD
    P["Parent Context\ncancel()"] --> C1["Child 1\nWithCancel"]
    P --> C2["Child 2\nWithTimeout(5s)"]
    C1 --> GC1["Grandchild 1\nWithValue"]
    C2 --> GC2["Grandchild 2\nWithCancel"]

    Cancel["cancel() dipanggil\npada Parent"] --> P

    P -- "otomatis cancel" --> C1
    P -- "otomatis cancel" --> C2
    C1 -- "otomatis cancel" --> GC1
    C2 -- "otomatis cancel" --> GC2

    style Cancel fill:#fce4ec
    style P fill:#ffcdd2
    style C1 fill:#ffcdd2
    style C2 fill:#ffcdd2
    style GC1 fill:#ffcdd2
    style GC2 fill:#ffcdd2
func main() {
    parent, cancelParent := context.WithCancel(context.Background())
    defer cancelParent()

    // Child mewarisi cancellation dari parent
    child, cancelChild := context.WithCancel(parent)
    defer cancelChild()

    // Grandchild mewarisi dari child
    grandchild, cancelGrandchild := context.WithTimeout(child, 10*time.Second)
    defer cancelGrandchild()

    // Cancel parent — child dan grandchild ikut di-cancel
    cancelParent()

    fmt.Println(parent.Err())      // context canceled
    fmt.Println(child.Err())       // context canceled
    fmt.Println(grandchild.Err())  // context canceled

    // Sebaliknya: cancel child TIDAK mempengaruhi parent
    cancelChild()
    fmt.Println(parent.Err()) // nil — parent masih aktif
}
Selalu panggil cancel() yang dikembalikan oleh WithCancel, WithTimeout, dan WithDeadline — biasanya dengan defer cancel(). Jika tidak dipanggil, context dan semua resource yang terkait (timer internal, channel) tidak akan dibebaskan sampai parent context di-cancel. Ini adalah sumber goroutine leak dan memory leak yang paling umum terkait context.

WithTimeout — Batas Waktu Relatif #

context.WithTimeout membuat context yang otomatis di-cancel setelah durasi tertentu dari sekarang. Ini adalah yang paling sering digunakan untuk operasi dengan batas waktu.

import (
    "context"
    "database/sql"
    "fmt"
    "time"
)

func queryDenganTimeout(db *sql.DB, id int) (*Pengguna, error) {
    // Context ini otomatis di-cancel setelah 5 detik
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel() // WAJIB: bebaskan resource timer jika selesai lebih awal

    var p Pengguna
    err := db.QueryRowContext(ctx,
        "SELECT id, nama, email FROM pengguna WHERE id = $1", id,
    ).Scan(&p.ID, &p.Nama, &p.Email)

    if err != nil {
        if err == context.DeadlineExceeded {
            return nil, fmt.Errorf("query timeout setelah 5 detik")
        }
        return nil, fmt.Errorf("queryDenganTimeout: %w", err)
    }

    return &p, nil
}

Meneruskan Context dari Caller #

Context seharusnya mengalir dari atas ke bawah — dari handler ke service ke repository, bukan dibuat baru di setiap layer:

sequenceDiagram
    participant Client as HTTP Client
    participant Handler as HTTP Handler
    participant Service as Service Layer
    participant Repo as Repository
    participant DB as Database

    Client->>Handler: HTTP Request
    Handler->>Handler: ctx = r.Context()\n(sudah ada timeout dari server)
    Handler->>Service: ProsesData(ctx, input)
    Service->>Service: ctx, cancel = WithTimeout(ctx, 3s)\n(tambahkan timeout lebih ketat)
    Service->>Repo: CariData(ctx, id)
    Repo->>DB: QueryContext(ctx, query)
    DB-->>Repo: hasil / timeout
    Repo-->>Service: data / error
    Service->>Service: cancel()
    Service-->>Handler: hasil / error
    Handler-->>Client: HTTP Response

    Note over Handler,DB: Jika client disconnect, r.Context() di-cancel\notomatis membatalkan query DB
// Handler — context berasal dari request
func handlerProsesPesanan(w http.ResponseWriter, r *http.Request) {
    // r.Context() otomatis di-cancel saat:
    // 1. Client memutus koneksi
    // 2. Server timeout (jika dikonfigurasi)
    ctx := r.Context()

    hasil, err := serviceProseskan(ctx, pesanan)
    if err != nil {
        if errors.Is(err, context.Canceled) {
            // Client sudah disconnect — tidak perlu kirim response
            return
        }
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    json.NewEncoder(w).Encode(hasil)
}

// Service — terima context dari caller, bisa tambahkan timeout lebih ketat
func serviceProseskan(ctx context.Context, pesanan Pesanan) (*Hasil, error) {
    // Tambahkan timeout lebih ketat untuk operasi spesifik
    ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
    defer cancel()

    // Teruskan context ke semua operasi downstream
    if err := repoCekStok(ctx, pesanan.ProdukID); err != nil {
        return nil, fmt.Errorf("serviceProseskan: cek stok: %w", err)
    }

    if err := repoBuatPesanan(ctx, pesanan); err != nil {
        return nil, fmt.Errorf("serviceProseskan: buat pesanan: %w", err)
    }

    return &Hasil{Status: "berhasil"}, nil
}

// Repository — terima dan gunakan context untuk semua I/O
func repoCekStok(ctx context.Context, produkID int) error {
    var stok int
    err := db.QueryRowContext(ctx,
        "SELECT stok FROM produk WHERE id = $1", produkID,
    ).Scan(&stok)
    if err != nil {
        return fmt.Errorf("repoCekStok: %w", err)
    }
    if stok == 0 {
        return ErrStokKosong
    }
    return nil
}

WithDeadline — Batas Waktu Absolut #

context.WithDeadline mirip dengan WithTimeout, tapi menggunakan waktu absolut bukan durasi relatif. Berguna saat kamu punya deadline tetap yang harus dipenuhi.

import (
    "context"
    "fmt"
    "time"
)

func prosesSebelumTengahMalam(ctx context.Context) error {
    // Deadline: tengah malam hari ini
    sekarang := time.Now()
    tengahMalam := time.Date(
        sekarang.Year(), sekarang.Month(), sekarang.Day()+1,
        0, 0, 0, 0, sekarang.Location(),
    )

    ctx, cancel := context.WithDeadline(ctx, tengahMalam)
    defer cancel()

    fmt.Printf("Proses harus selesai sebelum %v\n",
        tengahMalam.Format("15:04:05"))
    fmt.Printf("Sisa waktu: %v\n", time.Until(tengahMalam).Round(time.Second))

    // Periksa berapa sisa waktu
    deadline, ada := ctx.Deadline()
    if ada {
        fmt.Println("Deadline:", deadline)
        fmt.Println("Sisa:", time.Until(deadline).Round(time.Second))
    }

    // Jalankan proses...
    select {
    case <-time.After(2 * time.Hour): // proses butuh 2 jam
        return nil
    case <-ctx.Done():
        return fmt.Errorf("proses dibatalkan: %w", ctx.Err())
    }
}

// WithDeadline vs WithTimeout
func perbandingan() {
    // Keduanya setara:
    ctx1, cancel1 := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel1()

    ctx2, cancel2 := context.WithDeadline(context.Background(),
        time.Now().Add(5*time.Second))
    defer cancel2()

    // ctx1 dan ctx2 berperilaku identik
    _ = ctx1
    _ = ctx2
}

WithValue — Meneruskan Nilai #

context.WithValue menyimpan pasangan key-value dalam context. Nilai ini bisa diambil kembali di fungsi mana saja yang menerima context tersebut — berguna untuk request ID, informasi pengguna, dan data yang perlu melintas banyak layer tanpa dioper sebagai parameter eksplisit.

// PENTING: selalu gunakan tipe kustom untuk key, JANGAN string biasa
// Ini mencegah collision dengan package lain yang mungkin menggunakan key yang sama

type contextKey string

const (
    KeyRequestID  contextKey = "request_id"
    KeyPengguna   contextKey = "pengguna"
    KeyTraceID    contextKey = "trace_id"
)

// Middleware: simpan request ID di context
func middlewareRequestID(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // Ambil dari header atau buat baru
        requestID := r.Header.Get("X-Request-ID")
        if requestID == "" {
            requestID = generateRequestID()
        }

        // Simpan di context
        ctx := context.WithValue(r.Context(), KeyRequestID, requestID)

        // Sertakan di response header untuk tracing
        w.Header().Set("X-Request-ID", requestID)

        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

// Helper: ambil request ID dari context
func ambilRequestID(ctx context.Context) string {
    if id, ok := ctx.Value(KeyRequestID).(string); ok {
        return id
    }
    return "unknown"
}

// Penggunaan di handler atau service
func handlerBuatProduk(w http.ResponseWriter, r *http.Request) {
    requestID := ambilRequestID(r.Context())
    log.Printf("[%s] memproses request buat produk", requestID)

    // Teruskan context ke layer bawah — request ID ikut terbawa
    if err := serviceBuatProduk(r.Context(), input); err != nil {
        log.Printf("[%s] gagal buat produk: %v", requestID, err)
        http.Error(w, "gagal", http.StatusInternalServerError)
        return
    }
}

func serviceBuatProduk(ctx context.Context, input InputProduk) error {
    requestID := ambilRequestID(ctx) // masih bisa diakses!
    log.Printf("[%s] service: validasi input", requestID)
    // ...
    return nil
}

Apa yang Boleh dan Tidak Boleh di WithValue #

flowchart LR
    subgraph Boleh["✓ Boleh disimpan di context"]
        B1["Request ID / Trace ID\nuntuk logging dan tracing"]
        B2["Info pengguna terautentikasi\ndari middleware auth"]
        B3["Bahasa / Locale\nuntuk internationalisasi"]
        B4["Database transaction\nuntuk atomic operations"]
    end

    subgraph Jangan["✗ Jangan disimpan di context"]
        J1["Parameter fungsi yang opsional\ngunakan parameter eksplisit"]
        J2["Konfigurasi global\ngunakan dependency injection"]
        J3["Data besar\ngunakan struct atau parameter"]
        J4["Error\nkembalikan sebagai return value"]
    end

    style Boleh fill:#e8f5e9
    style Jangan fill:#fce4ec
// ANTI-PATTERN: gunakan context untuk oper parameter fungsi
func prosesData(ctx context.Context) error {
    // Mengambil "limit" dari context — ini membuat fungsi tidak jelas
    // Pemanggil tidak tahu fungsi ini butuh "limit"
    limit, _ := ctx.Value("limit").(int)
    _ = limit
    return nil
}

// BENAR: parameter eksplisit lebih jelas dan mudah di-test
func prosesDataBaik(ctx context.Context, limit int) error {
    _ = limit
    return nil
}

// ANTI-PATTERN: gunakan string biasa sebagai key
ctx := context.WithValue(ctx, "user_id", 42)
// Package lain bisa menggunakan key "user_id" yang sama → collision!

// BENAR: tipe kustom sebagai key
type appContextKey string
ctx = context.WithValue(ctx, appContextKey("user_id"), 42)

ctx.Done() — Mendengarkan Cancellation #

ctx.Done() mengembalikan channel yang ditutup saat context di-cancel. Ini adalah cara utama untuk merespons cancellation dalam goroutine yang berjalan lama.

// Pola: goroutine yang bisa di-cancel
func pekerja(ctx context.Context, id int, pekerjaan <-chan string) {
    for {
        select {
        case <-ctx.Done():
            fmt.Printf("pekerja %d berhenti: %v\n", id, ctx.Err())
            return

        case pek, ok := <-pekerjaan:
            if !ok {
                fmt.Printf("pekerja %d: channel ditutup\n", id)
                return
            }
            proseskanPekerjaan(ctx, pek)
        }
    }
}

// Pola: operasi periodik yang bisa di-cancel
func pollingPeriodik(ctx context.Context, interval time.Duration) {
    ticker := time.NewTicker(interval)
    defer ticker.Stop()

    for {
        select {
        case <-ctx.Done():
            fmt.Println("polling berhenti:", ctx.Err())
            return

        case <-ticker.C:
            if err := periksaStatus(ctx); err != nil {
                if errors.Is(err, context.Canceled) ||
                    errors.Is(err, context.DeadlineExceeded) {
                    return
                }
                fmt.Println("error saat polling:", err)
            }
        }
    }
}

// Pola: menunggu beberapa goroutine dengan context
func jalankanParalel(ctx context.Context, tugas []Tugas) error {
    ctx, cancel := context.WithCancel(ctx)
    defer cancel()

    errChan := make(chan error, len(tugas))

    for _, t := range tugas {
        go func(tugas Tugas) {
            errChan <- tugas.Jalankan(ctx)
        }(t)
    }

    // Tunggu semua selesai atau salah satu gagal
    for range tugas {
        if err := <-errChan; err != nil {
            cancel() // batalkan semua goroutine lain
            return err
        }
    }
    return nil
}

Memeriksa Jenis Cancellation #

Saat context di-cancel, ctx.Err() mengembalikan salah satu dari dua error: context.Canceled (dibatalkan manual) atau context.DeadlineExceeded (timeout). Membedakan keduanya penting untuk logging dan error handling yang tepat.

flowchart TD
    Cancel["ctx.Err()"] --> CE["context.Canceled\n(cancel() dipanggil manual)"]
    Cancel --> DE["context.DeadlineExceeded\n(timeout atau deadline tercapai)"]

    CE --> CEAction["Log sebagai INFO\nbiasanya karena client disconnect\natau operasi sengaja dibatalkan"]
    DE --> DEAction["Log sebagai WARNING\noperasi terlalu lambat\npertimbangkan tambah timeout"]

    style CE fill:#e3f2fd
    style DE fill:#fff3e0
    style CEAction fill:#e3f2fd
    style DEAction fill:#fff3e0
func tanganiErrorContext(err error) {
    switch {
    case err == nil:
        return

    case errors.Is(err, context.Canceled):
        // Dibatalkan secara manual — biasanya karena client disconnect
        // Log sebagai info, bukan error
        log.Printf("operasi dibatalkan (client mungkin disconnect)")

    case errors.Is(err, context.DeadlineExceeded):
        // Timeout — operasi terlalu lambat
        // Log sebagai warning, mungkin perlu investigasi
        log.Printf("operasi timeout — pertimbangkan tingkatkan timeout atau optimasi query")

    default:
        // Error lain
        log.Printf("error operasi: %v", err)
    }
}

// Dalam handler HTTP — bedakan cancel dari error sebenarnya
func handlerLong(w http.ResponseWriter, r *http.Request) {
    hasil, err := operasiPanjang(r.Context())
    if err != nil {
        if errors.Is(err, context.Canceled) {
            // Client disconnect — tidak perlu kirim response
            log.Printf("client disconnect untuk %s", r.URL.Path)
            return
        }
        if errors.Is(err, context.DeadlineExceeded) {
            http.Error(w, "request timeout", http.StatusGatewayTimeout)
            return
        }
        http.Error(w, "error internal", http.StatusInternalServerError)
        return
    }
    json.NewEncoder(w).Encode(hasil)
}

context.WithoutCancel (Go 1.21+) #

Sejak Go 1.21, ada context.WithoutCancel yang membuat context baru yang meneruskan nilai dari parent tapi tidak mewarisi cancellation-nya. Berguna untuk operasi cleanup yang harus tetap berjalan meskipun request utama sudah di-cancel.

func handlerBuatPesanan(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()

    // Buat pesanan — bisa dibatalkan jika client disconnect
    pesanan, err := serviceBuatPesanan(ctx, input)
    if err != nil {
        if errors.Is(err, context.Canceled) {
            return // client disconnect sebelum pesanan dibuat
        }
        http.Error(w, "gagal", 500)
        return
    }

    // Kirim notifikasi — HARUS selesai meski client disconnect
    // Gunakan context tanpa cancellation dari request
    ctxNotif := context.WithoutCancel(ctx) // Go 1.21+
    go func() {
        if err := kirimNotifikasi(ctxNotif, pesanan); err != nil {
            log.Printf("gagal kirim notifikasi pesanan %d: %v", pesanan.ID, err)
        }
    }()

    json.NewEncoder(w).Encode(pesanan)
}

Pola Penggunaan di Produksi #

Context untuk Database Transaction #

type contextKey string

const keyDBTx contextKey = "db_tx"

// Simpan transaction di context untuk atomic operation lintas fungsi
func denganTransaction(ctx context.Context, db *sql.DB,
    fn func(context.Context) error) error {

    tx, err := db.BeginTx(ctx, nil)
    if err != nil {
        return fmt.Errorf("mulai transaksi: %w", err)
    }

    // Simpan tx di context
    ctx = context.WithValue(ctx, keyDBTx, tx)

    if err := fn(ctx); err != nil {
        // Rollback jika fungsi gagal
        if rbErr := tx.Rollback(); rbErr != nil {
            return fmt.Errorf("rollback gagal: %v (error asli: %w)", rbErr, err)
        }
        return err
    }

    return tx.Commit()
}

// Ambil tx dari context atau gunakan db biasa
func ambilDB(ctx context.Context, db *sql.DB) interface {
    QueryRowContext(context.Context, string, ...any) *sql.Row
    ExecContext(context.Context, string, ...any) (sql.Result, error)
} {
    if tx, ok := ctx.Value(keyDBTx).(*sql.Tx); ok {
        return tx
    }
    return db
}

// Penggunaan
func servicePindahkanStok(ctx context.Context, dari, ke, produkID, jumlah int) error {
    return denganTransaction(ctx, db, func(ctx context.Context) error {
        // Kedua operasi dalam satu transaction
        if err := repoPenguranganStok(ctx, dari, produkID, jumlah); err != nil {
            return err
        }
        if err := repoPenambahanStok(ctx, ke, produkID, jumlah); err != nil {
            return err
        }
        return nil
    })
}

Context dengan Timeout Bertingkat #

// Setiap layer bisa menambahkan timeout yang lebih ketat dari parent
func handlerAPI(w http.ResponseWriter, r *http.Request) {
    // Server sudah punya timeout dari http.Server.WriteTimeout (misal 30 detik)
    ctx := r.Context()

    // Handler menambahkan batas total request: 20 detik
    ctx, cancel := context.WithTimeout(ctx, 20*time.Second)
    defer cancel()

    hasil, err := serviceProses(ctx, r)
    // ...
    _ = hasil
    _ = err
}

func serviceProses(ctx context.Context, r *http.Request) (*Hasil, error) {
    // Service menambahkan timeout lebih ketat untuk operasi DB: 5 detik
    dbCtx, dbCancel := context.WithTimeout(ctx, 5*time.Second)
    defer dbCancel()

    data, err := repoAmbilData(dbCtx)
    if err != nil {
        return nil, err
    }

    // Timeout berbeda untuk panggilan API eksternal: 3 detik
    apiCtx, apiCancel := context.WithTimeout(ctx, 3*time.Second)
    defer apiCancel()

    enriched, err := apiEksternal(apiCtx, data)
    if err != nil {
        return nil, err
    }

    return proses(enriched), nil
}

Propagasi Request ID untuk Distributed Tracing #

// Middleware yang menambahkan trace context
func middlewareTracing(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        traceID := r.Header.Get("X-Trace-ID")
        if traceID == "" {
            traceID = newTraceID()
        }
        spanID := newSpanID()

        ctx := r.Context()
        ctx = context.WithValue(ctx, KeyTraceID, traceID)
        ctx = context.WithValue(ctx, contextKey("span_id"), spanID)

        w.Header().Set("X-Trace-ID", traceID)

        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

// Logger yang otomatis menyertakan trace info
type ContextLogger struct {
    logger *log.Logger
}

func (l *ContextLogger) Info(ctx context.Context, format string, args ...any) {
    traceID := ambilNilaiString(ctx, KeyTraceID)
    spanID := ambilNilaiString(ctx, contextKey("span_id"))
    prefix := fmt.Sprintf("[trace:%s span:%s] ", traceID, spanID)
    l.logger.Printf(prefix+format, args...)
}

func ambilNilaiString(ctx context.Context, key contextKey) string {
    if v, ok := ctx.Value(key).(string); ok {
        return v
    }
    return "-"
}

Graceful Shutdown dengan Context #

func main() {
    // Context utama aplikasi
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()

    // Mulai server
    server := &http.Server{Addr: ":8080", Handler: buatHandler()}

    go func() {
        if err := server.ListenAndServe(); err != http.ErrServerClosed {
            log.Printf("server error: %v", err)
            cancel() // batalkan context utama jika server crash
        }
    }()

    // Mulai background jobs
    go prosesAntrian(ctx)
    go bersihkanCacheKadaluarsa(ctx)
    go sinkronisasiData(ctx)

    // Tunggu sinyal shutdown
    sigChan := make(chan os.Signal, 1)
    signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)

    select {
    case sig := <-sigChan:
        log.Printf("menerima sinyal: %v", sig)
    case <-ctx.Done():
        log.Printf("context utama di-cancel")
    }

    // Mulai shutdown
    log.Println("memulai graceful shutdown...")
    cancel() // hentikan semua background jobs

    // Beri waktu untuk server menyelesaikan request yang ada
    shutdownCtx, shutdownCancel := context.WithTimeout(
        context.Background(), 30*time.Second)
    defer shutdownCancel()

    if err := server.Shutdown(shutdownCtx); err != nil {
        log.Printf("shutdown error: %v", err)
    }

    log.Println("shutdown selesai")
}

Kapan Beralih ke Alternatif #

Tetap gunakan context jika:
  ✓ Cancellation dan timeout untuk semua operasi I/O
  ✓ Meneruskan request-scoped values (request ID, pengguna, trace ID)
  ✓ Koordinasi shutdown antar goroutine
  ✓ Integrasi dengan library yang mendukung context (database/sql, net/http, dll)

Pertimbangkan sync.WaitGroup jika:
  ✗ Hanya butuh menunggu sekelompok goroutine selesai
  ✗ Tidak butuh cancellation atau timeout
  ✗ Koordinasi sederhana tanpa propagasi nilai

Pertimbangkan channel biasa jika:
  ✗ Komunikasi satu arah antar goroutine yang spesifik
  ✗ Tidak butuh propagasi cancellation ke banyak goroutine

Pertimbangkan errgroup (golang.org/x/sync/errgroup) jika:
  ✗ Jalankan beberapa goroutine dan kumpulkan error pertama
  ✗ Otomatis cancel semua jika satu goroutine gagal
  ✗ errgroup.WithContext menggabungkan WaitGroup + Context dengan elegan

Ringkasan #

  • Selalu defer cancel() setelah WithCancel, WithTimeout, atau WithDeadline — tanpa ini, resource internal (timer, channel) tidak dibebaskan sampai parent di-cancel, menyebabkan goroutine leak.
  • Context mengalir dari atas ke bawah — handler menerima context dari request (r.Context()), meneruskannya ke service, service ke repository, repository ke database. Jangan buat context.Background() baru di tengah stack.
  • WithTimeout vs WithDeadline: gunakan WithTimeout untuk batas durasi relatif (“maksimal 5 detik dari sekarang”), gunakan WithDeadline untuk batas waktu absolut (“harus selesai sebelum jam 00:00”).
  • Bedakan context.Canceled dan context.DeadlineExceeded saat menangani error — yang pertama biasanya karena client disconnect (log sebagai info), yang kedua karena operasi terlalu lambat (log sebagai warning, perlu investigasi).
  • Gunakan tipe kustom untuk key WithValue, bukan string biasa — ini mencegah collision dengan package lain yang mungkin menggunakan key yang sama.
  • WithValue untuk data request-scoped saja — request ID, pengguna terautentikasi, trace ID. Jangan gunakan untuk oper parameter fungsi; gunakan parameter eksplisit.
  • context.WithoutCancel (Go 1.21+) untuk operasi cleanup yang harus selesai meski request utama di-cancel — seperti mengirim notifikasi atau menulis audit log.
  • Context-aware library: database/sql, net/http, os/exec, dan hampir semua library Go modern menerima context — selalu gunakan varian yang menerima context (QueryContext, NewRequestWithContext, CommandContext).
  • ctx.Done() di select adalah cara idiomatis untuk membuat goroutine yang responsif terhadap cancellation — selalu pasangkan dengan case lain yang menangani pekerjaan aktual.

← Sebelumnya: Net Http   Berikutnya: Sync →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact