Tarkibga o'tish

Context bilan ishlash

Foydalanuvchi sahifani ochdi. Server bazaga og‘ir so‘rov yubordi va u 30 soniya davom etadi. Foydalanuvchi 3 soniya kutdi-da, sahifani yopdi. Endi hech kim natijani kutmayapti, lekin baza 27 soniya davomida bekorga ishlashda davom etadi. Shu vaqt ichida ulanish band, CPU ham band.

Yuzlab foydalanuvchi shunday qilsa, ulanishlar puli tugaydi va server yangi so‘rovlarga javob bermay qo‘yadi.

context.Context bu muammoni hal qiladi. U HTTP so‘rovdan boshlab database so‘roviga qadar "ish bekor qilindi" signalini yetkazadi. Context haqidagi asosiy ma’lumotlar context asoslari darsida berilgan. Bu darsda uni API va database bilan ishlashda qo‘llaymiz.

So‘rovning contexti

Har bir HTTP so‘rovda tayyor context bor:

ctx := r.Context()

Bu context uch holatda bekor bo‘ladi:

  1. klient ulanishni uzsa: sahifani yopsa, internet uzilsa yoki curlni Ctrl+C bilan to‘xtatsa;
  2. HTTP/2 da klient shu so‘rovni bekor qilsa;
  3. handler ishini tugatsa.

Bu contextni bazagacha uzatsak, klient ketgan zahoti database so‘rovi ham to‘xtaydi.

Contextni uzatish zanjiri

HTTP so‘rov
   │  r.Context()
   ↓
Handler      ─── ctx ───→  Service  ─── ctx ───→  Repository
                                                      │
                                                      ↓
                                     pool.Query(ctx, ...)  ← shu yerda ishlatiladi

Qoida oddiy: context qabul qiladigan har bir funksiya uni birinchi parametr sifatida oladi va chaqirgan funksiyalarga uzatadi.

// Handler
func (h *Handler) getBook(w http.ResponseWriter, r *http.Request) {
    b, err := h.books.Get(r.Context(), id)
    // ...
}

// Service
func (s *Service) Get(ctx context.Context, id int) (Book, error) {
    return s.repo.Get(ctx, id)
}

// Repository
func (r *PostgresBookRepo) Get(ctx context.Context, id int) (book.Book, error) {
    row := r.pool.QueryRow(ctx, `SELECT ... WHERE id = $1`, id)
    // ...
}

Warning

Handler ichida context.Background() yoki context.TODO() yaratib, bazaga uzatmang. Bunday context hech qachon bekor bo‘lmaydi. Natijada klient allaqachon ketgan bo‘lsa ham, database ishlashda davom etadi.

Klient uzilganda nima bo‘ladi?

Quyidagi dastur bazada 10 soniya davom etadigan so‘rovni ishga tushiradi. pg_sleep — PostgreSQL’da kutish funksiyasi. U og‘ir so‘rovni taqlid qilish uchun ishlatiladi.

main.go
package main

import (
    "context"
    "errors"
    "fmt"
    "log"
    "net/http"
    "os"
    "time"

    "github.com/jackc/pgx/v5/pgxpool"
)

func main() {
    pool, err := pgxpool.New(context.Background(), os.Getenv("DATABASE_URL"))
    if err != nil {
        log.Fatal(err)
    }
    defer pool.Close()

    http.HandleFunc("GET /report", func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()

        _, err := pool.Exec(r.Context(), `SELECT pg_sleep(10)`)
        if errors.Is(err, context.Canceled) {
            log.Printf("klient ketdi, so‘rov %v dan keyin to‘xtatildi", time.Since(start).Round(time.Millisecond))
            return
        }
        if err != nil {
            log.Printf("baza xatosi: %v", err)
            http.Error(w, "ichki xato", http.StatusInternalServerError)
            return
        }

        fmt.Fprintln(w, "Hisobot tayyor")
    })

    log.Println("Server: http://localhost:8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}
curl http://localhost:8080/report
# 2 soniyadan keyin Ctrl+C bosing

Server logida:

2026/09/30 10:00:02 klient ketdi, so‘rov 2.004s dan keyin to‘xtatildi

Klient uzilgan zahoti context bekor qilindi. pgx bu signalni olib, PostgreSQL’ga so‘rovni bekor qilish buyrug‘ini yubordi. Baza 8 soniyani bekorga sarflamadi.

Klient o‘zi ketgani uchun unga javob yozishning ma’nosi yo‘q. Shuning uchun handler shunchaki return qiladi. Bu holatni 500 xatosi sifatida log qilmang, u server xatosi emas.

Timeout qo‘yish

Klient ulanishni uzmasa ham, so‘rov cheksiz davom etmasligi kerak. Masalan, bazada qulf (lock) tufayli so‘rov osilib qolishi mumkin. Buning uchun context.WithTimeout ishlatiladi:

ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second)
defer cancel()

