برمجة HTTP

الدرس 22: برمجة HTTP

🎯 تشبيه من الحياة

تخيل أنك تدير مطعماً:

حزمة net/http في Go هي "نظام إدارة المطعم" الخاص بك، وتساعدك على بناء خدمة ويب عالية الأداء ومستقرة بسرعة.


📚 المفاهيم الأساسية

المفهوم الوصف
http.Get/Post إرسال طلبات HTTP لاسترجاع موارد بعيدة
http.ListenAndServe بدء خادم HTTP والاستماع على منفذ
واجهة Handler الواجهة الأساسية التي تحدد معايير معالجة الطلبات
HandlerFunc محوّل يحول دالة عادية إلى Handler
ServeMux مُعدد طلبات HTTP (مُوجّه)
Middleware نمط لإدراج منطق شائع قبل وبعد معالجة الطلبات

📝 الصياغة الأساسية والاستخدام

1. إرسال طلبات HTTP

GO
package main

import (
	"fmt"
	"io"
	"net/http"
)

func main() {
	// إرسال طلب GET
	resp, err := http.Get("https://httpbin.org/get")
	if err != nil {
		fmt.Println("فشل الطلب:", err)
		return
	}
	defer resp.Body.Close() // 💡 يجب إغلاق جسم الاستجابة لتجنب تسرب الموارد

	// قراءة محتوى الاستجابة
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		fmt.Println("فشل القراءة:", err)
		return
	}

	fmt.Println("رمز الحالة:", resp.StatusCode)
	fmt.Println("الاستجابة:", string(body))
}
💡 نصيحة: يجب إغلاق resp.Body بعد الاستخدام بـ Close()، وإلا سيتسبب في تسرب اتصالات. استخدام defer هو أفضل ممارسة.

2. إرسال طلبات POST

GO
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
)

func main() {
	// بناء بيانات JSON
	data := map[string]string{
		"username": "gopher",
		"email":    "gopher@example.com",
	}
	jsonData, _ := json.Marshal(data)

	// إرسال طلب POST
	// 💡 المعامل الثالث هو جسم الطلب، يتطلب نوع io.Reader
	resp, err := http.Post(
		"https://httpbin.org/post",
		"application/json",
		bytes.NewBuffer(jsonData),
	)
	if err != nil {
		fmt.Println("فشل الطلب:", err)
		return
	}
	defer resp.Body.Close()

	body, _ := io.ReadAll(resp.Body)
	fmt.Println("رمز الحالة:", resp.StatusCode)
	fmt.Println("الاستجابة:", string(body))
}
💡 نصيحة: معامل Content-Type لـ http.Post مهم جداً — الخادم يعتمد عليه لتحليل صيغة جسم الطلب.

3. بدء خادم HTTP

GO
package main

import (
	"fmt"
	"net/http"
)

func main() {
	// تسجيل معالجات المسارات
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "مرحباً بكم في الصفحة الرئيسية!")
	})

	http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "Hello, Gopher!")
	})

	// بدء الخادم، الاستماع على المنفذ 8080
	// 💡 ListenAndServe يحجب حتى يتم إيقاف الخادم
	fmt.Println("الخادم يعمل على http://localhost:8080")
	err := http.ListenAndServe(":8080", nil)
	if err != nil {
		fmt.Println("فشل بدء الخادم:", err)
	}
}
💡 نصيحة: تمرير nil كمعامل ثاني يعني استخدام DefaultServeMux الافتراضي. في الإنتاج، يُوصى بإنشاء مُوجّه مخصص.

4. واجهة Handler

GO
// تعريف واجهة Handler: أي نوع ينفذ طريقة ServeHTTP هو Handler
type Handler interface {
	ServeHTTP(ResponseWriter, *Request)
}
GO
package main

import (
	"fmt"
	"net/http"
)

// نوع Handler مخصص
type GreetingHandler struct {
	Message string
}

// تنفيذ طريقة ServeHTTP لواجهة Handler
func (g GreetingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, g.Message)
}

func main() {
	// استخدام Handler مخصص
	handler := GreetingHandler{Message: "مرحباً، هذا Handler مخصص!"}
	http.Handle("/greet", handler) // 💡 ملاحظة: استخدم Handle، وليس HandleFunc

	http.ListenAndServe(":8080", nil)
}
💡 نصيحة: Handle تقبل واجهة Handler، HandleFunc تقبل دالة. كلاهما مكافئان وظيفياً، فقط شكل مختلف.

5. محوّل HandlerFunc

GO
package main

import (
	"fmt"
	"net/http"
)

func myHandler(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "Handler محوّل عبر HandlerFunc")
}

