context.Context

Pembatalan, tenggat waktu, dan nilai yang mengalir pada pohon panggilan.

Daftar isi

Konkurensi yang berjalan lama memerlukan cara berhenti dengan tertib, baik karena pekerjaannya sudah tidak diperlukan maupun karena tenggat waktunya terlampaui. context.Context menyediakan mekanisme itu beserta kemampuan membawa nilai yang mengalir menuruni pohon panggilan. Topik ini membahas peran context sebagai parameter pertama, pembatalan dan tenggat waktu, serta penyimpanan nilai pada context, disertai kesalahan pemakaian yang sering ditemukan pada layanan production. Setelah membacanya, alur pembatalan dapat dirancang dari fungsi pemanggil hingga pekerja terdalam.

Peran context pada pohon panggilan#

Sebuah context.Context menyimpan sinyal berhenti dan tenggat waktu untuk satu rantai panggilan. Objek context bukan data yang ditimpa dari waktu ke waktu, melainkan rujukan yang berlaku dari titik pembuatannya ke seluruh fungsi yang menerimanya, sehingga membentuk pohon. Satu context induk dapat diturunkan menjadi beberapa context anak melalui context.WithCancel, context.WithTimeout, atau context.WithDeadline, dan setiap penurunan mengembalikan fungsi cancel yang membatalkan cabangnya. Membatalkan context induk ikut membatalkan seluruh anaknya, sedangkan pembatalan anak tidak memengaruhi induknya. Konvensi paket context menetapkan ctx sebagai parameter pertama pada fungsi yang meneruskannya, sehingga rantai pembatalan terbaca langsung dari daftar parameternya.

Titik paling atas pohon context biasanya adalah context.Background, yaitu context kosong yang tidak pernah dibatalkan dan tidak memuat nilai. Fungsi main serta penangan permintaan jaringan memulai rantainya dari context ini, lalu menurunkannya setiap kali pekerjaan baru diluncurkan. Pada server yang menerima banyak permintaan, setiap permintaan memperoleh context turunannya sendiri, sehingga pembatalan satu permintaan tidak menjalar ke permintaan lain.

Antarmuka context menyediakan empat metode utama. Metode Done mengembalikan channel yang ditutup ketika context berhenti, metode Err menyatakan alasannya, metode Deadline melaporkan tenggat yang berlaku, dan metode Value membaca nilai yang melekat pada context. Penurunan tenggat mengikuti aturan yang sama dengan pembatalan, yaitu context anak tidak boleh berjalan lebih lama daripada induknya.

Rantai panggilan yang panjang pun aman, karena sinyal dari context paling atas menjangkau pekerja terdalam tanpa perlu diteruskan manual satu per satu. Mekanisme inilah yang membedakan context dari pemberhentian goroutine lewat channel terpisah, yang harus dirangkai sendiri oleh setiap fungsi perantara. Dengan context, kontrak penghentian melekat pada tanda tangan fungsi lewat parameter ctx yang seragam.

Pembatalan dan tenggat waktu#

Program berikut menurunkan tiga context dari context.Background untuk tiga pekerja dengan durasi yang berbeda. Pekerja pertama tuntas dalam 10 milidetik dengan tenggat 1 detik, pekerja kedua membutuhkan 1 detik dengan tenggat 10 milidetik, dan pekerja ketiga dibiarkan berjalan 10 detik sebelum dibatalkan secara eksplisit. Status akhir ketiganya dicetak setelah Wait kembali.

go batalkan-tenggat.go
package main

import (
	"context"
	"errors"
	"fmt"
	"sync"
	"time"
)

// kerja menunggu sampai durasi pekerjaan habis atau context berhenti,
// lalu mengisi status akhirnya pada slot yang disediakan pemanggil.
func kerja(ctx context.Context, durasi time.Duration, status *string) {
	select {
	case <-time.After(durasi):
		*status = "selesai"
	case <-ctx.Done():
		switch {
		case errors.Is(ctx.Err(), context.DeadlineExceeded):
			*status = "tenggat terlampaui"
		case errors.Is(ctx.Err(), context.Canceled):
			*status = "dibatalkan"
		}
	}
}

func main() {
	var (
		wg     sync.WaitGroup
		status = make([]string, 3)
	)

	ctxCepat, batalkanCepat := context.WithTimeout(context.Background(), 1*time.Second)
	defer batalkanCepat()
	wg.Add(1)
	go func() {
		defer wg.Done()
		kerja(ctxCepat, 10*time.Millisecond, &status[0])
	}()

	ctxLambat, batalkanLambat := context.WithTimeout(context.Background(), 10*time.Millisecond)
	defer batalkanLambat()
	wg.Add(1)
	go func() {
		defer wg.Done()
		kerja(ctxLambat, 1*time.Second, &status[1])
	}()

	ctxTunda, batalkanTunda := context.WithCancel(context.Background())
	wg.Add(1)
	go func() {
		defer wg.Done()
		kerja(ctxTunda, 10*time.Second, &status[2])
	}()

	time.Sleep(10 * time.Millisecond) // memberi waktu ketiga pekerja berjalan dahulu
	batalkanTunda()                   // membatalkan pekerja ketiga secara eksplisit

	wg.Wait()
	nama := []string{"cepat", "lambat", "dibatalkan"}
	for i, s := range nama {
		fmt.Printf("pekerja %s: %s\n", s, status[i])
	}
}
text Keluaran
pekerja cepat: selesai
pekerja lambat: tenggat terlampaui
pekerja dibatalkan: dibatalkan

