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:

MethodPeran
Done() <-chan struct{}channel yang ditutup saat context dibatalkan atau melewati tenggat
Err() erroralasan pembatalan; nil selama Done() belum tertutup
Deadline() (time.Time, bool)tenggat waktu context, bila ada
Value(key any) anynilai 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:

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

bash
go run context-demo.go

Output yang Dihasilkan:

text
user budi memproses tugas 1
user budi memproses tugas 2
worker berhenti: context canceled
Err() setelah cancel: context canceled
Canceled? true

Mari kita telaah baris demi baris program di atas:

  1. Baris 10–12 (kunciCtx dan kunciUser): tipe kunci unexported untuk WithValue; tipe sendiri mencegah tabrakan kunci antar package.
  2. Baris 14–25 (kerja): worker memproses tugas dalam perulangan select: menerima tugas baru atau keluar saat Done() tertutup.
  3. Baris 19 (ctx.Value): membaca data permintaan yang dibawa context — di sini nama pengguna untuk setiap baris log.
  4. Baris 20–22 (case <-ctx.Done()): jalur keluar; ctx.Err() mencetak alasan pembatalan, lalu return mengakhiri goroutine dengan rapi.
  5. Baris 28–29 (WithCancel dan defer cancel()): membuat context anak yang bisa dibatalkan; defer memastikan fungsi pelepas sumber daya selalu terpanggil.
  6. Baris 39–40 (tugas <- 1 dan tugas <- 2): channel tanpa buffer membuat pengiriman selesai hanya setelah worker menerimanya, sehingga urutan baris output terjamin.
  7. Baris 42–43 (cancel() dan wg.Wait()): pembatalan dikirim lebih dulu, lalu main menunggu 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 #

Berkas Sumber

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

context-demo.go

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