43 / 73

Context va bekor qilish

Har bir uzoq ishlaydigan amal oxir-oqibat o'zi mustaqil javob bera olmaydigan savolga to'qnash keladi: ishlashni davom ettirishim kerakmi? Mijoz aloqani uzdi, so'rov darajasidagi deadline (muddat) o'tdi, yuqoridagi chaqiruv xatoga uchradi va qolgan ish endi ma'nosiz — bularning hech birini quyi darajadagi bitta funksiya ichidan bilib bo'lmaydi. Shuning uchun Go bekor qilish signallari, deadlinelar va oz miqdordagi so'rov doirasidagi metama'lumotlarni API chegaralari bo'ylab xavfsiz uzatish mexanizmi sifatida context.Contextni standartlashtirdi.

Context nimani olib yuradi

context.Context — interfeys bo'lib, uning talablarini bajaruvchi (implementatsiya qiluvchi) har bir qiymat uchta asosiy narsani o'z ichiga oladi: context bekor qilinganda yopiladigan Done() channel'i, bekor qilish sababini tushuntiradigan Err() metodi va so'rov doirasidagi ma'lumotlar uchun kichik kalit-qiymat juftliklari to'plami. Context obyektini noldan qo'lda yasamaysiz; buning o'rniga uni ildizdan (root) boshlab ota-context'dan hosil qilasiz:

example.go
package main

import (
	"context"
	"fmt"
)

func main() {
	ctx := context.Background()
	ctx, cancel := context.WithCancel(ctx)
	defer cancel()

	doWork(ctx)
}

func doWork(ctx context.Context) {
	select {
	case <-ctx.Done():
		fmt.Println("work cancelled:", ctx.Err())
	default:
		fmt.Println("working...")
	}
}

context.Background() — main, testlar va eng yuqori darajadagi dastlabki sozlashlar uchun kelishilgan ildiz context hisoblanadi: u hech qachon bekor qilinmaydi va hech qanday qo'shimcha qiymat saqlamaydi. context.TODO() xuddi shunday shaklda ishlatiladi, ammo u "bu funksiya ehtimol context parametrini qabul qilishi kerak, lekin men uni hali o'tkazmadim" degan ma'noni bildiradi. Uni production kodida doimiy qoldirmang, keyinchalik to'ldirilishi kerak bo'lgan vaqtinchalik belgi sifatida qarang.

Context'lar oddiy zanjir emas, butun boshli daraxt (tree) ierarxiyasini hosil qiladi. Ota (parent) context'dan WithCancel, WithTimeout, WithDeadline yoki WithValue orqali yangi context yaratish otasining bekor qilinish holati va qiymatlarini meros qilib oladigan hamda o'zining qo'shimchalarini ustiga qo'shadigan bola (child) context beradi. Ota context'ni bekor qilish barcha avlodlar bo'ylab avtomatik pastga tarqaladi:

matn
context.Background()
        │
        ├── WithTimeout(5s) ──┬── WithCancel()
        │                     └── WithTimeout(3s)
        └── WithValue(requestID)

5 soniyalik timeout tarmog'ini bekor qilsangiz, uning ostidagi ikkala bola context ham bekor qilinadi: agar ota context vaqti tugab birinchi to'xtasa, 3 soniyalik bola context o'z muddatini kutib o'tirmaydi. Aynan shu mexanizm context'ni so'rovlarni boshqarish uchun juda qulay va qudratli qiladi: so'rov uchun eng yuqori darajadagi context'ni bekor qilasiz va shu so'rov nomidan ish qilayotgan har bir goroutine — chaqiruv zanjiri qanchalik chuqur bo'lishidan qat'i nazar — agar bekor qilish holatini tekshirayotgan bo'lsa, barchasi to'xtatiladi.

Bekor qilish va Done channel'i

WithCancel yangi hosil qilingan context va cancel funksiyasini qaytaradi; uni chaqirish context'ning Done() channel'ini yopadi va Err()ni context.Canceledga o'rnatadi. Context'ni kim yaratgan bo'lsa, uni bekor qilish huquqi ham o'shaniki bo'ladi. Aynan shu sababli context yaratilgandan so'ng darhol defer cancel() chaqirish — idiomatik qoidadir: o'rab turgan funksiya o'z ishini tugatgach, context qanday sabab bilan yakunlanganidan qat'i nazar, barcha ichki resurslarning xotiradan to'liq bo'shatilishini kafolatlaydi.

Bekor qilish talabiga rioya qiluvchi funksiya to'xtash xavfsiz bo'lgan nuqtalarda ctx.Done()ni muntazam tekshirib borishi kerak — ko'pincha sikl iteratsiyasining boshida:

example.go
func processItems(ctx context.Context, items []Item) error {
	for _, item := range items {
		select {
		case <-ctx.Done():
			return ctx.Err()
		default:
		}
		if err := processItem(ctx, item); err != nil {
			return err
		}
	}
	return nil
}

