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:

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

bash
go run timeout-demo.go

Output yang Dihasilkan:

text
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:

  1. Baris 13–27 (prosesLambat): fungsi pekerjaan yang menerima ctx sebagai parameter pertama — inilah pola propagasi yang membuat fungsi bisa dihentikan dari luar.
  2. 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.
  3. Baris 16–19 (goroutine pekerja): mensimulasikan pekerjaan berat selama 300 milidetik, lalu mengirim "data siap".
  4. Baris 21–26 (select): pertarungan dua cabang — hasil selesai lebih dulu, atau tenggat lewat lebih dulu.
  5. Baris 31 (WithTimeout(..., 100*time.Millisecond)): tenggat 100 milidetik; pekerjaan 300 milidetik pasti kalah, sehingga cabang ctx.Done() yang menang dan ctx.Err() bernilai context.DeadlineExceeded.
  6. Baris 34–36: mencetak hasil ganda kosong beserta galatnya, lalu membuktikan jenis galatnya dengan errors.Is.
  7. 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:

GalatTerjadi ketika
context.Canceledseseorang memanggil cancel() secara manual
context.DeadlineExceededtenggat 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:

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

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

timeout-demo.go

Unduh berkas
go
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)
}