Context — pembatalan & tenggat
Saat pengguna menutup browser, kueri database yang sedang berjalan untuk permintaan itu seharusnya ikut berhenti. Context adalah mekanisme yang membuat pembatalan itu menembus setiap lapisan.
Intisari
ctx context.Contextselalu parameter pertama, tidak pernah disimpan di struct.- Pembatalan mengalir ke bawah: membatalkan induk membatalkan seluruh turunannya.
defer batal()wajib setiap kali membuat context turunan — kalau tidak, sumber dayanya bocor.r.Context()di handler HTTP sudah dibatalkan otomatis saat klien memutus koneksi.context.WithValuehanya untuk data lintas-permintaan sepertirequest_id— bukan untuk mengoper parameter.
Kenapa ada
// Tanpa context: kueri ini berjalan sampai selesai, meski penggunanya
// sudah menutup tab lima detik yang lalu.
rows, err := db.Query("SELECT ...")
// Dengan context: batal berarti batal, sampai ke driver database.
rows, err := db.QueryContext(ctx, "SELECT ...")
Satu permintaan HTTP di aplikasi nyata menyentuh belasan lapisan: handler → service → repository → driver database → soket. Context adalah satu-satunya cara sinyal "berhenti" menembus semuanya tanpa setiap lapisan menciptakan mekanismenya sendiri.
Empat cara membuatnya
ctx := context.Background() // akar — di main() dan di tes
ctx := r.Context() // di handler HTTP: sudah punya pembatalan klien
// Tenggat relatif
ctx, batal := context.WithTimeout(ctx, 3*time.Second)
defer batal()
// Tenggat absolut
ctx, batal := context.WithDeadline(ctx, waktuMulai.Add(30*time.Second))
defer batal()
// Pembatalan manual
ctx, batal := context.WithCancel(ctx)
defer batal()
// Pembatalan dengan alasan (Go 1.20+) — muncul di ctx.Err()
ctx, batal := context.WithCancelCause(ctx)
batal(fmt.Errorf("pengguna membatalkan"))
defer batal() bukan opsional, bahkan saat kamu yakin context-nya akan kedaluwarsa
sendiri. Fungsi batal melepaskan sumber daya yang menghubungkan context ini ke induknya.
Tanpa itu, context induk yang berumur panjang menahan daftar turunan yang terus bertambah — kebocoran
memori yang naik pelan. Linter lostcancel di go vet menangkapnya.
Membaca pembatalan
select {
case <-ctx.Done():
return ctx.Err() // DeadlineExceeded atau Canceled
case hasil := <-ch:
return hasil, nil
}
// Di dalam loop panjang, periksa berkala
for i, item := range daftar {
if i%100 == 0 {
if err := ctx.Err(); err != nil {
return err
}
}
proses(item)
}
ctx.Err() | Artinya | Status HTTP yang wajar |
|---|---|---|
nil | Masih hidup | — |
context.Canceled | Dibatalkan — biasanya klien memutus | 499 / tidak perlu dicatat sebagai error |
context.DeadlineExceeded | Waktu habis | 504 |
Bedakan keduanya di log. Canceled hampir selalu berarti pengguna menutup tab —
itu bukan insiden, dan mencatatnya sebagai error akan menenggelamkan alarm yang sungguhan.
DeadlineExceeded berarti kamu terlalu lambat; itu yang layak diberi alarm.
Anggaran waktu bertingkat
// Batas terluar dipasang sekali, di middleware.
func BatasWaktu(d time.Duration) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx, batal := context.WithTimeout(r.Context(), d)
defer batal()
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
// Lapisan dalam mempersempit, tidak pernah memperlebar.
func (s *Layanan) Ambil(ctx context.Context, id int64) (Produk, error) {
ctx, batal := context.WithTimeout(ctx, 2*time.Second) // kueri DB
defer batal()
return s.db.Ambil(ctx, id)
}
WithTimeout selalu mengambil yang lebih ketat antara tenggat induk dan yang
baru. Jadi lapisan dalam tidak bisa memperpanjang umur permintaan — hanya mempersempit. Ini yang membuat
anggaran waktu bisa dijamin dari satu tempat.
| Lapisan | Anggaran khas |
|---|---|
| Load balancer (ALB idle timeout) | 60 detik |
Server HTTP (WriteTimeout) | 15 detik |
| Middleware permintaan | 10 detik |
| Kueri database | 2–3 detik |
| Panggilan HTTP ke layanan lain | 2–5 detik, dengan retry |
Urutannya harus mengecil ke dalam. Kalau timeout database (30 detik) lebih besar daripada timeout ALB (60 detik) dikurangi waktu lain, permintaan akan diputus di sisi klien sementara kuerinya tetap berjalan dan tetap memakan koneksi. Di bawah beban, itu cara tercepat menghabiskan connection pool.
WithValue: pakai hemat
// Kunci bertipe privat — mencegah tabrakan antar paket
type kunciCtx int
const (
kunciRequestID kunciCtx = iota
kunciPengguna
)
func DenganRequestID(ctx context.Context, id string) context.Context {
return context.WithValue(ctx, kunciRequestID, id)
}
func RequestID(ctx context.Context) string {
s, _ := ctx.Value(kunciRequestID).(string)
return s
}
| Layak di context | Tidak layak |
|---|---|
request_id, trace ID | ID produk yang mau diambil |
| Pengguna terautentikasi | Koneksi database |
| ID tenant | Konfigurasi aplikasi |
| Locale / bahasa | Logger… kecuali untuk membawa atribut permintaan |
Uji sederhananya: kalau fungsi tidak bisa bekerja tanpa nilai itu, jadikan parameter. Context untuk hal yang melintasi semua lapisan dan tidak ada satu lapisan pun yang benar-benar memilikinya. Nilai di context tidak bertipe dan tidak dijamin ada — setiap pemakaian adalah type assertion yang bisa gagal saat runtime.
Context yang lepas dari permintaan
// Pekerjaan latar yang HARUS selesai meski respons sudah terkirim
ctx := context.WithoutCancel(r.Context()) // Go 1.21+: bawa nilainya, buang pembatalannya
ctx, batal := context.WithTimeout(ctx, 30*time.Second)
go func() {
defer batal()
defer pulihkan()
simpanAudit(ctx, data)
}()
Sebelum Go 1.21, satu-satunya cara adalah memulai dari context.Background() — yang berarti
request_id dan trace ikut hilang, sehingga pekerjaan latar jadi tidak bisa dilacak. Sekarang
tidak perlu memilih.
Latihan: tulis handler HTTP yang memanggil fungsi lambat(ctx) berdurasi 5 detik
dengan context.WithTimeout(r.Context(), 2*time.Second). Panggil dan pastikan kamu menerima
504. Lalu panggil lagi dan tutup koneksinya di tengah (Ctrl-C pada curl) — dan buktikan di
log bahwa ctx.Err() kali ini context.Canceled, bukan
DeadlineExceeded.
Rangkuman ini sengaja dipangkas ke bagian yang dipakai di roadmap. Buka sumber aslinya saat kamu butuh detail lengkap atau referensi parameter.