45 / 73

Vaqt va rejalashtirish

Har bir jiddiy dastur oxir-oqibat "bu amal qancha vaqt oldi?", "bu vazifa qachon yana takrorlanishi kerak?" va "hozir soat necha, tizim soati tuzatilganda yoki boshqa vaqt mintaqasidagi serverda ham to'g'ri ishlaydigan tarzda qanday aniqlanadi?" kabi savollarga javob berishi kerak. Goning time paketi bu savollarning barchasiga aniq javob beradi. Biroq u buni boshqa tillardagi kabi vaqtni 1970-yildan beri o'tgan millisekundlarning oddiy butun soni deb hisoblaydigan dasturchilarni biroz chalg'itishi mumkin bo'lgan o'ziga xos dizayn qarorlari orqali amalga oshiradi. Ushbu konseptual qarorlarni tushunish shunchaki demo loyihada ishlaydigan kodni kabisa soniyalari, yozgi vaqtga o'tish (DST) yoki production tizimidagi NTP tuzatishlariga bardosh bera oladigan ishonchli koddan ajratib turadi.

time.Time — oddiy timestamp emas

Ko'plab dasturlash tillarida "joriy vaqt" — bu oddiy raqam: Unix epoch davridan beri o'tgan millisekundlar yoki soniyalar. Go tilidagi time.Time esa ancha mukammalroq. time.Time qiymati devor soati (wall clock) ko'rsatkichini, monotonik vaqtni (mavjud bo'lganda) va vaqt mintaqasini (location) o'zida saqlaydi. Unga shaffof bo'lmagan (opaque) yaxlit qiymat sifatida qarash — funksiyalarga uzatish, tayyor metodlari yordamida taqqoslash va ichki maydonlariga to'g'ridan-to'g'ri aralashmaslik — undan to'g'ri foydalanishning asosiy kalitidir.

example.go
package main

import (
	"fmt"
	"time"
)

func main() {
	now := time.Now()
	fmt.Println("now:", now)

	specific := time.Date(2023, time.January, 15, 14, 30, 0, 0, time.UTC)
	fmt.Println("specific:", specific)

	parsed, err := time.Parse(time.RFC3339, "2023-01-15T14:30:00Z")
	if err != nil {
		fmt.Println("parse error:", err)
		return
	}
	fmt.Println("parsed:", parsed)

	fromUnix := time.Unix(1673798400, 0)
	fmt.Println("from unix seconds:", fromUnix)
}

time.Date alohida e'tiborga loyiq: u yil, oy, kun, soat, daqiqa, soniya, nanosekund va *time.Location parametrlarini qabul qiladi hamda chegaradan chetga chiqqan qiymatlarni avtomatik normallashtiradi — masalan, time.Date(2023, time.January, 32, 0, 0, 0, 0, time.UTC) chaqiruvi panic berish o'rniga jimgina 1-fevralga aylanadi. Bu xususiyat sana arifmetikasi uchun ("hozirdan 45 kun keyingi sana") juda qulay, biroq kiritilgan ma'lumot xato bo'lsa ham time.Date hech qachon xatolik qaytarmasligini anglatadi; agar bu muhim bo'lsa, qiymatlarni oldindan tekshirib olish zarur.

Vaqtlarni solishtirish va tartiblash uchun ==, <, > operatorlari emas, maxsus metodlar ishlatiladi. Sababi time.Time — struct bo'lib, == operatori uning barcha maydonlarini, jumladan monotonik ko'rsatkich va joylashuv (pointer) manzilini ham birma-bir taqqoslaydi. Aynan bir lahzani ifodalovchi ikkita vaqt qiymati, agar biridan monotonik ko'rsatkich olib tashlangan bo'lsa yoki ularning *Location ko'rsatkichlari har xil bo'lsa, == bilan teng emas deb chiqishi mumkin. Shu sababli doimo metodlardan foydalaning:

example.go
now := time.Now()
later := now.Add(time.Hour)

now.Before(later)   // true
later.After(now)    // true
now.Equal(now)      // true — Equal xotiradagi ko'rinishni emas, vaqt lahzasini taqqoslaydi

Equal — vaqtlarni tenglikka tekshirishda == o'rniga doimo ishlatilishi shart bo'lgan metoddir.