func main() {
	// HandlerFunc تحول دالة عادية إلى Handler
	// 💡أساساً هو تحويل نوع: type HandlerFunc func(ResponseWriter, *Request)
	http.Handle("/adapted", http.HandlerFunc(myHandler))

	// شكل مكافئ (أكثر استخداماً)
	http.HandleFunc("/simple", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "استخدام HandleFunc مباشرة أكثر اختصاراً")
	})

	http.ListenAndServe(":8080", nil)
}
💡 نصيحة: HandlerFunc هو محوّل نوع يسمح للدوال العادية بتحقيق واجهة Handler.


🧪 أمثلة عملية

مثال: خادم ملفات ثابتة بسيط (الصعوبة ⭐)

GO
package main

import (
	"fmt"
	"log"
	"net/http"
)

func main() {
	// تسجيل خدمة الملفات الثابتة باستخدام المُوجّه الافتراضي
	// StripPrefix يزيل البادئة "/static/" من URL
	// FileServer يوفر الوصول إلى الملفات في المجلد
	fs := http.FileServer(http.Dir("./public"))
	http.Handle("/static/", http.StripPrefix("/static/", fs))

	// معالج الصفحة الرئيسية
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path != "/" {
			http.NotFound(w, r) // 💡 معالجة 404
			return
		}
		fmt.Fprintf(w, "مرحباً بكم في موقعي!")
	})

	fmt.Println("الخادم يعمل على http://localhost:8080")
	log.Fatal(http.ListenAndServe(":8080", nil))
}
▶ جرّب الكود

التشغيل:

BASH
# إنشاء مجلد وملفات اختبار
mkdir -p public
echo "<h1>Hello</h1>" > public/index.html

# تشغيل الخادم
go run main.go

# اختبار الوصول (طرفية أخرى)
curl http://localhost:8080/
curl http://localhost:8080/static/index.html

مثال: توجيه مخصص و middleware (الصعوبة ⭐⭐)

GO
package main

import (
	"fmt"
	"log"
	"net/http"
	"time"
)

// middleware التسجيل: يسجل وقت معالجة كل طلب
func LoggingMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		start := time.Now()
		log.Printf("بداية %s %s", r.Method, r.URL.Path)

		next.ServeHTTP(w, r) // 💡 استدعاء المعالج التالي

		log.Printf("انتهى %s %s استغرق %v", r.Method, r.URL.Path, time.Since(start))
	})
}

// middleware المصادقة: يتحقق من Token في رؤوس الطلبات
func AuthMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		token := r.Header.Get("Authorization")
		if token == "" {
			http.Error(w, "وصول غير مصرح به", http.StatusUnauthorized)
			return
		}
		log.Printf("Token المستخدم: %s", token)
		next.ServeHTTP(w, r)
	})
}

func main() {
	mux := http.NewServeMux() // 💡 إنشاء مُوجّه مخصص

	// مسارات عامة
	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "مرحباً بكم في خدمة API!")
	})

	mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, `{"status": "ok"}`)
	})

	// مسارات تتطلب مصادقة
	protected := http.NewServeMux()
	protected.HandleFunc("/profile", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "هذه هي صفحة ملفك الشخصي")
	})
	protected.HandleFunc("/settings", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "هذه هي صفحة الإعدادات")
	})

	// 💡 سلسلة middleware: التسجيل أولاً، ثم المصادقة، ثم المعالجة
	mux.Handle("/api/", AuthMiddleware(protected))

	// تطبيق middleware التسجيل على جميع المسارات
	finalHandler := LoggingMiddleware(mux)

	fmt.Println("الخادم يعمل على http://localhost:8080")
	log.Fatal(http.ListenAndServe(":8080", finalHandler))
}
▶ جرّب الكود

التشغيل والاختبار:

BASH
# التشغيل
go run main.go

# اختبار المسارات العامة
curl http://localhost:8080/
curl http://localhost:8080/health

# اختبار المسارات المحمية (بدون Token)
curl http://localhost:8080/api/profile
# المخرجات: وصول غير مصرح به

# اختبار المسارات المحمية (مع Token)
curl -H "Authorization: Bearer mytoken123" http://localhost:8080/api/profile
# المخرجات: هذه هي صفحة ملفك الشخصي

مثال: خدمة API RESTful كاملة (الصعوبة ⭐⭐⭐)

GO
package main

import (
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"strconv"
	"strings"
	"sync"
	"time"
)

// User نموذج المستخدم
type User struct {
	ID    int    `json:"id"`
	Name  string `json:"name"`
	Email string `json:"email"`
}

// APIResponse صيغة استجابة موحدة
type APIResponse struct {
	Code    int         `json:"code"`
	Message string      `json:"message"`
	Data    interface{} `json:"data,omitempty"`
}

// UserStore تخزين المستخدمين (باستخدام map لمحاكاة قاعدة البيانات)
type UserStore struct {
	mu     sync.RWMutex
	users  map[int]*User
	nextID int
}

