برمجة HTTP
الدرس 22: برمجة HTTP
🎯 تشبيه من الحياة
تخيل أنك تدير مطعماً:
- خادم HTTP مثل موظف الاستقبال في المطعم — عندما يصل العملاء (العملاء)، يوجههم موظف الاستقبال إلى نافذة الخدمة المناسبة بناءً على احتياجاتهم
- التوجيه (ServeMux) مثل فئات قائمة المطعم — أطباق مختلفة (مسارات URL) يتعامل معها طهاة مختلفون (دوال المعالجة)
- واجهة Handler مثل معايير عمل الطاهي — بغض النظر عن الطبق الذي يتم تحضيره، يجب اتباع سير عمل موحد
- الـ middleware مثل سير عمل خدمة المطعم — العملاء يخلعون أحذيتهم، يغسلون أيديهم، ثم يجلسون؛ هذه الخطوات مشتركة للجميع وليست خاصة بطباق معينة
حزمة 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{})
الفرق:
HandleFunc: تقبل دالة من نوعfunc(ResponseWriter, *Request)Handle: تقبل نوعاً ينفذ واجهةHandlerHandleFuncتلف الدالة داخلياً في محوّلHandlerFunc
س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)
الأسباب:
DefaultServeMuxعالمي — أي حزمة يمكنها تسجيل مسارات، مما قد يسبب تعارضات- Mux مخصص له نطاق قابل للتحكم، يتجنب تجاوز المسارات غير المتوقعة
- أساعد لإعادة الاستخدام والاختبار عبر مثيلات خادم مختلفة
📖 ملخص
غطى هذا الدرس المحتوى الأساسي لبرمجة 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: تنفيذ خدمة رفع الملفات
المتطلبات:
- إنشاء نقطة نهاية
POST /uploadلتلقي رفع الملفات - تحديد حجم الملف بـ 10MB
- السماح فقط بأنواع الصور (jpg, png, gif)
- حفظ الملفات في مجلد
./uploads - إرجاع URL وصول الملف
حل مرجعي
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
المتطلبات:
- تنفيذ توليد والتحقق من JWT بسيط (يمكن محاكاته بـ Base64)
- إنشاء نقطة نهاية
POST /loginللحصول على token - إنشاء نقطة نهاية محمية
GET /protected - تمرير token عبر
Authorization: Bearer <token>
حل مرجعي
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 آمن للتزامن
المتطلبات:
- تنفيذ خوارزمية تحديد معدل نافذة منزلقة
- دعم تكوين أقصى طلبات لكل نافذة وحجم النافذة
- توفير واجهة middleware
- استخدام Redis أو تخزين في الذاكرة (في الذاكرة مقبول)
حل مرجعي
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 — بما في ذلك اختبارات الوحدة، واختبارات مدفوعة بالجدول، واختبارات المعايير المرجعية، وغيرها، لمساعدتك على كتابة كود أكثر موثوقية.