Formatlash: format kodlari emas, reference vaqt

Aksariyat dasturlash tillari sanalarni %Y-%m-%d yoki yyyy-MM-dd kabi format kodlari bilan formatlaydi. Go esa boshqacha yo'l tutgan va bu time paketining eng ko'p muhokama qilinadigan qismidir: siz vaqt formatini qanday ko'rinishda xohlasangiz, aynan bitta muayyan reference (namuna) lahzadan andoza sifatida foydalanib yozasiz.

Bu reference lahza: Mon Jan 2 15:04:05 MST 2006 bo'lib, u mutlaqo tasodifiy emas. Uni 01/02 03:04:05PM '06 -0700 ko'rinishida yozganda 1 dan 7 gacha bo'lgan raqamlar qat'iy tartibda keladi (oy, kun, soat, daqiqa, soniya, yil, vaqt mintaqasi). Go jamoasi eslab qolish oson bo'lishi uchun aynan shu namunani tanlagan. Ushbu reference vaqtni qaysi ko'rinishda yozsangiz, u sizning layout (qolip) satringizga aylanadi:

example.go
now := time.Now()

fmt.Println(now.Format(time.RFC3339))          // 2023-01-15T14:30:00Z
fmt.Println(now.Format(time.Kitchen))           // 2:30PM
fmt.Println(now.Format("2006-01-02 15:04:05"))  // 2023-01-15 14:30:00
fmt.Println(now.Format("Jan 2, 2006"))          // Jan 15, 2023

Bu yerda yodlab olish kerak bo'lgan alohida format spetsifikatorlari (%Y, %m, %d) mini-tili yo'q: chiqish shaklini har doim o'sha bitta namunaviy sanani kerakli shaklda yozish orqali ifodalaysiz. Bunga bir oz ko'nikkaningizdan so'ng, u eng qulay usulga aylanadi: layout satriga qarab, natija qanday ko'rinishda chiqishini darhol tushunish mumkin. Standart kutubxonada keng tarqalgan formatlar oldindan konstantalar sifatida belgilab qo'yilgan (time.RFC3339, time.RFC1123, time.Kitchen, Go 1.20 dan time.DateOnly va time.TimeOnly).

Sanalarni tahlil qilish (parsing) ham xuddi shu qolip bo'yicha teskari yo'nalishda ishlaydi va bu yerda vaqt mintaqasining nozik jihati yuzaga chiqadi:

example.go
// time.Parse qolipda zona ma'lumoti bo'lmaganda uni UTC deb hisoblaydi —
// u kompyuterning lokal vaqt mintaqasidan FOYDALANMAYDI.
t1, _ := time.Parse("2006-01-02", "2023-01-15")

// Agar kiruvchi ma'lumot aniq bir mintaqaga (masalan New York) tegishli bo'lsa,
// buni ParseInLocation bilan aniq ko'rsatish lozim.
loc, _ := time.LoadLocation("America/New_York")
t2, _ := time.ParseInLocation("2006-01-02 15:04:05", "2023-01-15 14:30:00", loc)