rows, err := pool.Query(ctx, `SELECT ...`)
if errors.Is(err, context.DeadlineExceeded) {
    http.Error(w, "so‘rov juda uzoq davom etdi", http.StatusGatewayTimeout)
    return
}

Muhim jihatlar:

  • Yangi context r.Context()dan yaratildi, context.Background()dan emas. Shunda ikkala shart ham ishlaydi: 3 soniya o‘tsa ham, klient ketsa ham so‘rov to‘xtaydi.
  • defer cancel() majburiy. U timeout uchun ajratilgan resurslarni vaqt tugashini kutmasdan bo‘shatadi. go vet cancel chaqirilmaganini aniqlab, ogohlantirish beradi.
  • Timeout qayerda qo‘yilishi haqida kelishib oling. Ko‘pincha u repository’da har bir so‘rov uchun alohida yoki middleware’da butun so‘rov uchun qo‘yiladi.

Repository ichida timeout

const queryTimeout = 3 * time.Second

func (r *PostgresBookRepo) List(ctx context.Context) ([]book.Book, error) {
    ctx, cancel := context.WithTimeout(ctx, queryTimeout)
    defer cancel()

    rows, err := r.pool.Query(ctx, `SELECT ... FROM books ORDER BY id`)
    // ...
}

Tashqi context’da muddat allaqachon qisqaroq bo‘lsa, masalan 1 soniya qolgan bo‘lsa, yangi context ham 1 soniyada tugaydi. Ichki context tashqisidan uzoq yashay olmaydi.

Butun so‘rov uchun timeout middleware

func Timeout(d time.Duration) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            ctx, cancel := context.WithTimeout(r.Context(), d)
            defer cancel()
            next.ServeHTTP(w, r.WithContext(ctx))
        })
    }
}

r.WithContext(ctx) yangi context bilan so‘rov nusxasini yaratadi. Keyingi barcha handlerlar va ular chaqirgan funksiyalar shu muddatli contextni oladi.

Xatolarni to‘g‘ri tasniflash

Context bilan bog‘liq xatolar ikki xil:

Xato Sababi Javob
context.Canceled Klient ketdi yoki ish bekor qilindi Javob yozmaslik mumkin, 500 sifatida log qilinmaydi
context.DeadlineExceeded Belgilangan vaqt tugadi 503 Service Unavailable yoki 504 Gateway Timeout

Drayverlar bu xatolarni o‘z xatolariga o‘rab qaytaradi. Shuning uchun == bilan emas, errors.Is bilan tekshiriladi.

switch {
case errors.Is(err, context.Canceled):
    return // klient ketdi
case errors.Is(err, context.DeadlineExceeded):
    writeError(w, http.StatusServiceUnavailable, "server band, keyinroq urinib ko‘ring")
case err != nil:
    writeError(w, http.StatusInternalServerError, "ichki server xatosi")
}

Tranzaksiyalar va context

Tranzaksiya — bir nechta so‘rovni "yo hammasi bajariladi, yo hech biri" tamoyili bilan birlashtirish. Klassik misol — pul o‘tkazish:

  1. Alining hisobidan 100 000 so‘m yechiladi.
  2. Valining hisobiga 100 000 so‘m qo‘shiladi.

Birinchi qadamdan keyin server o‘chib qolsa, pul yo‘qoladi. Tranzaksiya ichida esa ikkinchi qadam bajarilmasa, birinchisi ham bekor qilinadi.

Kutubxonadan kitob olishni tranzaksiya bilan yozamiz. Kitobni berish ikki amaldan iborat: nusxalar sonini kamaytirish va berish yozuvini qo‘shish.

main.go
package main

import (
    "context"
    "errors"
    "fmt"
    "log"
    "os"
    "time"

    "github.com/jackc/pgx/v5/pgxpool"
)

var ErrOutOfStock = errors.New("kitobning bo‘sh nusxasi qolmagan")

func borrowBook(ctx context.Context, pool *pgxpool.Pool, bookID, userID int64) error {
    ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
    defer cancel()

    tx, err := pool.Begin(ctx)
    if err != nil {
        return fmt.Errorf("tranzaksiyani boshlash: %w", err)
    }
    // Commit muvaffaqiyatli bo‘lsa, Rollback hech narsa qilmaydi
    defer tx.Rollback(ctx)

    tag, err := tx.Exec(ctx,
        `UPDATE inventory SET available = available - 1
         WHERE book_id = $1 AND available > 0`, bookID)
    if err != nil {
        return fmt.Errorf("nusxani kamaytirish: %w", err)
    }
    if tag.RowsAffected() == 0 {
        return ErrOutOfStock
    }

    _, err = tx.Exec(ctx,
        `INSERT INTO loans (book_id, user_id) VALUES ($1, $2)`, bookID, userID)
    if err != nil {
        return fmt.Errorf("berish yozuvini qo‘shish: %w", err)
    }

    return tx.Commit(ctx)
}