Setiap pekerja menunggu lewat select pada dua sinyal, yaitu time.After yang menyatakan pekerjaan tuntas dan ctx.Done() yang menyatakan context berhenti. Status tenggat terlampaui berasal dari context.DeadlineExceeded, sedangkan status dibatalkan berasal dari context.Canceled, dan errors.Is membedakan keduanya. Jarak antara durasi pekerjaan dan tenggat sengaja dibuat lebar, yaitu 10 milidetik berbanding 1 detik dan sebaliknya. Dengan demikian, hasil akhirnya sama pada mesin apa pun dan tidak bergantung pada kecepatan eksekusi.

bash
mkdir -p /tmp/go-context
cp batalkan-tenggat.go context-nilai.go /tmp/go-context/
cd /tmp/go-context
go run batalkan-tenggat.go
text Keluaran
pekerja cepat: selesai
pekerja lambat: tenggat terlampaui
pekerja dibatalkan: dibatalkan

Nilai yang mengalir pada context#

context.WithValue menautkan satu pasangan kunci dan nilai pada context baru, dan nilai itu dapat dibaca oleh setiap fungsi yang menerima turunannya. Kuncinya bertipe khusus, bukan string, agar tidak bertabrakan dengan kunci paket lain, dan tipe tersebut lazim tidak diekspor. Pola ini dipakai untuk data yang melekat pada satu permintaan, misalnya identitas peminta yang perlu dibaca di beberapa lapisan panggilan.

go context-nilai.go
package main

import (
	"context"
	"fmt"
)

// kunciPeminta adalah tipe kunci yang tidak diekspor, sehingga hanya
// paket ini yang dapat menulis dan membaca nilai tersebut.
type kunciPeminta struct{}

func proses(ctx context.Context) {
	id, _ := ctx.Value(kunciPeminta{}).(string)
	fmt.Println("id peminta:", id)
}

func main() {
	ctx := context.WithValue(context.Background(), kunciPeminta{}, "permintaan-17")
	proses(ctx)
}
text Keluaran
id peminta: permintaan-17

Fungsi proses tidak menerima identitas sebagai parameter terpisah karena nilai itu mengalir lewat context. Type assertion pada pembacaannya menyatakan ekspektasi tipe secara eksplisit dan memberi nilai kosong apabila kunci tidak ditemukan. Pemakaian WithValue sebaiknya dijaga pada data pelengkap seperti identitas dan penanda pemantauan, sedangkan data yang wajib bagi logika fungsi tetap dinyatakan sebagai parameter biasa agar tanda tangan fungsinya jujur.

bash
cd /tmp/go-context
go run context-nilai.go
text Keluaran
id peminta: permintaan-17

Kesalahan umum#

  • Menyimpan context pada field struct. Masa hidup context menjadi tidak jelas dan pemanggilan berikutnya dapat memakai context yang sudah dibatalkan. Perbaikannya, teruskan context sebagai parameter pertama pada setiap fungsi yang memerlukannya.
  • Mengabaikan ctx.Done() pada perulangan panjang. Perulangan yang tidak memeriksa sinyal berhenti tetap berjalan setelah pembatalan dan sumber daya yang dipakainya tidak lepas. Perbaikannya, masukkan kasus <-ctx.Done() pada setiap select yang mewakili satu putaran kerja.
  • Menjadikan WithValue pengganti parameter fungsi. Nilai yang disembunyikan membuat tanda tangan fungsi tidak lagi menyatakan kebutuhannya. Perbaikannya, simpan hanya data pelengkap pada context dan nyatakan data utama sebagai parameter.
  • Menganggap WithTimeout menghentikan pekerjaan secara paksa. Context hanya memberi sinyal lewat Done, sedangkan goroutine yang tidak memeriksanya tetap berjalan sampai selesai. Perbaikannya, pasang pemantauan <-ctx.Done() pada titik kerja yang paling lama, misalnya setiap putaran perulangan atau setiap panggilan jaringan.

Ringkasan#

  • context.Context membawa sinyal berhenti dan tenggat waktu menuruni pohon panggilan sebagai parameter pertama.
  • WithCancel, WithTimeout, dan WithDeadline menurunkan context anak beserta fungsi cancel yang wajib dipanggil.
  • Status berhenti dibedakan melalui errors.Is terhadap context.Canceled dan context.DeadlineExceeded.
  • WithValue menautkan data pelengkap dengan kunci bertipe yang tidak diekspor, sedangkan data utama tetap menjadi parameter fungsi.

Referensi#