func NewUserStore() *UserStore {
	return &UserStore{
		users:  make(map[int]*User),
		nextID: 1,
	}
}

func (s *UserStore) Create(name, email string) *User {
	s.mu.Lock()
	defer s.mu.Unlock()

	user := &User{
		ID:    s.nextID,
		Name:  name,
		Email: email,
	}
	s.users[s.nextID] = user
	s.nextID++
	return user
}

func (s *UserStore) Get(id int) *User {
	s.mu.RLock()
	defer s.mu.RUnlock()
	return s.users[id]
}

func (s *UserStore) List() []*User {
	s.mu.RLock()
	defer s.mu.RUnlock()

	users := make([]*User, 0, len(s.users))
	for _, u := range s.users {
		users = append(users, u)
	}
	return users
}

func (s *UserStore) Update(id int, name, email string) *User {
	s.mu.Lock()
	defer s.mu.Unlock()

	user, ok := s.users[id]
	if !ok {
		return nil
	}
	if name != "" {
		user.Name = name
	}
	if email != "" {
		user.Email = email
	}
	return user
}

func (s *UserStore) Delete(id int) bool {
	s.mu.Lock()
	defer s.mu.Unlock()

	if _, ok := s.users[id]; !ok {
		return false
	}
	delete(s.users, id)
	return true
}

// UserHandler معالج API المستخدم
type UserHandler struct {
	store *UserStore
}

// writeJSON يكتب استجابة JSON
func (h *UserHandler) writeJSON(w http.ResponseWriter, code int, resp APIResponse) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(code)
	json.NewEncoder(w).Encode(resp)
}

// List الحصول على قائمة المستخدمين GET /users
func (h *UserHandler) List(w http.ResponseWriter, r *http.Request) {
	users := h.store.List()
	h.writeJSON(w, http.StatusOK, APIResponse{
		Code:    0,
		Message: "success",
		Data:    users,
	})
}

// Create إنشاء مستخدم POST /users
func (h *UserHandler) Create(w http.ResponseWriter, r *http.Request) {
	var req struct {
		Name  string `json:"name"`
		Email string `json:"email"`
	}

	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		h.writeJSON(w, http.StatusBadRequest, APIResponse{
			Code:    1,
			Message: "بيانات طلب غير صالحة",
		})
		return
	}

	if req.Name == "" || req.Email == "" {
		h.writeJSON(w, http.StatusBadRequest, APIResponse{
			Code:    2,
			Message: "الاسم والبريد الإلكتروني لا يمكن أن يكونا فارغين",
		})
		return
	}

	user := h.store.Create(req.Name, req.Email)
	h.writeJSON(w, http.StatusCreated, APIResponse{
		Code:    0,
		Message: "تم الإنشاء بنجاح",
		Data:    user,
	})
}

// Get الحصول على مستخدم واحد GET /users/{id}
func (h *UserHandler) Get(w http.ResponseWriter, r *http.Request) {
	idStr := strings.TrimPrefix(r.URL.Path, "/users/")
	id, err := strconv.Atoi(idStr)
	if err != nil {
		h.writeJSON(w, http.StatusBadRequest, APIResponse{
			Code:    1,
			Message: "معرف مستخدم غير صالح",
		})
		return
	}

	user := h.store.Get(id)
	if user == nil {
		h.writeJSON(w, http.StatusNotFound, APIResponse{
			Code:    3,
			Message: "المستخدم غير موجود",
		})
		return
	}

	h.writeJSON(w, http.StatusOK, APIResponse{
		Code:    0,
		Message: "success",
		Data:    user,
	})
}

// Update تحديث مستخدم PUT /users/{id}
func (h *UserHandler) Update(w http.ResponseWriter, r *http.Request) {
	idStr := strings.TrimPrefix(r.URL.Path, "/users/")
	id, err := strconv.Atoi(idStr)
	if err != nil {
		h.writeJSON(w, http.StatusBadRequest, APIResponse{
			Code:    1,
			Message: "معرف مستخدم غير صالح",
		})
		return
	}

	var req struct {
		Name  string `json:"name"`
		Email string `json:"email"`
	}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		h.writeJSON(w, http.StatusBadRequest, APIResponse{
			Code:    1,
			Message: "بيانات طلب غير صالحة",
		})
		return
	}

	user := h.store.Update(id, req.Name, req.Email)
	if user == nil {
		h.writeJSON(w, http.StatusNotFound, APIResponse{
			Code:    3,
			Message: "المستخدم غير موجود",
		})
		return
	}

	h.writeJSON(w, http.StatusOK, APIResponse{
		Code:    0,
		Message: "تم التحديث بنجاح",
		Data:    user,
	})
}

