Operasi nyata jarang berdiri sendiri. Sebuah handler HTTP memanggil service, service memanggil repository, repository menjalankan query — semuanya membentuk rantai panggilan yang panjang. Ketika pengguna menutup browser atau permintaan melewati batas waktu, kita perlu memberi tahu seluruh rantai sekaligus agar berhenti membuang tenaga. Menambahkan parameter “silakan berhenti” pada tiap fungsi membuat API berantakan. Package context memecahkan masalah ini dengan satu interface kecil yang mengalir sebagai argumen pertama: sinyal pembatalan, tenggat, dan data permintaan ikut menyeberang batas antar API. Chapter ini membahas context.Background, WithCancel, channel Done, Err, dan WithValue.
Apa Itu context.Context #
context.Context adalah interface dengan empat method:
| Method | Peran |
|---|---|
Done() <-chan struct{} | channel yang ditutup saat context dibatalkan atau melewati tenggat |
Err() error | alasan pembatalan; nil selama Done() belum tertutup |
Deadline() (time.Time, bool) | tenggat waktu context, bila ada |
Value(key any) any | nilai yang terkait kunci, bila ada |
Kunci desainnya ada pada Done(): berupa channel yang ditutup — bukan channel yang mengirim nilai. Menerima dari channel yang sudah tertutup selalu berhasil segera, sehingga Done() aman dipakai dalam select bersama channel lain meski context belum dibatalkan.
Background dan Pohon Context #
Semua context lahir dari context.Background() sebagai akar: context kosong tanpa tenggat, tanpa nilai, dan tidak pernah dibatalkan. Constructor seperti WithCancel membungkus context induk lalu menghasilkan context anak, sehingga terbentuk pohon. Membatalkan satu context menutup Done()-nya beserta seluruh turunannya. Untuk fungsi yang belum tahu context mana yang akan dipakai, tersedia context.TODO().
Membatalkan dengan WithCancel #
Berkas context-demo.go menjalankan satu worker yang memproses tugas dari channel, lalu berhenti dengan rapi saat cancel() dipanggil:
package main
import (
"context"
"errors"
"fmt"
"sync"
)
type kunciCtx string
const kunciUser kunciCtx = "user"
func kerja(ctx context.Context, tugas <-chan int, wg *sync.WaitGroup) {
defer wg.Done()
for {
select {
case id := <-tugas:
fmt.Printf("user %s memproses tugas %d\n", ctx.Value(kunciUser), id)
case <-ctx.Done():
fmt.Println("worker berhenti:", ctx.Err())
return
}
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
ctx = context.WithValue(ctx, kunciUser, "budi")
tugas := make(chan int)
var wg sync.WaitGroup
wg.Add(1)
go kerja(ctx, tugas, &wg)
tugas <- 1
tugas <- 2
cancel()
wg.Wait()
fmt.Println("Err() setelah cancel:", ctx.Err())
fmt.Println("Canceled?", errors.Is(ctx.Err(), context.Canceled))
}Jalankan program tersebut dari direktori chapter:
go run context-demo.goOutput yang Dihasilkan:
user budi memproses tugas 1
user budi memproses tugas 2
worker berhenti: context canceled
Err() setelah cancel: context canceled
Canceled? trueMari kita telaah baris demi baris program di atas:
- Baris 10–12 (
kunciCtxdankunciUser): tipe kunci unexported untukWithValue; tipe sendiri mencegah tabrakan kunci antar package. - Baris 14–25 (
kerja): worker memproses tugas dalam perulanganselect: menerima tugas baru atau keluar saatDone()tertutup. - Baris 19 (
ctx.Value): membaca data permintaan yang dibawa context — di sini nama pengguna untuk setiap baris log. - Baris 20–22 (
case <-ctx.Done()): jalur keluar;ctx.Err()mencetak alasan pembatalan, lalureturnmengakhiri goroutine dengan rapi. - Baris 28–29 (
WithCanceldandefer cancel()): membuat context anak yang bisa dibatalkan;defermemastikan fungsi pelepas sumber daya selalu terpanggil. - Baris 39–40 (
tugas <- 1dantugas <- 2): channel tanpa buffer membuat pengiriman selesai hanya setelah worker menerimanya, sehingga urutan baris output terjamin. - Baris 42–43 (
cancel()danwg.Wait()): pembatalan dikirim lebih dulu, lalumainmenunggu worker benar-benar keluar sebelum mencetak kesimpulan.
Urutan output tersebut pasti: pengiriman channel tanpa buffer bersifat sinkron sehingga kedua tugas terproses lebih dulu, dan wg.Wait() menahan main sampai pesan berhenti tercetak.
Membaca Sinyal dengan Done dan Err #
Setelah cancel() dipanggil, Done() tertutup dan Err() mengembalikan context.Canceled. Blok case <-ctx.Done() menghentikan pekerjaan, dan ctx.Err() memberi tahu pemanggil alasannya. Pemeriksaan errors.Is(err, context.Canceled) pada baris terakhir contoh adalah cara standar membedakan pembatalan dari galat lain.
Membawa Nilai dengan WithValue #
context.WithValue(ctx, kunci, nilai) menyimpan data berstatus permintaan — identitas pengguna, ID pelacakan, kredensial — yang perlu menembus batas fungsi. Nilai dicari dari context anak ke induk, seperti pencarian variabel pada scope bertingkat.
Konvensi di Ekosistem Go #
Standar komunitasnya baku: context.Context menjadi parameter pertama bernama ctx, dan fungsi yang melakukan I/O atau pekerjaan berat wajib menerimanya. Dengan konvensi ini, sinyal pembatalan menyebar dari titik masuk sampai ke daun tanpa mengubah bentuk API.
Pembatalan manual dengan WithCancel sudah tertangani. Chapter berikutnya melengkapi pola ini dengan tenggat waktu otomatis lewat context.WithTimeout.
Referensi #
- Package context — kontrak interface dan seluruh constructor.
- Go Blog: Context — latar desain dan pola pemakaian.
- context.WithCancel — detail constructor pembatalan manual.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
context-demo.go
Unduh berkaspackage main
import (
"context"
"errors"
"fmt"
"sync"
)
type kunciCtx string
const kunciUser kunciCtx = "user"
func kerja(ctx context.Context, tugas <-chan int, wg *sync.WaitGroup) {
defer wg.Done()
for {
select {
case id := <-tugas:
fmt.Printf("user %s memproses tugas %d\n", ctx.Value(kunciUser), id)
case <-ctx.Done():
fmt.Println("worker berhenti:", ctx.Err())
return
}
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
ctx = context.WithValue(ctx, kunciUser, "budi")
tugas := make(chan int)
var wg sync.WaitGroup
wg.Add(1)
go kerja(ctx, tugas, &wg)
tugas <- 1
tugas <- 2
cancel()
wg.Wait()
fmt.Println("Err() setelah cancel:", ctx.Err())
fmt.Println("Canceled?", errors.Is(ctx.Err(), context.Canceled))
}