Bu tekshiruvni unutish juda oson va bu context'ning o'z vazifasini bajarmay qolishining eng keng tarqalgan sababidir: chaqiruv zanjiri bo'ylab uzatilgan, lekin hech qayerda tekshirilmaydigan context.Context parametri go'yo bekor qilish tizimi ishlayotgandek soxta xavfsizlik taassurotini uyg'otadi. Agar processItem o'zi I/O amallarida bloklansa, u ham context'ni qabul qilishiga va tekshirishiga ishonch hosil qiling: http.NewRequestWithContext, db.QueryContext va Go standart kutubxonasidagi deyarli barcha bloklovchi chaqiruvlar bloklanishni to'xtatish imkoniyati bo'lishi uchun context qabul qiladi.

Tashqi dunyodan bekor qilish: signallar

Haqiqiy backend xizmatlarida bekor qilishning eng keng tarqalgan boshlang'ich manbai boshqa funksiya chaqiruvi emas — bu operatsion tizim tomonidan jarayonga to'xtash buyrug'i (signal) yuborilishidir. signal.NotifyContext aynan mana shu ko'prikni quradi: u berilgan signallardan biri kelgan zahoti bekor qilinadigan context qaytaradi. Bu main funksiyasida graceful shutdown (xavfsiz to'xtatish)ni amalga oshirishning standart usulidir:

example.go
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()

srv := &http.Server{Addr: ":8080", Handler: mux}
go srv.ListenAndServe()

<-ctx.Done() // SIGINT/SIGTERM kelguncha yoki stop() to'g'ridan-to'g'ri chaqirilguncha bloklanadi
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
srv.Shutdown(shutdownCtx)

ctx.Done()ni tekshiruvchi barcha komponentlar endi Ctrl-C yoki konteyner orkestratorining (masalan, Kubernetes) SIGTERM signali bilan toza to'xtaydi; dasturning boshqa biron joyida alohida signal qayta ishlash logikasi talab qilinmaydi. Go 1.26 da NotifyContext bekor qilinish sababi haqida yanada aniq ma'lumot beradigan bo'ldi: bekor qilinish endi uni aynan qaysi signal vujudga keltirganini saqlaydi (context.CancelCauseFunc uslubida). Natijada context.Cause(ctx) chaqiruvi orqali context aynan nima sababdan to'xtatilganini aniq bilish mumkin (qaysi signal kelgani yoki stop() qo'lda chaqirilgani).

Deadline'lar va timeout'lar

WithTimeout va WithDeadline — bir xil mexanizmning ikki xil ifodasidir: hozirgi vaqtdan nisbiy davomiylik (timeout) yoki mutlaq vaqt nuqtasi (deadline). WithTimeout(ctx, 5*time.Second) aslida WithDeadline(ctx, time.Now().Add(5*time.Second))ning qisqartmasidir. Amaliyotda WithTimeout ancha ko'p ishlatiladi, chunki timeout talablari odatda mutlaq ("soat 14:32:00 da to'xtat") emas, nisbiy ("5 soniyadan keyin to'xtat") bo'ladi.

example.go
func fetchWithTimeout(ctx context.Context, url string, timeout time.Duration) (*http.Response, error) {
	ctx, cancel := context.WithTimeout(ctx, timeout)
	defer cancel()

	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
	if err != nil {
		return nil, err
	}
	return http.DefaultClient.Do(req)
}

Timeout muddati tugaganda Do so'rovi cheksiz osilib qolmasdan, context.DeadlineExceeded bilan o'ralgan xatolik qaytaradi. Aynan shu sababli context'ni HTTP mijoz, ma'lumotlar bazasi drayveri va boshqa barcha bloklovchi chaqiruvlar orqali uzatish nihoyatda muhimdir. Aks holda sekin ishlayotgan tashqi server goroutineni cheksiz bloklab turishi mumkin, chunki bloklanish deadline haqida bexabar bo'lgan begona kod ichida sodir bo'layotgan bo'ladi.

database/sql ham xuddi shu qoidaga amal qiladi: QueryContext, ExecContext va shunga o'xshash metodlar context qabul qiladi va agar timeout yuz bersa, bazadagi so'rovni bekor qiladi. Production ma'lumotlar bazasiga yuboriladigan har bir so'rovga vaqt chegarasi (timeout context) qo'yilishi shart; bu tiqilib qolgan og'ir so'rovlar goroutinelarni va butun tizimni to'xtatib qo'yishining oldini oladi.

example.go
func getUser(ctx context.Context, db *sql.DB, userID int) (*User, error) {
	ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
	defer cancel()

	var user User
	row := db.QueryRowContext(ctx, "SELECT id, name FROM users WHERE id = $1", userID)
	if err := row.Scan(&user.ID, &user.Name); err != nil {
		return nil, err
	}
	return &user, nil
}

Context qiymatlari — ehtiyotkorlik bilan ishlatish kerak

context.WithValue hosil qilingan context'ga kalit-qiymat juftligini biriktiradi, uni quyi oqimning istalgan joyida ctx.Value(key) orqali olish mumkin. U oraliqdagi har bir funksiyaning parametri bo'lib o'tirmasdan, butun arxitektura qatlamlarini kesib o'tishi kerak bo'lgan so'rov doirasidagi maxsus metama'lumotlar uchun mo'ljallangan: masalan, loglarni tracing qilish uchun so'rov identifikatori (requestID), middleware aniqlagan autentifikatsiyalangan foydalanuvchi ma'lumotlari va hokazo.

example.go
type contextKey int

const userIDKey contextKey = iota

func withUserID(ctx context.Context, id int) context.Context {
	return context.WithValue(ctx, userIDKey, id)
}

func userIDFromContext(ctx context.Context) (int, bool) {
	id, ok := ctx.Value(userIDKey).(int)
	return id, ok
}

Kalit uchun eksport qilinmagan (unexported) maxsus tipdan (contextKey, oddiy string emas) foydalanish boshqa paketlardagi kalitlar bilan to'qnashuvning oldini oladi. Agar ikkita turli paket "userID" oddiy satrini kalit sifatida ishlatsa, ular bir-birining ma'lumotini bilmasdan ustiga yozib yuborishi mumkin. Eksport qilinmagan xususiy tiplar esa ularning asosiy qiymatlari bir xil bo'lsa ham, hech qachon bir-biri bilan to'qnashmaydi.

Biroq bu mexanizmni suiiste'mol qilmaslik kerak. Context qiymatlari Go tilining statik tip tizimini chetlab o'tadi (chunki qiymatni olish doimo ish vaqtida bajariladigan type assertion'ni talab qiladi) va funksiyaning nimaga bog'liqligini uning signaturasida yashirib qo'yadi. Agar funksiyaning ishlashi uchun biror ma'lumot zarur bo'lsa, uni yashirmasdan oddiy funksiya parametri sifatida uzating. Context qiymatlarini esa faqat tizimning barcha qatlamlaridan o'tuvchi ixtiyoriy, qo'shimcha metama'lumotlar uchungina saqlang.

Context bilan ishlashning oltin qoidalari

Kompilyator majburlamasa-da, Go hamjamiyatida qat'iy qabul qilingan va go vet tomonidan qo'llab-quvvatlanadigan qoidalar context'dan to'g'ri foydalanishni ta'minlaydi:

Context doim birinchi parametr bo'lishi kerak, nomi ctx deb ataladi va struct maydonida saqlanmaydi. func Process(ctx context.Context, data []byte) error ko'rinishida yoziladi — aksincha emas va hech qachon struct ichiga ctx maydoni kiritilmaydi. Context'ni struct ichida saqlash uni obyekt yaratilgan paytdagi holatiga bog'lab qo'yadi, bu esa alohida operatsiyalarning o'zgaruvchan umriga moslashishi kerak bo'lgan context falsafasiga ziddir.

example.go
// Noto'g'ri: bu context qaysi so'rovga tegishli ekani noaniq
type Service struct {
	ctx context.Context
}

// Idiomatik: context struct ichida emas, chaqiruv orqali uzatiladi
type Service struct{}

func (s *Service) Process(ctx context.Context, data []byte) error {
	// ...
}

WithCancel, WithTimeout yoki WithDeadline qaytargan cancel funksiyasini har doim chaqiring, hatto context muddati tugab ulgurgan bo'lsa ham. Yaratilgandan so'ng darhol defer cancel() qilish ortiqcha xarajat talab qilmaydi va context'ning ichki timer hamda resurslari ota-context tugaguncha behuda osilib (leak bo'lib) yotmasdan, o'z vaqtida bo'shatilishini kafolatlaydi.

Va context qiymatlarini olishda doimo xavfsiz type assertion (value, ok := ...) ishlating. ctx.Value(key).(int) shakli agar kalit topilmasa yoki tipi boshqa bo'lsa, to'g'ridan-to'g'ri panic chaqiradi; ikki natijali id, ok := ctx.Value(key).(int) uslubi esa xatolikni crash qilmasdan xavfsiz ushlab qolish imkonini beradi.

Context, mutexlar va channellar birgalikda Go dasturlarida talab etiladigan to'liq concurrency asboblar to'plamini hosil qiladi: muloqot va oqimlarni muvofiqlashtirish uchun channellar, umumiy holatni himoyalash uchun mutexlar hamda barcha qatlamlar bo'ylab bekor qilish va timeoutlarni uzatish uchun context. Keyingi bobda bu vositalarni to'g'ri bog'laydigan dizayn prinsiplariga qaraymiz: goroutine sizib chiqishining (leak) oldini olish, backpressure boshqaruvi va concurrent dasturlarni ishonchli testlash usullari.