Timeout dalam sepuluh baris
Tugas context.Context adalah memberi tahu kode kapan harus berhenti. Di sini sebuah operasi lambat diberi waktu 50 ms, dan menyerah saat context memintanya:
Pemanggilan pertama selesai dalam 10 ms dan mengembalikan rows <nil>. Pemanggilan kedua butuh 200 ms, tetapi context-nya kedaluwarsa di 50 ms (dihitung sejak context dibuat), jadi yang dikembalikan adalah context deadline exceeded.
Tidak ada yang dihentikan secara paksa. Go tidak punya cara untuk mematikan goroutine dari luar. Context adalah sinyal, dan kode harus memeriksanya: dengan select pada ctx.Done(), dengan memeriksa ctx.Err() di antara langkah-langkah, atau dengan mengirim ctx ke pemanggilan library (http.NewRequestWithContext, db.QueryContext, exec.CommandContext) yang memeriksanya untukmu.
Interface Context
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key any) any
}
| Method | Mengembalikan |
|---|---|
Done() | channel yang ditutup saat context dibatalkan atau kehabisan waktu (nil untuk context yang tidak pernah bisa dibatalkan) |
Err() | nil selama masih aktif, lalu context.Canceled atau context.DeadlineExceeded |
Deadline() | deadline dan true, atau ok == false jika tidak ada |
Value(key) | nilai yang disimpan dengan key di context ini atau di salah satu leluhurnya, atau nil |
Context bersifat immutable. Kamu tidak pernah mengubahnya; kamu menurunkan context anak darinya dengan salah satu fungsi With, dan context anak itu menambahkan sinyal pembatalan, deadline, atau nilai.
Dari mana context berasal
Setiap pohon context dimulai dari sebuah akar:
context.Background()untukmain,init, test, dan persiapan di level teratas sebuah server.context.TODO()saat sebuah fungsi seharusnya menerima context tetapi pemanggilnya belum punya. Perilakunya persis sepertiBackground; namanya hanya penanda untuk refactoring nanti.
Di dalam handler HTTP kamu tidak membuat akar. Kamu memakai r.Context(), yang dibatalkan server saat client terputus atau handler selesai.
WithCancel: berhenti kapan saja
context.WithCancel mengembalikan context anak dan sebuah fungsi cancel. Memanggil cancel menutup channel Done milik anak itu dan channel Done milik semua context yang diturunkan darinya.
Pengiriman di producer ada di dalam select di samping ctx.Done(). Itulah yang membuatnya bisa berhenti: out <- i saja akan tertahan selamanya begitu consumer berhenti membaca, dan goroutine-nya akan bocor. for range nums di akhir menunggu sampai producer menutup channel. Selama channel dikosongkan, producer mungkin masih sempat mengirim satu atau dua nilai, karena saat kedua case di select-nya siap, Go memilih salah satunya secara acak; pembatalan berlangsung cepat, tetapi tidak seketika.
cancel aman dipanggil lebih dari sekali dan dari goroutine mana pun. Hanya pemanggilan pertama yang melakukan sesuatu.
WithTimeout dan WithDeadline
WithTimeout(parent, d) sama dengan WithDeadline(parent, time.Now().Add(d)). Pakai timeout untuk "paling lama sekian" dan deadline saat kamu punya waktu yang pasti.
Begitu waktunya lewat, Done ditutup dan Err mengembalikan context.DeadlineExceeded. Kalau cancel dipanggil lebih dulu, Err mengembalikan context.Canceled. Periksa yang mana dengan errors.Is, karena library biasanya membungkus error-nya:
Selalu panggil cancel, bahkan untuk timeout yang akan terpicu dengan sendirinya. Context menyimpan sebuah timer dan satu slot di induknya sampai salah satu hal itu terjadi, dan defer cancel() melepaskan keduanya begitu fungsi selesai. go vet melaporkan fungsi cancel yang dibuang: the cancel function returned by context.WithTimeout should be called, not discarded, to avoid a context leak.
Anak tidak bisa hidup lebih lama dari induknya
Context membentuk pohon. Membatalkan induk membatalkan semua keturunannya. Anak bisa punya deadline yang lebih pendek dari induknya, tetapi tidak pernah lebih panjang: deadline yang lebih awal selalu menang.
Inilah yang membuat context berguna di berbagai lapisan. Handler HTTP mendapat context yang mati bersama request-nya; pemanggilan database tiga lapisan di bawahnya menurunkan timeout 2 detik dari context itu. Kalau client memutus koneksi setelah 100 ms, query dibatalkan saat itu juga, bukan dua detik kemudian.
Selalu select pada ctx.Done() saat menunggu
Setiap goroutine yang menunggu (saat mengirim ke channel, menerima, atau menunggu timer) sebaiknya menunggu ctx.Done() pada saat yang sama. Untuk loop yang berat di CPU dan tidak pernah tertahan, periksa ctx.Err() sesekali:
for i, item := range items {
if i%1000 == 0 {
if err := ctx.Err(); err != nil {
return err
}
}
process(item)
}
Pakai time.After di dalam select untuk penantian sederhana, tetapi lebih baik pakai timer yang bisa dihentikan (atau timeout context) kalau penantian itu sering dibatalkan.
Penyebab pembatalan (Go 1.20 dan 1.21)
ctx.Err() hanya mengatakan canceled atau deadline exceeded. Untuk mencatat alasannya, pakai varian Cause:
WithCancelCause hadir di Go 1.20, WithTimeoutCause dan WithDeadlineCause di Go 1.21. Err tetap mengembalikan nilai standar sehingga pemeriksaan yang sudah ada tetap berfungsi; context.Cause memberikan detailnya.
WithValue, secukupnya
context.WithValue(parent, key, value) melampirkan satu nilai. ctx.Value(key) mencarinya lewat rantai induk.
Aturan untuk nilai:
- Pakai tipe yang tidak diekspor untuk key, jangan pernah
stringbiasa. Dua package yang sama-sama memakai"user"akan saling menimpa. (go vettidak menangkap hal ini;staticcheckmenangkapnya.) - Bungkus akses ke nilai dalam fungsi helper bertipe seperti
WithRequestIDdanRequestID, supaya pemanggil tidak pernah melihatanyatau key-nya. - Simpan hanya data yang terkait request dan melewati API: trace ID dan ID request, pengguna yang sudah terautentikasi, logger. Jangan pernah parameter opsional, handle database, atau konfigurasi. Semua itu tempatnya di argumen fungsi atau field struct, tempat compiler bisa memeriksanya dan pembaca bisa melihatnya.
- Pencarian menelusuri rantai satu induk demi satu induk, jadi setiap nilai yang kamu tambahkan membuat pencarian nilai lain bertambah satu langkah.
Konvensi
ctx context.Contextadalah parameter pertama dari setiap fungsi yang melakukan I/O, tertahan, atau memanggil sesuatu yang melakukannya:func Fetch(ctx context.Context, url string) error.- Jangan simpan context di dalam struct. Kirim ke setiap pemanggilan method. Context milik satu operasi, dan struct biasanya hidup lebih lama darinya. (Pengecualiannya adalah tipe yang mewakili satu operasi, seperti
http.Request.) - Jangan pernah mengirim
nilsebagai context. Pakaicontext.TODO()kalau tidak ada yang lebih baik. - Kembalikan
ctx.Err(), atau bungkus dengan%w, saat kamu berhenti karena context, supaya pemanggil bisa membedakan timeout dari kegagalan sungguhan.
Context di server dan client HTTP
Di sisi server: r.Context() dibatalkan saat client terputus, saat handler selesai, atau saat stream HTTP/2 di-reset. Di sisi client: http.NewRequestWithContext membuat request mematuhi timeout atau pembatalan. Program ini menjalankan kedua sisi lewat httptest:
Client menyerah di 50 ms dan menutup koneksi. Server menyadarinya, context request-nya dibatalkan, dan handler berhenti alih-alih menghabiskan 450 milidetik lagi untuk laporan yang tidak akan dibaca siapa pun. Di handler sungguhan, kamu meneruskan r.Context() ke setiap pemanggilan database dan HTTP, dan semuanya berhenti bersamaan.
Helper lainnya (Go 1.21)
context.WithoutCancel(ctx)mengembalikan context dengan nilai yang sama tetapi tidak ikut dibatalkan saatctxdibatalkan. Pakai untuk pekerjaan yang harus selesai setelah request berakhir, seperti menulis log audit.context.AfterFunc(ctx, f)menjalankanfdi goroutine-nya sendiri begituctxselesai, dan mengembalikan fungsistopuntuk membatalkan pendaftarannya.
Kesalahan umum
- Tidak memanggil
cancel. Selalu tulisdefer cancel()tepat setelahWithCancel,WithTimeout, atauWithDeadline. - Menjalankan goroutine yang mengabaikan
ctx. Kalau goroutine itu tertahan tanpa select padactx.Done(), pembatalan tidak berpengaruh apa-apa dan goroutine-nya bocor. - Membuat
context.Background()baru jauh di dalam rantai pemanggilan. Itu memutus hubungan dengan deadline dan pembatalan dari pemanggil. Teruskanctxyang kamu terima. - Membandingkan error dengan
==. Pakaierrors.Is(err, context.DeadlineExceeded); kebanyakan library membungkusnya. - Memakai
WithValueuntuk dependensi. Handle database yang disembunyikan di context adalah parameter yang tidak bisa lagi diperiksa compiler. - Mengharapkan pembatalan terjadi seketika. Kode baru menyadarinya pada pemeriksaan berikutnya. Loop panjang tanpa pemeriksaan akan terus berjalan.
Pertanyaan yang sering diajukan
Untuk apa context dipakai di Go?
context.Context memberi tahu sebuah fungsi dan semua yang dipanggilnya kapan harus menyerah: karena pemanggil membatalkan, karena deadline sudah lewat, atau karena client terputus. Context juga bisa membawa nilai yang terkait request, seperti ID request. Menurut konvensi, context adalah parameter pertama dan diberi nama ctx.
Apa bedanya context.Background dan context.TODO?
Keduanya mengembalikan context kosong yang tidak pernah dibatalkan dan tidak punya deadline maupun nilai. Perilakunya persis sama. Background() adalah akar untuk main, test, dan persiapan di level teratas. TODO() menandai tempat yang seharusnya menerima context sungguhan, tetapi kode di sekitarnya belum punya, sehingga mudah ditemukan nanti.
Kenapa saya harus memanggil cancel setelah context.WithTimeout?
WithTimeout, WithDeadline, dan WithCancel mendaftarkan context baru ke induknya dan mungkin menjalankan timer. Memanggil cancel melepaskan resource itu begitu kamu selesai, bukan saat timeout terpicu atau induknya dibatalkan. Tulis defer cancel() tepat setelah membuatnya; go vet memberi peringatan saat fungsi cancel dibuang.
Apa arti "context deadline exceeded" di Go?
Itu adalah teks dari context.DeadlineExceeded, error yang dikembalikan ctx.Err() begitu deadline sebuah context sudah lewat. Fungsi yang mematuhi context, seperti client HTTP dan driver database, mengembalikannya (sering kali dibungkus) saat waktunya habis. Periksa dengan errors.Is(err, context.DeadlineExceeded).
Apakah saya sebaiknya memakai context.WithValue untuk mengirim parameter?
Tidak. Pakai hanya untuk data yang terkait request, yang melintasi batas API dan tidak perlu diketahui oleh fungsi-fungsi di antaranya, seperti trace ID atau pengguna yang sudah terautentikasi. Apa pun yang dibutuhkan fungsi untuk melakukan tugasnya seharusnya ada di parameternya, tempat compiler bisa memeriksanya.