// Delete حذف مستخدم DELETE /users/{id}
func (h *UserHandler) Delete(w http.ResponseWriter, r *http.Request) {
	idStr := strings.TrimPrefix(r.URL.Path, "/users/")
	id, err := strconv.Atoi(idStr)
	if err != nil {
		h.writeJSON(w, http.StatusBadRequest, APIResponse{
			Code:    1,
			Message: "معرف مستخدم غير صالح",
		})
		return
	}

	if !h.store.Delete(id) {
		h.writeJSON(w, http.StatusNotFound, APIResponse{
			Code:    3,
			Message: "المستخدم غير موجود",
		})
		return
	}

	h.writeJSON(w, http.StatusOK, APIResponse{
		Code:    0,
		Message: "تم الحذف بنجاح",
	})
}

// CORSMiddleware middleware CORS
func CORSMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Access-Control-Allow-Origin", "*")
		w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
		w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")

		if r.Method == "OPTIONS" {
			w.WriteHeader(http.StatusOK)
			return
		}

		next.ServeHTTP(w, r)
	})
}

// RecoveryMiddleware middleware الاسترداد: يلتقط panics
func RecoveryMiddleware(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: %v", err)
				http.Error(w, "خطأ داخلي في الخادم", http.StatusInternalServerError)
			}
		}()
		next.ServeHTTP(w, r)
	})
}

func main() {
	store := NewUserStore()
	handler := &UserHandler{store: store}

	// إنشاء مُوجّه
	mux := http.NewServeMux()

	// 💡 تسجيل المسارات: التوجيه إلى معالجات مختلفة بناءً على طريقة HTTP
	mux.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) {
		switch r.Method {
		case http.MethodGet:
			handler.List(w, r)
		case http.MethodPost:
			handler.Create(w, r)
		default:
			http.Error(w, "طريقة غير مسموحة", http.StatusMethodNotAllowed)
		}
	})

	// 💡 معالجة المسارات مع معاملات المسار /users/{id}
	mux.HandleFunc("/users/", func(w http.ResponseWriter, r *http.Request) {
		switch r.Method {
		case http.MethodGet:
			handler.Get(w, r)
		case http.MethodPut:
			handler.Update(w, r)
		case http.MethodDelete:
			handler.Delete(w, r)
		default:
			http.Error(w, "طريقة غير مسموحة", http.StatusMethodNotAllowed)
		}
	})

	// تطبيق سلسلة middleware
	finalHandler := RecoveryMiddleware(CORSMiddleware(mux))

	// إنشاء خادم (أوقات قابلة للتكوين)
	server := &http.Server{
		Addr:         ":8080",
		Handler:      finalHandler,
		ReadTimeout:  10 * time.Second,  // 💡 وقت القراءة
		WriteTimeout: 10 * time.Second,  // 💡 وقت الكتابة
	}

	// إدراج بيانات اختبار
	store.Create("Alice", "alice@example.com")
	store.Create("Bob", "bob@example.com")

	fmt.Println("خادم API RESTful يعمل على http://localhost:8080")
	fmt.Println("نقاط النهاية المتاحة:")
	fmt.Println("  GET    /users      - الحصول على قائمة المستخدمين")
	fmt.Println("  POST   /users      - إنشاء مستخدم")
	fmt.Println("  GET    /users/{id} - الحصول على مستخدم واحد")
	fmt.Println("  PUT    /users/{id} - تحديث مستخدم")
	fmt.Println("  DELETE /users/{id} - حذف مستخدم")

	log.Fatal(server.ListenAndServe())
}
▶ جرّب الكود

التشغيل والاختبار الكامل:

BASH
# تشغيل الخادم
go run main.go

# الحصول على قائمة المستخدمين
curl http://localhost:8080/users

# إنشاء مستخدم
curl -X POST http://localhost:8080/users \
  -H "Content-Type: application/json" \
  -d '{"name": "Charlie", "email": "charlie@example.com"}'

# الحصول على مستخدم واحد
curl http://localhost:8080/users/1

# تحديث مستخدم
curl -X PUT http://localhost:8080/users/1 \
  -H "Content-Type: application/json" \
  -d '{"name": "Alice Smith"}'

# حذف مستخدم
curl -X DELETE http://localhost:8080/users/3

مثال المخرجات المتوقعة:

TEXT
# GET /users
{"code":0,"message":"success","data":[{"id":1,"name":"Alice","email":"alice@example.com"},{"id":2,"name":"Bob","email":"bob@example.com"}]}

# POST /users
{"code":0,"message":"تم الإنشاء بنجاح","data":{"id":3,"name":"Charlie","email":"charlie@example.com"}}

# GET /users/1
{"code":0,"message":"success","data":{"id":1,"name":"Alice","email":"alice@example.com"}}

🎬 سيناريوهات تطبيقية

السيناريو 1: بناء خدمة استعلام الطقس

GO
package main

import (
	"encoding/json"
	"fmt"
	"net/http"
	"net/url"
)