Siljishsiz (offset'siz) sana satri noaniq bo'lib, time.Parse bu noaniqlikka "mashinaning lokal zonasini ol" deb emas, "sukut bo'yicha UTC deb hisobla" deb javob beradi. Bu kod qayerda ishlashidan qat'i nazar deterministik (bir xil) natija berishini ta'minlaydi. Agar vaqt ma'lum bir mintaqaga tegishli bo'lsa, uni tahlil qilishda albatta ParseInLocationdan foydalanish zarur.

Oddiy offset emas, to'liq Location

time.Location qat'iy belgilangan UTC siljishidan (offset) ancha ko'proq ma'lumotga ega: masalan, America/New_York yozgi vaqtga o'tish qoidalarini (DST), mintaqa tarixidagi o'zgarishlarni va kabisa soniyalarini to'liq biladi; oddiygina -05:00 offseti esa bularni bilmaydi.

example.go
utc := time.Now().In(time.UTC)
ny, _ := time.LoadLocation("America/New_York")
tokyo, _ := time.LoadLocation("Asia/Tokyo")

fmt.Println(utc.In(ny))
fmt.Println(utc.In(tokyo))

// Qat'iy offset faqat yozgi vaqtga o'tmaydigan, o'zgarmas zonalarda ishlatiladi
fixed := time.FixedZone("shiptime", -6*3600)

time.LoadLocation IANA vaqt zonalari ma'lumotlar bazasidan o'qiydi. U Linux va macOS tizimlarida operatsion tizim tarkibida bo'ladi; agar time/tzdata paketini import qilsangiz, zona ma'lumotlari Go binar faylining o'ziga joylashtiriladi (bu /usr/share/zoneinfo mavjud bo'lmagan minimal scratch Docker konteynerlar uchun juda qo'l keladi).

Sanalarni to'g'ri boshqarishning oltin qoidasi: barcha hisob-kitoblarni va saqlashni UTC formatida bajaring, foydalanuvchiga ko'rsatish kerak bo'lgan so'nggi nuqtada esa uning lokal zonasiga o'giring. Ma'lumotlar bazasiga time.Now().UTC() yoziladi, ekranga esa storedTime.In(userLocation) orqali chiqariladi. Bir xil tizimda lokal vaqt bilan UTC vaqtini aralashtirib yuborish — yiliga ikki marta (DST o'zgarishida) kutilmagan soatlik tafovutlarni keltirib chiqaruvchi eng xavfli xatodir.

Monotonik vaqt: nima uchun Sub va Since xavfsiz?

time.Now() chaqirilganda, hosil bo'lgan time.Time bitta emas, ikkita alohida vaqt ko'rsatkichini o'zida saqlaydi: devor soati (wall clock — joriy sana va vaqt) va operatsion tizim taqdim etsa, monotonik soat (monotonic clock — faqat oldinga siljiydigan, NTP sinxronizatsiyasi, kabisa soniyalari yoki tizim vaqtini o'zgartirish ta'sir qilmaydigan hisoblagich).

example.go
start := time.Now()
time.Sleep(time.Second)
elapsed := time.Since(start) // ikkala vaqtda ham mavjud bo'lsa, monotonik o'qishdan foydalanadi

Bu nega muhim? Devor soatlari sakrashi mumkin. NTP xizmati vaqt siljishini tuzatish uchun tizim soatini orqaga surishi, virtual mashina (VM) to'xtatilib qayta yoqilganda vaqt o'zgarishi yoki foydalanuvchi soatni qo'lda o'zgartirishi mumkin. Agar amallar davomiyligini devor soati bo'yicha ayirsangiz, soat orqaga surilgan taqdirda, amalda musbat vaqt olgan operatsiya uchun "manfiy davomiylik" natijasi chiqishi mumkin. Monotonik soatda esa bu xato yuz bermaydi, chunki u faqat bitta yo'nalishda — oldinga qarab yuguradi.

time.Since(t) va t2.Sub(t1) agar ikkala vaqtda ham monotonik ko'rsatkich bo'lsa, undan avtomatik foydalanadi. Monotonik ko'rsatkich faqat devor soati talab qilinadigan amallarda — .Format chaqirilganda, JSON'ga aylantirilganda, ma'lumotlar bazasiga yozilganda yoki .Round(0) chaqirilganda olib tashlanadi (chunki monotonik vaqt faqat bitta kompyuter xotirasidagi o'lchovlar uchungina ma'noga ega). Shu bois bazadan o'qib olingan vaqtni time.Now() bilan ayirish monotonik emas, devor soati ayirmasiga aylanadi.

Timer'lar, ticker'lar va ularni to'g'ri tozalash

time.Timer bitta muddat o'tgach bir marta ishga tushadi; time.Ticker esa belgilangan interval bilan doimiy takrorlanadi. Ikkalasi ham Go channellari orqali xabar beradi, bu esa ularni select operatori bilan birga ishlatish uchun ideal qiladi.

example.go
timer := time.NewTimer(2 * time.Second)
<-timer.C
fmt.Println("fired")

