Operasi yang berjalan terlalu lama adalah kewajiban yang terus memakan sumber daya: goroutine menunggu, koneksi terbuka, dan pengguna menatap layar tanpa jawaban. Program produksi membutuhkan dua kemampuan — membatalkan pekerjaan yang tidak lagi dibutuhkan, dan memasang batas waktu agar operasi lambat berhenti sendiri. Chapter sebelumnya sudah menunjukkan pembatalan manual dengan WithCancel; chapter ini melengkapinya dengan context.WithTimeout, pola select yang memperebutkan sinyal tenggat dengan hasil pekerjaan, serta cara menurunkan context ke seluruh fungsi di rantai panggilan.
Tenggat dengan WithTimeout #
context.WithTimeout(parent, durasi) membuat context anak yang Done()-nya tertutup otomatis begitu durasi terlampaui — penghitung mundurnya jalan sendiri tanpa perlu ada yang memanggil cancel. Sebenarnya fungsi ini adalah singkatan dari WithDeadline(parent, time.Now().Add(durasi)), sehingga keduanya hanya berbeda cara menyebut waktu. Fungsi cancel yang dikembalikan tetap wajib dipanggil — lewat defer cancel() — karena tenggat hanya menyentuh context, bukan melepas semua sumber daya lebih awal.
Pola select: Tenggat Melawan Hasil #
Inti polanya adalah satu pernyataan select dengan dua cabang: cabang hasil pekerjaan dan cabang ctx.Done(). Siapa pun yang lebih dulu siap menang; cabang yang kalah tidak menunggu. Berkas timeout-demo.go mendemonstrasikan keduanya dengan pekerjaan simulasi selama 300 milidetik:
package main
import (
"context"
"errors"
"fmt"
"time"
)
// prosesLambat mensimulasikan pekerjaan berat selama 300 milidetik.
// Fungsi ini menghormati ctx: saat ctx melewati tenggat, prosesLambat
// kembali lebih cepat tanpa menunggu pekerjaan selesai.
func prosesLambat(ctx context.Context) (string, error) {
selesai := make(chan string, 1)
go func() {
time.Sleep(300 * time.Millisecond) // simulasi pekerjaan berat
selesai <- "data siap"
}()
select {
case hasil := <-selesai:
return hasil, nil
case <-ctx.Done():
return "", ctx.Err()
}
}
func main() {
// Tenggat 100 milidetik: pekerjaan 300 milidetik tidak akan selesai tepat waktu.
ctxPendek, cancelPendek := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancelPendek()
hasil, err := prosesLambat(ctxPendek)
fmt.Printf("batas 100ms → hasil=%q err=%v\n", hasil, err)
fmt.Println("DeadlineExceeded?", errors.Is(err, context.DeadlineExceeded))
// Tenggat 500 milidetik: pekerjaan 300 milidetik selesai sebelum tenggat.
ctxPanjang, cancelPanjang := context.WithTimeout(context.Background(), 500*time.Millisecond)
defer cancelPanjang()
hasil, err = prosesLambat(ctxPanjang)
fmt.Printf("batas 500ms → hasil=%q err=%v\n", hasil, err)
}Jalankan program tersebut dari direktori chapter:
go run timeout-demo.goOutput yang Dihasilkan:
batas 100ms → hasil="" err=context deadline exceeded
DeadlineExceeded? true
batas 500ms → hasil="data siap" err=<nil>Mari kita telaah baris demi baris program di atas:
- Baris 13–27 (
prosesLambat): fungsi pekerjaan yang menerimactxsebagai parameter pertama — inilah pola propagasi yang membuat fungsi bisa dihentikan dari luar. - Baris 14 (
make(chan string, 1)): channel hasil berbuffer satu, sehingga goroutine pekerja tetap bisa mengirim hasil walau tidak ada yang membaca; goroutine tidak menggantung menunggu pembaca. - Baris 16–19 (goroutine pekerja): mensimulasikan pekerjaan berat selama 300 milidetik, lalu mengirim
"data siap". - Baris 21–26 (
select): pertarungan dua cabang — hasil selesai lebih dulu, atau tenggat lewat lebih dulu. - Baris 31 (
WithTimeout(..., 100*time.Millisecond)): tenggat 100 milidetik; pekerjaan 300 milidetik pasti kalah, sehingga cabangctx.Done()yang menang danctx.Err()bernilaicontext.DeadlineExceeded. - Baris 34–36: mencetak hasil ganda kosong beserta galatnya, lalu membuktikan jenis galatnya dengan
errors.Is. - Baris 39–43: pemanggilan kedua dengan tenggat 500 milidetik; kali ini pekerjaan 300 milidetik selesai lebih dulu dan cabang hasil yang menang.
Output di atas stabil di setiap eksekusi karena tiap panggilan hanya punya satu jalur kemenangan dengan margin lega: 100 milidetik berbanding 300 milidetik untuk kegagalan, dan 300 milidetik berbanding 500 milidetik untuk keberhasilan.
Membedakan Canceled dan DeadlineExceeded #
Context menyediakan dua nilai galat dasar yang mudah dibedakan:
| Galat | Terjadi ketika |
|---|---|
context.Canceled | seseorang memanggil cancel() secara manual |
context.DeadlineExceeded | tenggat waktu tercapai sebelum cancel() dipanggil |
Pemanggil tinggal memeriksa dengan errors.Is(err, context.DeadlineExceeded) atau errors.Is(err, context.Canceled), lalu memutuskan: ulangi operasinya, catat sebagai kegagalan, atau anggap wajar karena pengguna memang membatalkan. Kode yang mengabaikan perbedaan ini cenderung mencatat semua pembatalan sebagai galat sistem dan memenuhi log dengan alarm palsu.
Propagasi Context ke Fungsi #
Tenggat baru berguna bila semua fungsi di rantai menghormatinya. Aturan mainnya: teruskan ctx yang diterima ke setiap pemanggilan anak, dan turunkan context baru bila bagian tertentu butuh batas lebih ketat. Pola pada handler HTTP berikut menunjukkan bentuk praktisnya:
func handler(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 200*time.Millisecond)
defer cancel()
hasil, err := ambilData(ctx) // ambilData meneruskan ctx ke query
if err != nil {
http.Error(w, "permintaan kehabisan waktu", http.StatusGatewayTimeout)
return
}
fmt.Fprintln(w, hasil)
}r.Context() yang batal otomatis saat klien memutus koneksi menjadi induknya, lalu WithTimeout memasang batas tambahan 200 milidetik untuk operasi di dalamnya. Kedua jalur pembatalan — klien pergi atau waktu habis — sama-sama menjalar ke ambilData tanpa handler perlu tahu detailnya.
Dengan pola select dan propagasi ini, operasi panjang sudah punya rem darurat. Masalah berikutnya berbeda: bagaimana memproses banyak pekerjaan sekaligus tanpa meledakkan jumlah goroutine — chapter worker pool menjawabnya.
Referensi #
- context.WithTimeout — detail constructor tenggat waktu.
- Go Blog: Context — filosofi dan pola pemakaian context.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
timeout-demo.go
Unduh berkaspackage main
import (
"context"
"errors"
"fmt"
"time"
)
// prosesLambat mensimulasikan pekerjaan berat selama 300 milidetik.
// Fungsi ini menghormati ctx: saat ctx melewati tenggat, prosesLambat
// kembali lebih cepat tanpa menunggu pekerjaan selesai.
func prosesLambat(ctx context.Context) (string, error) {
selesai := make(chan string, 1)
go func() {
time.Sleep(300 * time.Millisecond) // simulasi pekerjaan berat
selesai <- "data siap"
}()
select {
case hasil := <-selesai:
return hasil, nil
case <-ctx.Done():
return "", ctx.Err()
}
}
func main() {
// Tenggat 100 milidetik: pekerjaan 300 milidetik tidak akan selesai tepat waktu.
ctxPendek, cancelPendek := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancelPendek()
hasil, err := prosesLambat(ctxPendek)
fmt.Printf("batas 100ms → hasil=%q err=%v\n", hasil, err)
fmt.Println("DeadlineExceeded?", errors.Is(err, context.DeadlineExceeded))
// Tenggat 500 milidetik: pekerjaan 300 milidetik selesai sebelum tenggat.
ctxPanjang, cancelPanjang := context.WithTimeout(context.Background(), 500*time.Millisecond)
defer cancelPanjang()
hasil, err = prosesLambat(ctxPanjang)
fmt.Printf("batas 500ms → hasil=%q err=%v\n", hasil, err)
}