48 / 73

HTTP va Networking

net/http paketi Go tilining zamonaviy backend xizmatlari uchun birinchi raqamli tanlovga aylanishining asosiy sabablaridan biridir: production talablariga to'liq javob beruvchi HTTP client va server, jumladan connection pooling (ulanishlar puli), keep-alive, TLS va HTTP/2 protokollari standart kutubxonaning o'zida hech qanday tashqi kutubxonalarsiz tayyor holda keladi. Uning ustiga qurilgan uchinchi tomon freymvorklari (Gin, Echo, Chi) asosan qulayroq marshrutlash (routing) imkoniyatlarini qo'shadi, biroq ularning barchasi ostida baribir standart net/http poydevori yotadi. Shu sababli standart kutubxona arxitekturasini — Handler, Client va middleware'lar qanday tuzilishini — chuqur tushunish har qanday freymvorkda ishlaganda ham o'zini to'liq oqlaydi.

So'rov yuborish: http.Get'dan sozlangan Client'gacha

http.Get — so'rov yuborishning eng qisqa yo'li bo'lib, oddiy skriptlar va bir martalik chaqiruvlar uchun juda qulay. Ammo u standart http.DefaultClientdan foydalanadi, unda esa hech qanday timeout o'rnatilmagan: agar narigi tomondagi server qotib qolsa yoki sekin ishlasa, sizning goroutine'ingiz hech qachon to'xtamaydigan cheksiz bloklanish holatida qolib ketadi.

example.go
resp, err := http.Get("https://api.example.com/data")
if err != nil {
	return err
}
defer resp.Body.Close() // majburiy, hatto tanani o'qimasangiz ham — ulanishni bo'shatadi

Bu yerdagi defer resp.Body.Close() shunchaki ixtiyoriy tozalash emas: agar javob tanasi yopilmasa, TCP ulanishi qayta ishlatish uchun mijozning connection pool'iga qaytarilmaydi. Yuqori yuklama ostida bu xato ochiq crash sifatida emas, balki serverda ulanishlar yetishmovchiligi (connection exhaustion / pool starvation) sifatida namoyon bo'ladi.

Production kodlarida har doim aniq timeout'ga ega alohida http.Client yarating va so'rovlarni NewRequestWithContext orqali yuboring:

example.go
var client = &http.Client{
	Timeout: 10 * time.Second,
}

req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
	return err
}
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Accept", "application/json")

resp, err := client.Do(req)
if err != nil {
	return err
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
	return fmt.Errorf("unexpected status: %s", resp.Status)
}

NewRequestWithContext funksiyasi (eskiroq http.NewRequest dan afzal) chaqiruvchi uzatgan context.Context orqali so'rovni muddatidan oldin bekor qilish imkonini beradi. Usiz yagona bekor qilish mexanizmingiz Client.Timeout bo'lib qoladi, u esa butun mijozga birdek tegishli — har bir chaqiruv uchun alohida uzaytirib yoki qisqartirib bo'lmaydi, context'dan olingan deadline esa buni bemalol qiladi. Har bir so'rov uchun yangidan http.Client ochmasdan, bitta mijoz instansiyasini butun dastur bo'ylab qayta-qayta ishlatish kerak: yangi Client (yoki yangi Transport) har safar bo'sh connection pool degani bo'lib, avvalgi keep-alive ulanishlarining bekor ketishiga sabab bo'ladi.

JSON tanali POST so'rovi ham xuddi shu shaklda yoziladi, faqat body reader va Content-Type sarlavhasi qo'shiladi:

example.go
body, err := json.Marshal(user)
if err != nil {
	return err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
if err != nil {
	return err
}
req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req)

So'rovlarga xizmat ko'rsatish: Handler — yagona abstraksiya

Go serverining yuragi bitta oddiy interfeysdan iborat:

example.go
type Handler interface {
	ServeHTTP(w http.ResponseWriter, r *http.Request)
}

http.HandlerFunc adapteri oddiy funksiyalarni ushbu interfeys talablariga moslashtirish imkonini beradi. Shu sababli serverga oddiy funksiyalarni ham, ServeHTTP metodiga ega strukturalarni ham handler sifatida ulash mumkin:

example.go
mux := http.NewServeMux()
mux.HandleFunc("GET /users/{id}", getUserHandler)   // Go 1.22+: metod + yo'l shabloni
mux.HandleFunc("POST /users", createUserHandler)

server := &http.Server{
	Addr:         ":8080",
	Handler:      mux,
	ReadTimeout:  15 * time.Second,
	WriteTimeout: 15 * time.Second,
	IdleTimeout:  60 * time.Second,
}
log.Fatal(server.ListenAndServe())