// WeatherResponse استجابة API طقس محاكاة
type WeatherResponse struct {
	City        string  `json:"city"`
	Temperature float64 `json:"temperature"`
	Condition   string  `json:"condition"`
	Humidity    int     `json:"humidity"`
}

// WeatherService معالج خدمة الطقس
type WeatherService struct {
	// بيانات محاكاة
	data map[string]WeatherResponse
}

func NewWeatherService() *WeatherService {
	return &WeatherService{
		data: map[string]WeatherResponse{
			"beijing":  {City: "Beijing", Temperature: 28.5, Condition: "Sunny", Humidity: 45},
			"shanghai": {City: "Shanghai", Temperature: 30.2, Condition: "Cloudy", Humidity: 65},
			"guangzhou": {City: "Guangzhou", Temperature: 33.1, Condition: "Thunderstorm", Humidity: 80},
		},
	}
}

// GetWeather استعلام الطقس GET /weather?city=beijing
func (s *WeatherService) GetWeather(w http.ResponseWriter, r *http.Request) {
	// 💡 الحصول على اسم المدينة من معاملات استعلام URL
	city := r.URL.Query().Get("city")
	if city == "" {
		http.Error(w, `{"error": "يرجى تقديم معامل city"}`, http.StatusBadRequest)
		return
	}

	// فك ترميز URL (يدعم أسماء المدن الصينية)
	city, _ = url.QueryUnescape(city)

	// محاكاة البحث عن بيانات الطقس
	weather, ok := s.data[city]
	if !ok {
		http.Error(w, fmt.Sprintf(`{"error": "المدينة غير موجودة: %s"}`, city), http.StatusNotFound)
		return
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(weather)
}

// GetForecast قائمة التوقعات GET /forecast
func (s *WeatherService) GetForecast(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json")

	// إرجاع طقس جميع المدن
	forecasts := make([]WeatherResponse, 0, len(s.data))
	for _, v := range s.data {
		forecasts = append(forecasts, v)
	}

	json.NewEncoder(w).Encode(map[string]interface{}{
		"count":     len(forecasts),
		"forecasts": forecasts,
	})
}

func main() {
	service := NewWeatherService()

	mux := http.NewServeMux()
	mux.HandleFunc("/weather", service.GetWeather)
	mux.HandleFunc("/forecast", service.GetForecast)

	fmt.Println("خدمة الطقس تعمل على http://localhost:8080")
	fmt.Println("الاستخدام:")
	fmt.Println("  GET /weather?city=beijing")
	fmt.Println("  GET /forecast")

	http.ListenAndServe(":8080", mux)
}
BASH
# الاختبار
curl "http://localhost:8080/weather?city=beijing"
# {"city":"Beijing","temperature":28.5,"condition":"Sunny","humidity":45}

curl http://localhost:8080/forecast
# {"count":3,"forecasts":[...]}

السيناريو 2: تنفيذ middleware تحديد المعدل

GO
package main

import (
	"fmt"
	"net/http"
	"sync"
	"time"
)

// RateLimiter محدد معدل دلو الرموز
type RateLimiter struct {
	mu       sync.Mutex
	tokens   map[string]int
	limit    int
	interval time.Duration
	lastTick map[string]time.Time
}

func NewRateLimiter(limit int, interval time.Duration) *RateLimiter {
	return &RateLimiter{
		tokens:   make(map[string]int),
		limit:    limit,
		interval: interval,
		lastTick: make(map[string]time.Time),
	}
}

// Allow يتحقق مما إذا كان الطلب مسموحاً به
func (rl *RateLimiter) Allow(key string) bool {
	rl.mu.Lock()
	defer rl.mu.Unlock()

	now := time.Now()
	last, exists := rl.lastTick[key]

	if !exists || now.Sub(last) >= rl.interval {
		// 💡 إعادة تعيين الرموز
		rl.tokens[key] = rl.limit
		rl.lastTick[key] = now
	}

	if rl.tokens[key] <= 0 {
		return false
	}

	rl.tokens[key]--
	return true
}

// RateLimitMiddleware middleware تحديد المعدل
func RateLimitMiddleware(limiter *RateLimiter) func(http.Handler) http.Handler {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			// استخدام IP العميل كمفتاح تحديد المعدل
			clientIP := r.RemoteAddr

			if !limiter.Allow(clientIP) {
				w.Header().Set("Content-Type", "application/json")
				w.WriteHeader(http.StatusTooManyRequests) // 429
				fmt.Fprintf(w, `{"error": "طلبات كثيرة جداً، يرجى المحاولة لاحقاً"}`)
				return
			}

			next.ServeHTTP(w, r)
		})
	}
}

