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:#f3e5f5Empat 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:#ffcdd2func 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 panggilcancel()yang dikembalikan olehWithCancel,WithTimeout, danWithDeadline— biasanya dengandefer 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:#fff3e0func 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()setelahWithCancel,WithTimeout, atauWithDeadline— 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 buatcontext.Background()baru di tengah stack.WithTimeoutvsWithDeadline: gunakanWithTimeoutuntuk batas durasi relatif (“maksimal 5 detik dari sekarang”), gunakanWithDeadlineuntuk batas waktu absolut (“harus selesai sebelum jam 00:00”).- Bedakan
context.Canceleddancontext.DeadlineExceededsaat 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.WithValueuntuk 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()diselectadalah cara idiomatis untuk membuat goroutine yang responsif terhadap cancellation — selalu pasangkan dengan case lain yang menangani pekerjaan aktual.