Metod va yo'l parametrlarini o'z ichiga olgan marshrutlash sintaksisi ("GET /users/{id}") Go 1.22 versiyasida http.ServeMuxga qo'shildi. Bungacha standart mux faqat yo'l prefiksi bo'yicha taqqoslay olardi va HTTP metodlarini handler ichida switch r.Method orqali qo'lda tekshirishga to'g'ri kelardi. Yangi standart mux juda ko'p vaziyatlarda tashqi router kutubxonalariga bo'lgan ehtiyojni yo'qotadi. (Marshrut guruhlari yoki tipli path-parametr ajratish kerak bo'lsa, baribir alohida router paketi qulayroq bo'lishi mumkin.) Testlar yoki loglarda redirectlarni tekshirsangiz, bir nozik o'zgarishni bilib qo'ying: Go 1.26 dan boshlab ServeMux ning avtomatik trailing-slash redirectlari eski 301 (Moved Permanently) o'rniga 307 (Temporary Redirect) kodini ishlatadi — 307 so'rov metodi va tanasini saqlab qoladi, 301 esa buni barcha mijozlarda kafolatlamaydi.

Path parametrlarini o'qish va JSON javob qaytarish quyidagicha amalga oshiriladi:

example.go
func getUserHandler(w http.ResponseWriter, r *http.Request) {
	id := r.PathValue("id") // Go 1.22+
	user, ok := store.Get(id)
	if !ok {
		http.Error(w, "user not found", http.StatusNotFound)
		return
	}
	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(user)
}

Server timeout'larini (ReadTimeout, WriteTimeout, IdleTimeout) aniq belgilash shart: http.ListenAndServe ishlatadigan nol qiymatli standartlarda qoldirilsa, read timeout o'rnatilmagan server sekin mijoz (slowloris) hujumlariga zaif bo'ladi (bir necha soniyada bir baytdan yuboradigan ulanishlar goroutinelarni va server xotirasini cheksiz band qilib qo'yadi).

http.Server dagi shu "zerikarli, ammo har doim qiling" ro'yxatiga yana ikkita maydon kiradi. MaxHeaderValueCount (Go 1.27) bitta header nomi ostida kelishi mumkin bo'lgan qiymatlar sonini cheklaydi va mijoz bir xil headerni minglab marta yuboradigan resurs tugatish holatini yopadi. HTTP/2 ni maxsus sozlayotgan bo'lsangiz, DisableClientPriority (u ham Go 1.27) server sukut bo'yicha hurmat qiladigan RFC 9218 mijoz-ustuvorlik signallaridan voz kechish imkonini beradi — noto'g'ri ishlaydigan mijozning ustuvorlik ishoralari foydadan ko'ra zarar keltiradigan kam uchraydigan holatlar uchun.

Manzil satrini shakllantirishda fmt.Sprintf("%s:%d", host, port) o'rniga har doim net.JoinHostPort(host, port) funksiyasidan foydalaning. Sprintf IPv6 manzillarda xato beradi, chunki IPv6 formatida manzil kvadrat qavs ichiga olinishi shart ([::1]:8080).

Middleware: Handlerni Handler bilan o'rash

Handler bitta metodli interfeys bo'lgani sababli, Go tilida "middleware" — bu bitta Handlerni qabul qilib, uning atrofida qo'shimcha vazifalarni bajaruvchi yangi Handler qaytaradigan oddiy funksiyadir:

example.go
func WithLogging(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		start := time.Now()
		next.ServeHTTP(w, r)
		log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start))
	})
}

Middleware'larni zanjir qilib birlashtirish funksiyalarning oddiy kompozitsiyasidan iborat:

example.go
func Chain(h http.Handler, mw ...func(http.Handler) http.Handler) http.Handler {
	for i := len(mw) - 1; i >= 0; i-- {
		h = mw[i](h)
	}
	return h
}

handler := Chain(baseHandler, WithRecovery, WithLogging, WithAuth)

Tartib juda muhim: Chain ro'yxatdagi oxirgi middleware'ni eng ichkariga qo'llaydi, shuning uchun so'rov avval WithRecoverydan, keyin WithLoggingdan, so'ngra WithAuthdan o'tib, asosiy baseHandlerga yetib boradi. Shunday qilib, handler ichidagi yoki logging/auth paytidagi har qanday panic eng tashqi WithRecovery tomonidan xavfsiz ushlab qolinadi:

example.go
func WithRecovery(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		defer func() {
			if err := recover(); err != nil {
				log.Printf("panic recovered: %v", err)
				http.Error(w, "internal server error", http.StatusInternalServerError)
			}
		}()
		next.ServeHTTP(w, r)
	})
}

So'rov doirasidagi ma'lumotlarni (masalan, autentifikatsiyadan o'tgan foydalanuvchi ID'sini) middleware'dan handler'ga uzatish r.Context() orqali amalga oshiriladi. Kalit sifatida oddiy string ishlatish xavfli to'qnashuvlarga olib kelishi mumkin; shuning uchun eksport qilinmagan xususiy tiplardan foydalaniladi:

example.go
type ctxKey int

const userIDKey ctxKey = iota

func WithAuth(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		token, ok := bearerToken(r)
		if !ok || !isValid(token) {
			http.Error(w, "unauthorized", http.StatusUnauthorized)
			return
		}
		ctx := context.WithValue(r.Context(), userIDKey, userIDFor(token))
		next.ServeHTTP(w, r.WithContext(ctx))
	})
}

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

Kalitni UserIDFromContext kabi accessor funksiya bilan juftlash muhim: aks holda har bir handler ctx.Value(userIDKey) ni chaqirib, o'zi type assertion qilishga majbur bo'ladi. Accessor esa type assertion'ni bitta joyda ushlab turadi va keyinchalik ichki tasvirni o'zgartirmoqchi bo'lsangiz, har bir chaqiruv joyiga tegmasdan o'zgartirish imkonini beradi.

So'rov tanasini xavfsiz qayta ishlash

So'rov tanasini o'qishda hajmni cheklash shart, aks holda buzg'unchi server xotirasini to'ldirib yuborishi mumkin. Buning uchun http.MaxBytesReader ishlatiladi:

example.go
func createUserHandler(w http.ResponseWriter, r *http.Request) {
	r.Body = http.MaxBytesReader(w, r.Body, 1<<20) // 1MB dan katta tanalarni rad etish

	var req CreateUserRequest
	dec := json.NewDecoder(r.Body)
	dec.DisallowUnknownFields()
	if err := dec.Decode(&req); err != nil {
		http.Error(w, "invalid request body", http.StatusBadRequest)
		return
	}
	// ...
}

http.MaxBytesReader — bu shunchaki boshqa nom ostidagi io.LimitReader emas: u chegara oshib ketganda o'qishni to'xtatibgina qolmay, ulanishning saqlanib qolmasdan yopilishini ham ta'minlaydi. Bu muhim, chunki qayta ishlatilishi kutilayotgan ulanishga chegaradan oshiq ma'lumot yuborishda davom etayotgan mijoz o'sha ulanishni chalkash holatda qoldirishi mumkin.

CSRF (Cross-Site Request Forgery) himoyasi

Cookie orqali autentifikatsiya qilinadigan endpointlar albatta CSRF himoyasiga muhtoj. Go 1.25 versiyasida standart kutubxonaga net/http.CrossOriginProtection middleware'i qo'shildi. U brauzerlar avtomatik yuboradigan Sec-Fetch-Site header'i asosida begona saytlardan kelgan holatni o'zgartiruvchi so'rovlarni avtomatik bloklaydi:

example.go
protection := http.NewCrossOriginProtection()
protection.AddTrustedOrigin("https://app.example.com")

handler := protection.Handler(mux)
server := &http.Server{Addr: ":8080", Handler: handler}

Graceful shutdown (Serverni xavfsiz to'xtatish)

Serverni darhol o'chirib qo'yish bajarilayotgan so'rovlarning uzilib qolishiga olib keladi. Server.Shutdown yangi so'rovlar qabul qilishni to'xtatadi va mavjud barcha faol ulanishlar o'z ishini tugatguncha (context timeout muddati doirasida) sabr bilan kutadi:

example.go
server := &http.Server{Addr: ":8080", Handler: mux}

go func() {
	if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
		log.Fatalf("server error: %v", err)
	}
}()

quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit

ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
	log.Printf("forced shutdown: %v", err)
}

errors.Is(err, http.ErrServerClosed) shartini albatta qo'ying: server toza to'xtatilganda ListenAndServe aynan shu xatolikni qaytaradi va uni fojiali nosozlik deb hisoblamaslik kerak.

Reverse Proxy va xavfsizlik

Serveringiz Load Balancer yoki Nginx kabi reverse proxy ortida turganda, X-Forwarded-For kabi headerlarga ko'r-ko'rona ishonmang, chunki tajovuzkor o'zi bu sarlavhani soxtalashtirishi mumkin. r.Header ichidagi har qanday ma'lumotni tashqi foydalanuvchi nazorat qilishi mumkin bo'lgan shubhali kiruvchi ma'lumot sifatida qabul qiling.

Agar Go servisingizning o'zi reverse proxy vazifasini bajarsa, Go 1.26 dan boshlab eskirgan Director o'rniga zamonaviy Rewrite metodidan foydalaning:

example.go
proxy := &httputil.ReverseProxy{
	Rewrite: func(pr *httputil.ProxyRequest) {
		pr.SetURL(target)
		pr.SetXForwarded() // X-Forwarded-For/Host/Proto sarlavhalarini to'g'ri o'rnatadi
	},
}

Tarmoq so'rovlari va server arxitekturasini ko'rib chiqdik. Keyingi bobda tizim faoliyatini to'liq nazorat qilish imkonini beruvchi logging va observability vositalarini (log/slog, metrikalar va tracing) tahlil qilamiz.