func main() {
	// إنشاء محدد المعدل: أقصى 10 طلبات في الدقيقة لكل IP
	limiter := NewRateLimiter(10, time.Minute)

	mux := http.NewServeMux()

	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "تم الطلب بنجاح! الوقت: %s", time.Now().Format("15:04:05"))
	})

	mux.HandleFunc("/api/data", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, `{"data": "هذه بيانات API", "time": "%s"}`, time.Now().Format("15:04:05"))
	})

	// تطبيق middleware تحديد المعدل
	handler := RateLimitMiddleware(limiter)(mux)

	fmt.Println("خادم تحديد المعدل يعمل على http://localhost:8080")
	fmt.Println("قاعدة تحديد المعدل: أقصى 10 طلبات في الدقيقة")
	http.ListenAndServe(":8080", handler)
}
BASH
# اختبار تحديد المعدل (إرسال طلبات متعددة بسرعة)
for i in {1..15}; do
  curl -s http://localhost:8080/
  echo ""
done
# أول 10 طلبات تعود بشكل طبيعي، اللاحقة تعيد خطأ 429

❓ أسئلة شائعة

س1: ما الفرق بين http.HandleFunc و http.Handle؟

GO
// HandleFunc تقبل دالة
http.HandleFunc("/path", func(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello")
})

// Handle تقبل واجهة Handler
type MyHandler struct{}
func (h MyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello")
}
http.Handle("/path", MyHandler{})

الفرق:


س2: كيف أحصل على معاملات الطلب في دالة المعالجة؟

GO
func handler(w http.ResponseWriter, r *http.Request) {
	// 💡 معاملات الاستعلام (?key=value في URL)
	name := r.URL.Query().Get("name")

	// 💡 بيانات النموذج (POST form-urlencoded)
	r.ParseForm()
	email := r.Form.Get("email")

	// 💡 معاملات المسار (يحتاج تحليل يدوياً أو استخدام مُوجّه طرف ثالث)
	// مثال، 123 في /users/123
	path := r.URL.Path // "/users/123"
	// التحليل اليدوي: strings.TrimPrefix(path, "/users/")

	// 💡 رؤوس الطلب
	token := r.Header.Get("Authorization")

	// 💡 جسم الطلب JSON
	var data map[string]string
	json.NewDecoder(r.Body).Decode(&data)

	fmt.Fprintf(w, "name=%s, email=%s, token=%s", name, email, token)
}

س3: كيف أنفذ إيقاف الخادم بأناقة؟

GO
package main

import (
	"context"
	"fmt"
	"log"
	"net/http"
	"os"
	"os/signal"
	"syscall"
	"time"
)

func main() {
	server := &http.Server{
		Addr: ":8080",
		Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			time.Sleep(2 * time.Second) // محاكاة عملية تستغرق وقتاً
			fmt.Fprintf(w, "اكتملت المعالجة")
		}),
	}

	// بدء الخادم في goroutine
	go func() {
		fmt.Println("الخادم يعمل على :8080")
		if err := server.ListenAndServe(); err != http.ErrServerClosed {
			log.Fatalf("خطأ الخادم: %v", err)
		}
	}()

	// 💡 الاستماع لإشارات النظام
	quit := make(chan os.Signal, 1)
	signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
	<-quit // حجب وانتظر الإشارة

	fmt.Println("\nإيقاف الخادم بأناقة...")

	// 💡 منح الطلبات الموجودة 5 ثوانٍ للإكمال
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	if err := server.Shutdown(ctx); err != nil {
		log.Fatalf("فشل الإيقاف: %v", err)
	}

	fmt.Println("تم إيقاف الخادم بأمان")
}
BASH
# بعد التشغيل، اضغط Ctrl+C لتفعيل الإيقاف الأنيق
go run main.go
# المخرجات: الخادم يعمل على :8080
# اضغط Ctrl+C
# المخرجات: إيقاف الخادم بأناقة...
# المخرجات: تم إيقاف الخادم بأمان

س4: لماذا يُوصى بتمرير Mux مخصص كمعامل ثاني لـ ListenAndServe؟

GO
// ❌ استخدام DefaultServeMux الافتراضي (غير موصى به)
http.HandleFunc("/api/users", usersHandler)
http.ListenAndServe(":8080", nil) // nil يعني استخدام DefaultServeMux

// ✅ استخدام Mux مخصص (موصى به)
mux := http.NewServeMux()
mux.HandleFunc("/api/users", usersHandler)
http.ListenAndServe(":8080", mux)

الأسباب:

  1. DefaultServeMux عالمي — أي حزمة يمكنها تسجيل مسارات، مما قد يسبب تعارضات
  2. Mux مخصص له نطاق قابل للتحكم، يتجنب تجاوز المسارات غير المتوقعة
  3. أساعد لإعادة الاستخدام والاختبار عبر مثيلات خادم مختلفة

📖 ملخص

غطى هذا الدرس المحتوى الأساسي لبرمجة HTTP في Go:

نقطة المعرفة الاستنتاجات الرئيسية
عميل HTTP http.Get/Post لإرسال الطلبات، يجب إغلاق resp.Body
خادم HTTP http.ListenAndServe لبدء الخدمة، تسجيل المسارات
واجهة Handler طريقة ServeHTTP(ResponseWriter, *Request)
HandlerFunc محوّل دوال، يحول الدالة إلى Handler
ServeMux مُوجّه، يدعم مطابقة البادئات
Middleware يلف Handler، يدرج منطق شائع قبل/بعد الطلبات
الإيقاف الأنيق استخدام server.Shutdown ومعالجة الإشارات

النمط الأساسي:

TEXT
طلب العميل → سلسلة Middleware → مطابقة المسارات → معالجة Handler → إرجاع الاستجابة

📝 تمارين

التمرين 1: تنفيذ خدمة رفع الملفات

المتطلبات:

حل مرجعي
GO
package main

import (
	"fmt"
	"io"
	"net/http"
	"os"
	"path/filepath"
	"strings"
)

func uploadHandler(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		http.Error(w, "يُدعم فقط طريقة POST", http.StatusMethodNotAllowed)
		return
	}

	// 💡 تحديد حجم الرفع بـ 10MB
	r.ParseMultipartForm(10 << 20)

	file, header, err := r.FormFile("file")
	if err != nil {
		http.Error(w, "فشل الحصول على الملف", http.StatusBadRequest)
		return
	}
	defer file.Close()

	// التحقق من نوع الملف
	ext := strings.ToLower(filepath.Ext(header.Filename))
	allowed := map[string]bool{".jpg": true, ".jpeg": true, ".png": true, ".gif": true}
	if !allowed[ext] {
		http.Error(w, "يُسمح فقط بملفات الصور", http.StatusBadRequest)
		return
	}

	// حفظ الملف
	os.MkdirAll("./uploads", 0755)
	dst, err := os.Create(filepath.Join("./uploads", header.Filename))
	if err != nil {
		http.Error(w, "فشل حفظ الملف", http.StatusInternalServerError)
		return
	}
	defer dst.Close()

	io.Copy(dst, file)

	fmt.Fprintf(w, `{"url": "/files/%s", "size": %d}`, header.Filename, header.Size)
}

func main() {
	http.HandleFunc("/upload", uploadHandler)
	http.Handle("/files/", http.StripPrefix("/files/", http.FileServer(http.Dir("./uploads"))))

	fmt.Println("خدمة رفع الملفات تعمل على http://localhost:8080")
	http.ListenAndServe(":8080", nil)
}

التمرين 2: تنفيذ middleware مصادقة JWT

المتطلبات:

حل مرجعي
GO
package main

import (
	"encoding/base64"
	"encoding/json"
	"fmt"
	"net/http"
	"strings"
	"time"
)

// هيكل JWT مبسط
type SimpleToken struct {
	Username string    `json:"username"`
	ExpireAt time.Time `json:"expire"`
}

// توليد token بسيط
func generateToken(username string) (string, error) {
	token := SimpleToken{
		Username: username,
		ExpireAt: time.Now().Add(1 * time.Hour),
	}
	data, err := json.Marshal(token)
	if err != nil {
		return "", err
	}
	return base64.StdEncoding.EncodeToString(data), nil
}

// التحقق من token
func validateToken(tokenStr string) (*SimpleToken, error) {
	data, err := base64.StdEncoding.DecodeString(tokenStr)
	if err != nil {
		return nil, fmt.Errorf("token غير صالح")
	}

	var token SimpleToken
	if err := json.Unmarshal(data, &token); err != nil {
		return nil, fmt.Errorf("فشل تحليل token")
	}

	if time.Now().After(token.ExpireAt) {
		return nil, fmt.Errorf("token منتهي الصلاحية")
	}

	return &token, nil
}

// AuthMiddleware middleware المصادقة
func AuthMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		auth := r.Header.Get("Authorization")
		if auth == "" {
			http.Error(w, `{"error": "يرجى تقديم رأس Authorization"}`, http.StatusUnauthorized)
			return
		}

		// تحليل "Bearer <token>"
		parts := strings.SplitN(auth, " ", 2)
		if len(parts) != 2 || parts[0] != "Bearer" {
			http.Error(w, `{"error": "صيغة Authorization غير صالحة"}`, http.StatusUnauthorized)
			return
		}

		token, err := validateToken(parts[1])
		if err != nil {
			http.Error(w, fmt.Sprintf(`{"error": "%s"}`, err), http.StatusUnauthorized)
			return
		}

		// تخزين معلومات المستخدم في رأس الطلب (مبسط)
		r.Header.Set("X-Username", token.Username)
		next.ServeHTTP(w, r)
	})
}