Eng ko'p yo'l qo'yiladigan xato — resurslarni tozalashni unutishdir. Ishlatilib, lekin Stop() qilinmagan Timer yoki Ticker xotira sizib chiqishiga (leak) olib keladi: Go runtime tizimi uni xotirada saqlab turadi, ticker esa hech kim o'qimayotgan channelga tinimsiz ma'lumot yuborishda davom etadi. Shu sababli, ticker yaratilishi bilanoq defer ticker.Stop() qilish qat'iy tavsiya etiladi:

example.go
ticker := time.NewTicker(time.Second)
defer ticker.Stop() // ticker'ni to'xtatish — Stop channel'ni yopmaydi

for i := 0; i < 5; i++ {
	t := <-ticker.C
	fmt.Printf("tick %d at %v\n", i+1, t)
}

Go 1.23 versiyasidan oldin Timerni qayta sozlash (Reset) ancha nozik masala edi, chunki uning channeli 1 o'lchamli buferga ega bo'lib, eski ishga tushish qiymati buferda qolib ketishi mumkin edi. Agar Stop false qaytarsa (ya'ni timer allaqachon ishga tushgan bo'lsa), buferda eskirgan qiymat turishi mumkin edi va uni bo'shatmasdan Reset chaqirilsa, keyingi <-timer.C yangi qiymatni kutish o'rniga darhol eski vaqtni qaytarardi. Shu sababli eski kodda quyidagi "bo'shatib yuborish" (drain) andozasi keng tarqalgan:

example.go
timer := time.NewTimer(5 * time.Second)
// ...endi bu timer kerak emasligiga qaror qildik...
if !timer.Stop() {
	<-timer.C // faqat Stop timer allaqachon ishga tushganini bildirsa bo'shatiladi — 1.23 gacha bo'lgan andoza
}

Go 1.23 versiyasida bu muammo butunlay bartaraf etildi: go 1.23 va undan keyingi versiyalarda Timer va Ticker channellari to'liq buferlanmagan (sig'imi 0) bo'ldi va runtime tizimi Stop/Reset chaqirilganda hech qanday eski qiymat qolmasligini kafolatlaydi. Endilikda Timer.Reset va Timer.Stop metodlarini istalgan paytda ortiqcha xavfsizlik amallarini bajarmasdan to'g'ridan-to'g'ri chaqirish mumkin.

Bu o'zgarishning ko'zga tashlanadigan yon ta'siri bor: len(timer.C) va cap(timer.C) endi 1 emas, 0 qaytaradi — shuning uchun bloklanmaydigan qabul qilish o'rniga channel uzunligini tekshiradigan kod ishlamay qoladi. Eski semantikani vaqtincha qaytarish uchun mavjud bo'lgan GODEBUG=asynctimerchan=1 yo'li esa Go 1.27 da butunlay olib tashlandi: timer channellari endi modulning go direktivasidan qat'i nazar, har doim buferlanmagan. Yuqoridagi drain andozasi bilan yozilgan kodni uchratsangiz, u hozirgi toolchain'da xato emas — shunchaki keraksiz. Ammo hamon go 1.22 yoki undan eski versiyani e'lon qilgan modulni qo'llab-quvvatlayotgan bo'lsangiz, bu direktiva ko'tarilmaguncha eski, buferli va xavfliroq xatti-harakat kuchda qolishini yodda tuting.

time.After(d) — select operatorlarida timeout belgilash uchun juda qulay bir martalik funksiya, biroq u har bir chaqiruvda yangi timer yaratadi va u muddati yetguncha xotiradan o'chirilmaydi. Uni tezkor sikllar ichida ishlatish timerlarning xotirada to'planib qolishiga olib keladi. Qayta-qayta takrorlanuvchi jarayonlar uchun bitta Timer yaratib, uni qayta sozlash (Reset) to'g'riroqdir.

example.go
select {
case <-time.After(2 * time.Second):
	fmt.Println("timed out")
case result := <-resultChan:
	fmt.Println("got", result)
}

time.AfterFunc berilgan kechikishdan keyin funksiyani alohida goroutineda ishga tushiradi. Bu "ishga tushir va unut" uslubidagi rejalashtirish uchun qulay, ammo o'sha funksiyadagi barcha xavfsizlik va sinxronizatsiya uchun javobgarlik dasturchi zimmasida bo'ladi.

Bekor qilinadigan operatsiyalarda timer o'rniga context ishlatish