func main() {
    ctx := context.Background()
    pool, err := pgxpool.New(ctx, os.Getenv("DATABASE_URL"))
    if err != nil {
        log.Fatal(err)
    }
    defer pool.Close()

    setup := `
        DROP TABLE IF EXISTS loans, inventory;
        CREATE TABLE inventory (book_id BIGINT PRIMARY KEY, available INT NOT NULL CHECK (available >= 0));
        CREATE TABLE loans (id BIGSERIAL PRIMARY KEY, book_id BIGINT NOT NULL, user_id BIGINT NOT NULL,
                            taken_at TIMESTAMPTZ NOT NULL DEFAULT now());
        INSERT INTO inventory VALUES (1, 1);`
    if _, err := pool.Exec(ctx, setup); err != nil {
        log.Fatal(err)
    }

    for user := int64(1); user <= 2; user++ {
        err := borrowBook(ctx, pool, 1, user)
        switch {
        case errors.Is(err, ErrOutOfStock):
            fmt.Printf("%d-foydalanuvchi: %v\n", user, err)
        case err != nil:
            log.Fatal(err)
        default:
            fmt.Printf("%d-foydalanuvchi kitobni oldi\n", user)
        }
    }

    var loans int
    pool.QueryRow(ctx, `SELECT count(*) FROM loans`).Scan(&loans)
    fmt.Println("Berilgan kitoblar:", loans)
}
1-foydalanuvchi kitobni oldi
2-foydalanuvchi: kitobning bo‘sh nusxasi qolmagan
Berilgan kitoblar: 1

Kod qanday ishlaydi:

  • pool.Begin(ctx) tranzaksiyani boshlaydi. Tranzaksiya ichidagi barcha so‘rovlar tx orqali yuboriladi, pool orqali emas.
  • defer tx.Rollback(ctx) — xavfsizlik to‘ri. Funksiya xato bilan chiqsa, tranzaksiyadagi barcha o‘zgarishlar bekor qilinadi. Commit muvaffaqiyatli bajarilgan bo‘lsa, Rollback hech narsa qilmaydi.
  • available > 0 sharti UPDATE ichida tekshirildi. Alohida SELECT bilan tekshirilganda, ikkita parallel so‘rov ikkalasi ham "bitta nusxa bor" deb o‘ylashi mumkin edi. UPDATE esa qatorni qulflaydi, shuning uchun ikkinchi so‘rov o‘zgargan qiymatni ko‘radi.
  • Context butun tranzaksiya uchun umumiy. Vaqt tugasa yoki klient ketsa, tranzaksiya avtomatik bekor qilinadi.

database/sqlda ham xuddi shu tartib: db.BeginTx(ctx, nil), tx.ExecContext(ctx, ...), tx.Commit() va defer tx.Rollback().

Warning

Tranzaksiya ichida tashqi API chaqirmang, email yubormang va uzoq hisob-kitob qilmang. Tranzaksiya ochiq turgan vaqt davomida u ulanishni band qiladi va qatorlarni qulflab turadi. Tranzaksiya imkon qadar qisqa bo‘lishi kerak.

Context’ga nimalarni joylash mumkin?

context.WithValue bilan contextga qiymat joylash mumkin. Lekin u faqat so‘rov doirasidagi metadata uchun mo‘ljallangan: request ID, autentifikatsiyadan o‘tgan foydalanuvchi, trace ID.

Funksiya ishlashi uchun zarur bo‘lgan parametrlarni, masalan bookID yoki database ulanishini, contextga joylamang. Ular funksiya imzosida ochiq ko‘rinib turishi kerak.

Xulosa

  • r.Context() klient uzilganda bekor bo‘ladi. Uni handlerdan repository’gacha birinchi parametr sifatida uzating.
  • Handler ichida context.Background() ishlatmang.
  • context.WithTimeout so‘rov davomiyligini cheklaydi. defer cancel()ni unutmang.
  • context.Canceled va context.DeadlineExceeded xatolarini errors.Is bilan tekshirib, alohida qayta ishlang.
  • Tranzaksiya Begin(ctx) bilan boshlanadi, defer Rollback bilan himoyalanadi va Commit bilan yakunlanadi.

Keyingi darsda eng xavfli zaifliklardan biri — SQL injection va undan himoyalanishni ko‘ramiz.