func main() {
	mux := http.NewServeMux()

	// نقطة نهاية تسجيل الدخول
	mux.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodPost {
			http.Error(w, "يُدعم فقط POST", http.StatusMethodNotAllowed)
			return
		}

		var req struct {
			Username string `json:"username"`
			Password string `json:"password"`
		}
		json.NewDecoder(r.Body).Decode(&req)

		// تحقق بسيط (يجب التحقق من قاعدة البيانات في الإنتاج)
		if req.Username != "admin" || req.Password != "123456" {
			http.Error(w, `{"error": "اسم مستخدم أو كلمة مرور غير صالحة"}`, http.StatusUnauthorized)
			return
		}

		token, _ := generateToken(req.Username)
		fmt.Fprintf(w, `{"token": "%s"}`, token)
	})

	// نقطة نهاية محمية
	protected := http.NewServeMux()
	protected.HandleFunc("/protected", func(w http.ResponseWriter, r *http.Request) {
		username := r.Header.Get("X-Username")
		fmt.Fprintf(w, `{"message": "مرحباً %s، هذا محتوى محمي"}`, username)
	})

	mux.Handle("/protected", AuthMiddleware(protected))

	fmt.Println("خدمة مصادقة JWT تعمل على http://localhost:8080")
	http.ListenAndServe(":8080", mux)
}
BASH
# الحصول على token
curl -X POST http://localhost:8080/login \
  -H "Content-Type: application/json" \
  -d '{"username": "admin", "password": "123456"}'

# استخدام token للوصول إلى مورد محمي
curl -H "Authorization: Bearer <token>" http://localhost:8080/protected

التمرين 3: بناء محدد معدل API آمن للتزامن

المتطلبات:

حل مرجعي
GO
package main

import (
	"fmt"
	"net/http"
	"sync"
	"time"
)

// SlidingWindowLimiter محدد معدل نافذة منزلقة
type SlidingWindowLimiter struct {
	mu       sync.Mutex
	windows  map[string]*window
	limit    int
	interval time.Duration
}

type window struct {
	count     int
	startTime time.Time
}

func NewSlidingWindowLimiter(limit int, interval time.Duration) *SlidingWindowLimiter {
	return &SlidingWindowLimiter{
		windows:  make(map[string]*window),
		limit:    limit,
		interval: interval,
	}
}

func (l *SlidingWindowLimiter) Allow(key string) bool {
	l.mu.Lock()
	defer l.mu.Unlock()

	now := time.Now()
	w, exists := l.windows[key]

	if !exists || now.Sub(w.startTime) >= l.interval {
		// فتح نافذة جديدة
		l.windows[key] = &window{count: 1, startTime: now}
		return true
	}

	if w.count >= l.limit {
		return false
	}

	w.count++
	return true
}

// Cleanup تنظيف النوافذ المنتهية (يجب استدعاؤها دورياً)
func (l *SlidingWindowLimiter) Cleanup() {
	l.mu.Lock()
	defer l.mu.Unlock()

	now := time.Now()
	for key, w := range l.windows {
		if now.Sub(w.startTime) >= l.interval*2 {
			delete(l.windows, key)
		}
	}
}

func SlidingWindowMiddleware(limiter *SlidingWindowLimiter) func(http.Handler) http.Handler {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			if !limiter.Allow(r.RemoteAddr) {
				w.Header().Set("Retry-After", "60")
				http.Error(w, `{"error": "طلبات كثيرة جداً"}`, http.StatusTooManyRequests)
				return
			}
			next.ServeHTTP(w, r)
		})
	}
}

func main() {
	limiter := NewSlidingWindowLimiter(5, time.Minute) // 5 في الدقيقة

	// تنظيف دورياً
	go func() {
		for {
			time.Sleep(10 * time.Minute)
			limiter.Cleanup()
		}
	}()

	mux := http.NewServeMux()
	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "تم الطلب بنجاح: %s", time.Now().Format("15:04:05"))
	})

	handler := SlidingWindowMiddleware(limiter)(mux)

	fmt.Println("محدد معدل النافذة المنزلقة يعمل على http://localhost:8080")
	fmt.Println("قاعدة تحديد المعدل: 5 طلبات في الدقيقة")
	http.ListenAndServe(":8080", handler)
}

📚 الدرس التالي

تهانينا على إكمال برمجة HTTP! في الدرس التالي، سنتعلم برمجة الاختبارات في Go — بما في ذلك اختبارات الوحدة، واختبارات مدفوعة بالجدول، واختبارات المعايير المرجعية، وغيرها، لمساعدتك على كتابة كود أكثر موثوقية.

الدرس التالي: الاختبارات →

Web-Tutorial.com

فريق Web-Tutorial التقني

منصة دروس برمجية يديرها عدة مطورين. كل درس يتم كتابته ومراجعته بواسطة مطورين متخصصين في المجال. نعمل على ضمان دقة وموثوقية المحتوى — إذا لاحظت أي مشكلة، فيرجى إخبارنا.

100%