Ilova kodida "ma'lum vaqtdan so'ng amalni to'xtatish" uchun odatda xom timerlardan ko'ra context.WithTimeout ancha afzalroq vositadir. Chunki u butun tizim bo'ylab tarqaluvchi bekor qilish signallari bilan mukammal integratsiyalashadi:

example.go
func doWork(ctx context.Context) error {
	select {
	case <-time.After(5 * time.Second):
		return nil
	case <-ctx.Done():
		return ctx.Err()
	}
}

func caller() error {
	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
	defer cancel()
	return doWork(ctx)
}

Oddiy time.After chaqiruvchini muddatidan oldin to'xtatish yoki bekor qilish sababini bilish imkonini bermaydi. context.WithTimeout esa ikkala imkoniyatni ham beradi hamda ctx.Err() orqali "vaqt tugadimi" yoki "qo'lda bekor qilindimi" holatini aniq ajratib beradi.

Amaliy rejalashtirish andozalari

Tashqi murakkab kutubxonalarni ishlatmasdan, oddiy vaqt vositalari yordamida ikkita muhim andozani amalga oshirish mumkin:

Debouncing — qisqa vaqt ichida ketma-ket yuz bergan ko'plab hodisalarni bitta kechiktirilgan amalga jamlaydi — masalan, foydalanuvchi klaviaturada har bir harfni bosganda emas, balki yozishdan to'xtaganidan so'ng qidiruvni ishga tushirish uchun juda qulay:

example.go
type Debouncer struct {
	mu    sync.Mutex
	timer *time.Timer
	delay time.Duration
}

func (d *Debouncer) Trigger(fn func()) {
	d.mu.Lock()
	defer d.mu.Unlock()
	if d.timer != nil {
		d.timer.Stop()
	}
	d.timer = time.AfterFunc(d.delay, fn)
}

Rate limiting — amallarning qanchalik tez-tez bajarilishiga ruxsat berilishini cheklash. Bitta oqim uchun oddiy hisoblagich yetarli, biroq ko'p oqimli concurrent muhitda yoki token-bucket algoritmi kerak bo'lganda standart golang.org/x/time/rate paketidan foydalanish tavsiya etiladi:

example.go
type RateLimiter struct {
	mu       sync.Mutex
	interval time.Duration
	last     time.Time
}

func (rl *RateLimiter) Allow() bool {
	rl.mu.Lock()
	defer rl.mu.Unlock()
	now := time.Now()
	if now.Sub(rl.last) >= rl.interval {
		rl.last = now
		return true
	}
	return false
}

time.Duration: tushunarli arifmetika va yaxlitlash

time.Duration — nanosekundlarni ifodalovchi nomlangan int64 turi bo'lib, aynan shu bois 5 * time.Second yoki time.Hour / 2 kabi amallar kutilganidek aniq ishlaydi: bu maxsus tipga o'ralgan oddiy butun son arifmetikasidir. Uni inson o'qiy oladigan birliklarga o'tkazish uchun tayyor metodlar mavjud:

example.go
d := 90 * time.Minute
fmt.Println(d.Hours())             // 1.5
fmt.Println(d.String())            // "1h30m0s"
fmt.Println(d.Round(time.Hour))    // 2h0m0s — eng yaqiniga yaxlitlaydi
fmt.Println(d.Truncate(time.Hour)) // 1h0m0s — doim pastga yaxlitlaydi

time.ParseDuration funksiyasi String() metodiga mos ixcham satrlarni tahlil qiladi ("1h30m", "2.5s", "500ms"), bu konfiguratsiya fayllari va CLI flaglarini boshqarishda juda qulaydir. flag.Duration ham ichida aynan shuni ishlatadi, shuning uchun buyruq qatoridagi --timeout=30s sizdan hech qanday tahlil kodi yozishni talab qilmasdan, o'zi ishlab ketadi.

Go tilida vaqt bilan ishlashda tayyor vositalardan — Since, LoadLocation, reference vaqt orqali Formatlash — foydalanish eng to'g'ri yo'ldir. Bu vositalar siz hali o'ylab ham ko'rmagan ko'plab nozik chekka holatlarni (edge cases) allaqachon to'g'ri hal qilib qo'ygan.