Setiap permintaan HTTP punya daur hidup: dimulai saat klien mengirim, berakhir saat respons selesai atau klien pergi di tengah jalan. Operasi di belakang handler — query database, panggilan API lain, komputasi berat — idealnya ikut berhenti saat permintaannya sudah tidak relevan. Go menjembatani kedua sisi itu lewat context.Context: server menempelkan satu context pada tiap request, klien bisa membatalkan kapan saja, dan seluruh operasi hilir ikut tertutup. Chapter ini mempraktikkan alurnya dari r.Context() sampai pembatalan yang terlihat di dalam handler.
Context Bawaan Setiap Request #
Setiap *http.Request yang diterima handler sudah membawa context yang bisa diambil dengan r.Context(). Context tersebut hidup tepat selama umur request, dan dua peristiwa menutupnya secara otomatis: respons selesai dikirim, atau klien memutus koneksi sebelum respons jadi.
Perilaku kedua mengejutkan: ketika klien membatalkan permintaan, server Go mendeteksi koneksi putus lalu menutup r.Context(). Handler yang mendengarkan sinyal itu bisa berhenti lebih awal dan menghemat CPU serta koneksi database.
Handler yang Merespons Pembatalan #
Handler yang bekerja lama sebaiknya memeriksa ctx.Done() secara berkala. Channel itu tertutup ketika context selesai — karena pembatalan maupun tenggat waktu. Pola standarnya membungkus tiap langkah dalam select:
for langkah := 1; langkah <= 10; langkah++ {
select {
case <-ctx.Done():
return // permintaan sudah tidak relevan, berhenti bekerja
case <-time.After(20 * time.Millisecond):
// satu langkah kerja selesai, lanjut ke langkah berikutnya
}
}Ketika Done tertutup lebih dulu, handler keluar dari loop tanpa menulis respons yang tidak akan dibaca siapa pun.
Membatalkan Permintaan dari Sisi Klien #
Di sisi pengirim, context dilampirkan ke permintaan keluar. Cara modern memakai http.NewRequestWithContext yang menerima context saat permintaan dibangun:
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)Permintaan lama yang sudah dibangun tetap bisa dilengkapi context lewat metode req.WithContext(ctx):
req, err := http.NewRequest(http.MethodGet, url, nil) if err != nil { log.Fatal(err) } req = req.WithContext(ctx)
Kedua bentuk tersebut setara; NewRequestWithContext hanyalah jalan pintas resmi sejak Go 1.13. Ketika context mencapai tenggat 50 milidetik, client.Do mengembalikan error yang membungkus context.DeadlineExceeded, dan koneksi yang sedang berjalan ditutup klien.
Program Demonstrasi #
Berkas contoh.go menggabungkan seluruh alur: server httptest dengan handler sepuluh langkah, permintaan normal, permintaan yang dibatalkan klien, lalu middleware-handler yang bertukar nilai lewat context:
package main
import (
"context"
"errors"
"fmt"
"io"
"net/http"
"net/http/httptest"
"strings"
"time"
)
type kunciRequestID struct{}
func main() {
// Bagian 1: handler merespons pembatalan yang datang dari klien.
hasil := make(chan string, 1)
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
for langkah := 1; langkah <= 10; langkah++ {
select {
case <-ctx.Done():
hasil <- "dibatalkan"
return
case <-time.After(20 * time.Millisecond):
// satu langkah kerja selesai, lanjut ke langkah berikutnya
}
}
hasil <- "selesai"
fmt.Fprintln(w, "respons penuh")
}))
defer ts.Close()
klien := &http.Client{Timeout: 2 * time.Second}
// permintaan normal: handler dibiarkan menyelesaikan seluruh kerjanya
resp, err := klien.Get(ts.URL)
if err != nil {
fmt.Println("permintaan normal gagal:", err)
return
}
isi, _ := io.ReadAll(resp.Body)
resp.Body.Close()
fmt.Println("permintaan normal:", resp.Status, "isi:", strings.TrimSpace(string(isi)))
fmt.Println("pandangan handler:", <-hasil)
// permintaan yang dibatalkan setelah 50 milidetik
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, ts.URL, nil)
if err != nil {
fmt.Println("gagal membuat permintaan:", err)
return
}
_, err = klien.Do(req)
fmt.Println("permintaan dibatalkan klien:", err != nil)
fmt.Println("error bertipe deadline:", errors.Is(err, context.DeadlineExceeded))
fmt.Println("pandangan handler:", <-hasil)
// Bagian 2: nilai context mengalir dari middleware ke handler.
middleware := func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := context.WithValue(r.Context(), kunciRequestID{}, "req-001")
next.ServeHTTP(w, r.WithContext(ctx))
})
}
tsNilai := httptest.NewServer(middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
id, _ := r.Context().Value(kunciRequestID{}).(string)
fmt.Fprintf(w, "request id: %s", id)
})))
defer tsNilai.Close()
resp2, err := klien.Get(tsNilai.URL)
if err != nil {
fmt.Println("permintaan nilai gagal:", err)
return
}
isi2, _ := io.ReadAll(resp2.Body)
resp2.Body.Close()
fmt.Println("nilai request id terbaca handler:", strings.TrimSpace(string(isi2)))
}Output yang Dihasilkan:
permintaan normal: 200 OK isi: respons penuh
pandangan handler: selesai
permintaan dibatalkan klien: true
error bertipe deadline: true
pandangan handler: dibatalkan
nilai request id terbaca handler: request id: req-001Mari kita telaah bagian-bagian penting program di atas:
- Baris 14: tipe
kunciRequestIDjadi kunci context; tipe tak diekspor mencegah tabrakan antar package. - Baris 18: channel
hasilberbuffer satu menjadi jalur pelaporan handler kemain, karena respons ke klien yang membatalkan tidak akan pernah sampai. - Baris 20-21:
httptest.NewServermenjalankan server lokal;ctx := r.Context()mengambil context bawaan request. - Baris 22-29: loop sepuluh langkah memakai
selectantara<-ctx.Done()dan<-time.After(...)— pola yang membuat handler bisa dibatalkan. - Baris 24-26: begitu
Donetertutup, handler mengirim"dibatalkan"lalureturntanpa menulis respons sia-sia. - Baris 31-32: pada jalur normal, handler mengirim
"selesai"lalu menulis respons penuh. - Baris 50-51:
context.WithTimeoutmenciptakan context yang membatalkan dirinya sendiri;defer cancel()mencegah kebocoran timer. - Baris 53:
http.NewRequestWithContextmelampirkan context itu ke permintaan keluar. - Baris 58-61:
client.Dogagal dengan error deadline, dan channel membuktikan handler di server juga melihat pembatalan. - Baris 64-69: middleware menanam nilai lewat
context.WithValuelalu meneruskanr.WithContext(ctx). - Baris 71-74: handler dalam membaca nilai dengan
r.Context().Value(...)dan type assertion dua-nilai yang aman. - Baris 77-84: klien menerima body
request id: req-001— nilai middleware sampai ke handler tanpa parameter tambahan.
Nilai dalam Context: Metadata Request #
Selain sinyal pembatalan, context bisa membawa nilai berumur satu request: request ID, identitas pengguna terautentikasi, atau bahasa pilihan. Demo di atas menunjukkan pola lengkapnya: middleware menanam nilai, handler dalam membacanya, tanpa parameter fungsi tambahan di antara keduanya.
Context Menyebar ke Operasi Hilir #
Kekuatan context baru terasa ketika context request diteruskan ke operasi lambat. Panggilan database modern menerima context lewat QueryContext, sehingga query ikut dibatalkan saat klien pergi:
ctx := r.Context() rows, err := db.QueryContext(ctx, “SELECT id, nama FROM pelanggan WHERE kota = $1”, kota)
Pola yang sama berlaku untuk panggilan API keluar dan operasi berkas. Pembahasan mendalam ada di chapter Context ; penerapan pada query database di Context dan Query Timeout .
Dengan context, satu pembatalan menjalar dari jari klien sampai query database. Pertanyaan berikutnya adalah bagaimana mematikan seluruh server dengan sama mulusnya ketika sistem operasi mengirim sinyal berhenti — itulah pekerjaan graceful shutdown pada chapter berikutnya.
Referensi #
- Dokumentasi package context — pembatalan, tenggat, dan nilai.
- Dokumentasi package net/http
— termasuk method
Request.Context. - Go Blog: Go Concurrency Patterns: Context — artikel asli desain context.
- Dokumentasi net/http/httptest — server pengujian lokal.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
contoh.go
Unduh berkaspackage main
import (
"context"
"errors"
"fmt"
"io"
"net/http"
"net/http/httptest"
"strings"
"time"
)
type kunciRequestID struct{}
func main() {
// Bagian 1: handler merespons pembatalan yang datang dari klien.
hasil := make(chan string, 1)
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
for langkah := 1; langkah <= 10; langkah++ {
select {
case <-ctx.Done():
hasil <- "dibatalkan"
return
case <-time.After(20 * time.Millisecond):
// satu langkah kerja selesai, lanjut ke langkah berikutnya
}
}
hasil <- "selesai"
fmt.Fprintln(w, "respons penuh")
}))
defer ts.Close()
klien := &http.Client{Timeout: 2 * time.Second}
// permintaan normal: handler dibiarkan menyelesaikan seluruh kerjanya
resp, err := klien.Get(ts.URL)
if err != nil {
fmt.Println("permintaan normal gagal:", err)
return
}
isi, _ := io.ReadAll(resp.Body)
resp.Body.Close()
fmt.Println("permintaan normal:", resp.Status, "isi:", strings.TrimSpace(string(isi)))
fmt.Println("pandangan handler:", <-hasil)
// permintaan yang dibatalkan setelah 50 milidetik
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, ts.URL, nil)
if err != nil {
fmt.Println("gagal membuat permintaan:", err)
return
}
_, err = klien.Do(req)
fmt.Println("permintaan dibatalkan klien:", err != nil)
fmt.Println("error bertipe deadline:", errors.Is(err, context.DeadlineExceeded))
fmt.Println("pandangan handler:", <-hasil)
// Bagian 2: nilai context mengalir dari middleware ke handler.
middleware := func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := context.WithValue(r.Context(), kunciRequestID{}, "req-001")
next.ServeHTTP(w, r.WithContext(ctx))
})
}
tsNilai := httptest.NewServer(middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
id, _ := r.Context().Value(kunciRequestID{}).(string)
fmt.Fprintf(w, "request id: %s", id)
})))
defer tsNilai.Close()
resp2, err := klien.Get(tsNilai.URL)
if err != nil {
fmt.Println("permintaan nilai gagal:", err)
return
}
isi2, _ := io.ReadAll(resp2.Body)
resp2.Body.Close()
fmt.Println("nilai request id terbaca handler:", strings.TrimSpace(string(isi2)))
}