التعامل مع API في تطبيقات Go

التعامل مع API في تطبيقات Go

عندما تبدأ في بناء تطبيقات حديثة، ستكتشف بسرعة أن التعامل مع الـ API ليس مجرد ميزة إضافية، بل هو جزء أساسي من الحياة اليومية لأي مطور Backend. التطبيقات اليوم لا تعمل بمعزل عن بعضها، بل تتواصل مع خدمات خارجية، وتستهلك بيانات من منصات أخرى، وترسل طلبات إلى أنظمة دفع، وتتعامل مع خرائط، وتُجري عمليات تسجيل دخول، وتربط بين عدة أجزاء داخل نفس المنظومة. ولهذا السبب أصبح فهم API في Go مهارة مهمة جدًا، ليس فقط للمطور الذي يريد بناء خدمة بسيطة، بل لأي شخص يريد أن يبني Backend قويًا، نظيفًا، وسهل الصيانة.

Go من أكثر اللغات المناسبة لهذا المجال، لأنها تمنحك مزيجًا جميلًا من البساطة، والسرعة، والوضوح، والاستقرار. قد تشعر في البداية أن بناء API في Go يتطلب بعض التفاصيل، خصوصًا إذا كنت قادمًا من لغات أعلى تجريدًا مثل Python أو JavaScript، لكن الجميل في Go أن هذه التفاصيل ليست عبئًا، بل جزء من فلسفتها. Go تريد منك أن تكتب كودًا صريحًا، مفهومًا، وقابلًا للصيانة. وهذا بالضبط ما نحتاجه عندما نتعامل مع API، لأن API غير المنظم يتحول بسرعة إلى مصدر فوضى في أي مشروع.

في هذا المقال سنأخذ رحلة عملية ومريحة داخل عالم API في Go. سنبدأ من الأساسيات، ثم ننتقل إلى بناء API بسيط، ثم نتعرف على JSON، والـ request والـ response، والـ handlers، والـ routing، ثم نضيف مفاهيم مهمة مثل التحقق من المدخلات، والتعامل مع الأخطاء، والأمان، وCRUD، والـ middleware، واستهلاك API خارجي من داخل تطبيق Go نفسه. وسنحاول أن نجعل الشرح قريبًا من الواقع، لأن أجمل طريقة لفهم API ليست فقط عبر التعريفات، بل عبر بناء شيء يمكن أن تراه وتلمسه وتفهم كيف يعمل فعليًا.

ما هو الـ API أصلًا؟

API اختصار لـ Application Programming Interface، ويمكن فهمه ببساطة على أنه “الوسيلة التي تسمح لتطبيقين أو مكونين بالتواصل مع بعضهما”. عندما يرسل المتصفح طلبًا إلى الخادم، أو عندما يستدعي تطبيقك خدمة خارجية، أو عندما يطلب برنامجك بيانات من قاعدة بيانات عبر طبقة وسيطة، فأنت في الواقع تستخدم نوعًا من API.

في عالم الويب، غالبًا ما نعني بـ API مجموعة من endpoints تستقبل طلبات HTTP وتُعيد بيانات، عادة بصيغة JSON. مثلًا، قد يكون لديك endpoint مثل /users يعيد قائمة المستخدمين، أو /products/10 يعيد بيانات منتج معين، أو /login يستقبل اسم المستخدم وكلمة المرور ثم يعيد token. هذا النوع من APIs هو الأكثر شيوعًا اليوم، وGo مناسبة جدًا لبنائه.

الجميل في Go أنها لا تفرض عليك إطارًا ضخمًا في البداية. يمكنك أن تبدأ باستخدام المكتبة القياسية فقط، وهذا يمنحك فهمًا عميقًا لطريقة عمل HTTP من الأساس. ثم لاحقًا، إذا أردت، يمكنك الانتقال إلى أطر مثل Gin أو Fiber أو Echo، لكنك لن تكون مضطرًا إلى الاعتماد عليها من أول يوم. هذه نقطة مهمة جدًا، لأن فهم الأساسيات يريحك لاحقًا مهما تغيّرت الأدوات.

لماذا Go مناسبة جدًا لبناء API؟

هناك عدة أسباب تجعل Go من أفضل اللغات لبناء API حديثة. أولًا، الأداء ممتاز. ثانيًا، التزامن في Go قوي وبسيط نسبيًا، وهذا مفيد جدًا عندما يخدم الـ API عددًا كبيرًا من الطلبات. ثالثًا، الملفات التنفيذية الناتجة من Go سهلة النشر. رابعًا، الكود في Go غالبًا يكون واضحًا ومنظمًا، وهذا أمر حاسم في المشاريع التي تكبر بمرور الوقت.

الكثير من فرق backend تختار Go لأنها تريد خدمة سريعة لا تستهلك موارد كثيرة، ويمكن نشرها بسهولة داخل Docker أو على الخوادم السحابية. كما أن Go تمنحك إحساسًا بالانضباط. عندما تكتب handler أو struct أو response model، فأنت تبني طبقة منطقية واضحة، وهذا يساعد الفريق كله على قراءة الكود وصيانته لاحقًا.

وفي الحقيقة، التعامل مع API في Go ليس فقط عن استجابة سريعة، بل عن بناء نظام كامل يمكن الاعتماد عليه. تطبيق API جيد ليس مجرد كود “يعمل”، بل كود يمكن فهمه، توسيعه، اختباره، وتأمينه. Go تساعدك في هذا المسار لأنها تشجع على البنية النظيفة منذ البداية.

فهم HTTP في Go

قبل أن نبني API، من المهم أن نفهم كيف تتعامل Go مع HTTP. المكتبة الأساسية التي نستخدمها غالبًا هي net/http. هذه المكتبة توفر كل ما تحتاجه تقريبًا لبدء السيرفر، واستقبال الطلبات، وإرسال الردود، وتعريف المسارات، والتعامل مع الـ headers، والـ status codes، والـ request body.

في Go، الـ handler هو الدالة التي تستقبل http.ResponseWriter و*http.Request. الأولى تُستخدم لكتابة الرد، والثانية تمثل الطلب القادم من العميل. هذه البساطة هي واحدة من أجمل نقاط Go. لا توجد طبقات معقدة جدًا في البداية، بل أدوات واضحة جدًا.

لنأخذ مثالًا صغيرًا جدًا:

package main

import (
	"fmt"
	"net/http"
)

func homeHandler(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintln(w, "Hello from Go API")
}

func main() {
	http.HandleFunc("/", homeHandler)
	http.ListenAndServe(":8080", nil)
}

هذا المثال الصغير يوضح الفكرة الأساسية: نحدد دالة تستجيب للطلب، ثم نربطها بمسار معين، ثم نشغل السيرفر. بسيط جدًا، لكنه يفتح الباب لعالم كامل من الإمكانيات.

أول API حقيقي في Go

دعنا نبني أول endpoint حقيقي يعيد JSON بدل نص عادي. في الواقع، معظم APIs الحديثة تتحدث بصيغة JSON، لذلك من المهم أن نعرف كيف نُخرج JSON من Go.

package main

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

type Message struct {
	Text string `json:"text"`
}

func homeHandler(w http.ResponseWriter, r *http.Request) {
	response := Message{
		Text: "Hello from Go API",
	}

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

func main() {
	http.HandleFunc("/", homeHandler)
	http.ListenAndServe(":8080", nil)
}

في هذا المثال لدينا struct باسم Message يحتوي على حقل Text. أضفنا tag باسم json:"text" حتى نتحكم في اسم الحقل داخل JSON. ثم أنشأنا response وأرسلناه بصيغة JSON باستخدام json.NewEncoder(w).Encode(response).

هذا النمط ستستخدمه كثيرًا في Go. بدل أن ترجع نصًا فقط، ستبني structs تمثل البيانات، ثم تحولها إلى JSON، ثم ترسلها للعميل. وهذه الطريقة ممتازة لأنها تنظف الكود وتجعل البيانات منظمة.

التعامل مع JSON في Go

الـ JSON هو اللغة المفضلة تقريبًا بين كثير من الـ APIs الحديثة. لذلك عليك أن تتقن قراءته وإرساله. في Go نستخدم الحزمة encoding/json لهذا الغرض.

تحويل Struct إلى JSON

type User struct {
	ID    int    `json:"id"`
	Name  string `json:"name"`
	Email string `json:"email"`
}

ثم:

user := User{
	ID:    1,
	Name:  "Hassan",
	Email: "hassan@example.com",
}

ولإرساله:

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

قراءة JSON من الطلب

عندما يرسل العميل بيانات إلى API، غالبًا تكون في body بصيغة JSON. في هذه الحالة نستخدم json.NewDecoder(r.Body).Decode(&payload).

مثال:

type CreateUserRequest struct {
	Name  string `json:"name"`
	Email string `json:"email"`
}

func createUserHandler(w http.ResponseWriter, r *http.Request) {
	var req CreateUserRequest

	err := json.NewDecoder(r.Body).Decode(&req)
	if err != nil {
		http.Error(w, "invalid json", http.StatusBadRequest)
		return
	}

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

هنا استقبلنا JSON، فككناه إلى struct، ثم أعدناه من جديد. في التطبيقات الواقعية، ستضيف منطقًا أكبر من مجرد الإعادة، مثل التحقق من صحة البيانات، ثم الحفظ في قاعدة البيانات، ثم إعادة response مناسب.

بناء REST API في Go

REST هو نمط معماري شائع جدًا في APIs. الفكرة الأساسية أن تتعامل مع الموارد ككيانات واضحة، وتستخدم HTTP verbs لتنفيذ العمليات عليها.

مثلًا:

  • GET /users لجلب كل المستخدمين

  • GET /users/1 لجلب مستخدم واحد

  • POST /users لإنشاء مستخدم جديد

  • PUT /users/1 لتحديث مستخدم

  • DELETE /users/1 لحذف مستخدم

هذا الأسلوب منظم جدًا، ويسهّل على الفريق فهم الـ API واستخدامها. Go مناسبة تمامًا لهذا النمط، لأن الكود فيها يميل إلى الوضوح والصرامة المفيدة.

بناء CRUD API بسيط

لننتقل إلى مثال أكثر عملية. سنبني API صغيرة لإدارة المستخدمين، مع عمليات:

  • إنشاء مستخدم

  • عرض المستخدمين

  • عرض مستخدم محدد

  • تحديث مستخدم

  • حذف مستخدم

سنستخدم ذاكرة مؤقتة داخل التطبيق فقط، حتى يبقى المثال واضحًا. لاحقًا يمكن استبدالها بقاعدة بيانات.

package main

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

type User struct {
	ID    int    `json:"id"`
	Name  string `json:"name"`
	Email string `json:"email"`
}

type CreateUserRequest struct {
	Name  string `json:"name"`
	Email string `json:"email"`
}

var (
	users  = []User{}
	lastID  = 0
	mu      sync.Mutex
)

func main() {
	http.HandleFunc("/users", usersHandler)
	http.HandleFunc("/users/", userByIDHandler)

	http.ListenAndServe(":8080", nil)
}

func usersHandler(w http.ResponseWriter, r *http.Request) {
	switch r.Method {
	case http.MethodGet:
		getUsers(w, r)
	case http.MethodPost:
		createUser(w, r)
	default:
		http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
	}
}

func userByIDHandler(w http.ResponseWriter, r *http.Request) {
	idStr := strings.TrimPrefix(r.URL.Path, "/users/")
	id, err := strconv.Atoi(idStr)
	if err != nil {
		http.Error(w, "invalid id", http.StatusBadRequest)
		return
	}

	switch r.Method {
	case http.MethodGet:
		getUserByID(w, r, id)
	case http.MethodPut:
		updateUser(w, r, id)
	case http.MethodDelete:
		deleteUser(w, r, id)
	default:
		http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
	}
}

func getUsers(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(users)
}

func createUser(w http.ResponseWriter, r *http.Request) {
	var req CreateUserRequest

	err := json.NewDecoder(r.Body).Decode(&req)
	if err != nil {
		http.Error(w, "invalid json", http.StatusBadRequest)
		return
	}

	if req.Name == "" || req.Email == "" {
		http.Error(w, "name and email are required", http.StatusBadRequest)
		return
	}

	mu.Lock()
	defer mu.Unlock()

	lastID++
	user := User{
		ID:    lastID,
		Name:  req.Name,
		Email: req.Email,
	}

	users = append(users, user)

	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(http.StatusCreated)
	json.NewEncoder(w).Encode(user)
}

func getUserByID(w http.ResponseWriter, r *http.Request, id int) {
	for _, user := range users {
		if user.ID == id {
			w.Header().Set("Content-Type", "application/json")
			json.NewEncoder(w).Encode(user)
			return
		}
	}

	http.Error(w, "user not found", http.StatusNotFound)
}

func updateUser(w http.ResponseWriter, r *http.Request, id int) {
	var req CreateUserRequest

	err := json.NewDecoder(r.Body).Decode(&req)
	if err != nil {
		http.Error(w, "invalid json", http.StatusBadRequest)
		return
	}

	for i, user := range users {
		if user.ID == id {
			users[i].Name = req.Name
			users[i].Email = req.Email

			w.Header().Set("Content-Type", "application/json")
			json.NewEncoder(w).Encode(users[i])
			return
		}
	}

	http.Error(w, "user not found", http.StatusNotFound)
}

func deleteUser(w http.ResponseWriter, r *http.Request, id int) {
	for i, user := range users {
		if user.ID == id {
			users = append(users[:i], users[i+1:]...)
			w.WriteHeader(http.StatusNoContent)
			return
		}
	}

	http.Error(w, "user not found", http.StatusNotFound)
}

هذا المثال كبير نسبيًا، لكنه مهم جدًا لأنه يوضح أساسيات العمل الحقيقي. لديك routing بسيط، وhandling للـ methods، وقراءة JSON، وكتابة JSON، والتعامل مع status codes، وإدارة أخطاء أساسية، وحتى حماية بسيطة بواسطة mutex عندما نعدل على الشريحة users.

ماذا نتعلم من المثال السابق؟

هناك عدة دروس مهمة جدًا:

أولًا، عندما تبني API، لا تعتمد فقط على “أن الكود يعمل”، بل على تنظيمه أيضًا. فصلنا العمليات إلى دوال صغيرة، وهذا يجعل الكود أوضح وأسهل في الصيانة.

ثانيًا، لا تهمل الـ HTTP methods. التعامل الصحيح مع GET وPOST وPUT وDELETE يجعل الـ API مفهومة ومتوقعة.

ثالثًا، لا تنس status codes. هذه نقطة يخطئ فيها كثير من المبتدئين. الرد لا يجب أن يكون دائمًا 200 OK. أحيانًا تحتاج إلى 201 Created، أو 400 Bad Request، أو 404 Not Found, أو 405 Method Not Allowed, أو 500 Internal Server Error.

رابعًا، لا تهمل التحقق من البيانات. API بدون validation يتحول إلى باب مفتوح للأخطاء والمشاكل.

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

التعامل مع الـ Routing

في المثال السابق استخدمنا http.HandleFunc وطرقًا بسيطة جدًا. هذا مناسب للمشاريع الصغيرة أو للتعلم، لكن في المشاريع الأكبر ستحتاج إلى routing أكثر تنظيمًا. هنا لديك خياران: إما الاستمرار بالمكتبة القياسية مع تنظيم يدوي جيد، أو استخدام Router/Framework مثل Gin أو Chi أو Echo.

مثال باستخدام chi

package main

import (
	"fmt"
	"net/http"

	"github.com/go-chi/chi/v5"
)

func main() {
	r := chi.NewRouter()

	r.Get("/users", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintln(w, "list users")
	})

	r.Post("/users", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintln(w, "create user")
	})

	http.ListenAndServe(":8080", r)
}

الـ Router يجعل الكود أنظف وأكثر قابلية للتوسع. كل route له تعريف واضح، ويمكنك إضافة middleware بسهولة، وتنظيم الـ API بحسب الموارد.

مثال باستخدام Gin

package main

import "github.com/gin-gonic/gin"

func main() {
	r := gin.Default()

	r.GET("/users", func(c *gin.Context) {
		c.JSON(200, gin.H{"message": "list users"})
	})

	r.POST("/users", func(c *gin.Context) {
		c.JSON(201, gin.H{"message": "create user"})
	})

	r.Run(":8080")
}

Gin مشهور جدًا بسبب بساطته وسرعته وسهولة استخدامه. كثير من المطورين يبدأون به بعد فهم أساسيات net/http. لكنه يظل أداة، وليس بديلًا عن الفهم الأساسي. من المهم أن تعرف ماذا يحدث تحت الغطاء، حتى لو استخدمت إطارًا مريحًا.

لماذا الـ status codes مهمة جدًا؟

الـ status code ليس مجرد رقم، بل رسالة. عندما ترسل 200، فأنت تقول إن كل شيء سار بشكل طبيعي. عندما ترسل 201، فأنت تقول إن موردًا جديدًا تم إنشاؤه. عندما ترسل 400، فأنت تشير إلى أن المشكلة من جهة الطلب نفسه. عندما ترسل 401 أو 403، فأنت تتحدث عن authentication أو authorization. عندما ترسل 404، فأنت تقول إن المورد غير موجود.

في API جيدة، لا نكتفي بإرسال رسائل نصية فقط. بل نرسل status code مناسبًا، ونبني response body واضحًا أيضًا. هذا يجعل الـ API سهلة الفهم من قبل تطبيقات frontend أو mobile أو أي خدمة تستهلكها.

مثال:

w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(map[string]string{
	"message": "user created successfully",
})

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

التحقق من البيانات Validation

إذا كنت تبني API حقيقية، فالتحقق من البيانات أمر لا يمكن تجاهله. تخيل أن العميل أرسل اسمًا فارغًا، أو بريدًا غير صحيح، أو رقمًا في حقل نصي، أو تاريخًا بصيغة خاطئة. إن لم تتحقق من البيانات، ستسمح للأخطاء بالدخول إلى النظام من الباب الأمامي.

في Go يمكنك التحقق يدويًا، أو استخدام مكتبات مساعدة. لكن حتى التحقق اليدوي مهم جدًا ويجب فهمه. مثال:

if req.Name == "" {
	http.Error(w, "name is required", http.StatusBadRequest)
	return
}

if req.Email == "" {
	http.Error(w, "email is required", http.StatusBadRequest)
	return
}

يمكنك أيضًا كتابة دوال تحقق مخصصة:

func isValidEmail(email string) bool {
	return strings.Contains(email, "@")
}

ومع المشاريع الأكبر، ستحتاج غالبًا إلى validation أكثر تطورًا، يشمل الطول، والصيغ، والقيود، وربما التحقق من القيم المكررة في قاعدة البيانات.

التحقق الجيد لا يحمي النظام فقط، بل يجعل تجربة المستخدم أفضل أيضًا، لأنه يعطيه رسالة واضحة بدل فشل غامض.

التعامل مع الأخطاء Error Handling

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

مثال على قراءة JSON:

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

لكن في المشاريع الأكبر، ستحتاج إلى نمط أكثر تنظيمًا. بدل أن تكتب http.Error في كل مكان، يمكنك بناء helper دوال تعيد JSON errors بشكل موحد.

مثال:

func writeJSONError(w http.ResponseWriter, message string, status int) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(status)
	json.NewEncoder(w).Encode(map[string]string{
		"error": message,
	})
}

ثم تستخدمها هكذا:

writeJSONError(w, "invalid json", http.StatusBadRequest)

هذا يجعل response موحدًا في كل الـ API، وهو أفضل بكثير من أن يكون كل endpoint يرد بطريقة مختلفة.

بناء Response موحد

من أفضل الممارسات في الـ API أن يكون الرد موحدًا إلى حد ما. هذا يساعد الـ frontend أو أي مستهلك آخر على التعامل مع النتائج بسهولة. يمكنك مثلًا اعتماد شكل ثابت مثل:

{
  "success": true,
  "data": {...},
  "message": "..."
}

أو عند الخطأ:

{
  "success": false,
  "error": "invalid request"
}

في Go، يمكنك بناء helper بسيط لذلك:

type APIResponse struct {
	Success bool        `json:"success"`
	Data    interface{} `json:"data,omitempty"`
	Message string      `json:"message,omitempty"`
	Error   string      `json:"error,omitempty"`
}

ثم:

func writeJSON(w http.ResponseWriter, status int, payload APIResponse) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(status)
	json.NewEncoder(w).Encode(payload)
}

وبهذا يصبح الكود أنظف بكثير.

التوثيق Documentation

API جيدة بدون توثيق جيد قد تكون صعبة الاستخدام حتى لو كانت ممتازة تقنيًا. التوثيق يشرح المسارات، والـ parameters، والـ request body، والـ response body، والـ status codes، والأمثلة. في Go، يمكنك استخدام أدوات كثيرة مثل Swagger/OpenAPI لتوليد توثيق تلقائي.

مثال عملي: عندما تكتب endpoint لإنشاء مستخدم، يجب أن توضح:

  • ماذا يرسل العميل؟

  • ما الحقول المطلوبة؟

  • ما الحقول الاختيارية؟

  • ما الرد الناجح؟

  • ماذا يحدث عند الخطأ؟

التوثيق الجيد يوفر وقتًا كبيرًا على الفريق، ويقلل الأسئلة المتكررة، ويجعل الـ API أكثر احترافية. أحيانًا المشكلة ليست في الكود، بل في عدم وضوح ما يجب أن يفعله الكود.

استخدام Middleware في Go

الـ middleware جزء مهم جدًا من أي API متقدمة. هو ببساطة طبقة تنفذ قبل أو بعد handler، وتستخدم عادة لأشياء مثل:

  • تسجيل الطلبات

  • التحقق من الهوية

  • ضغط الردود

  • تحديد معدل الطلبات

  • إضافة headers

  • قياس الأداء

  • التقاط الأخطاء

مثال بسيط على middleware في Go:

func loggingMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		fmt.Println("Request:", r.Method, r.URL.Path)
		next.ServeHTTP(w, r)
	})
}

ثم تستخدمه:

mux := http.NewServeMux()
mux.HandleFunc("/", homeHandler)

http.ListenAndServe(":8080", loggingMiddleware(mux))

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

المصادقة Authentication

كثير من الـ APIs تحتاج إلى حماية. لا يكفي أن تفتح endpoint للعالم كله إذا كان فيه بيانات حساسة. هنا تأتي المصادقة. أحد أكثر الأنماط شيوعًا هو JWT، أو tokens بشكل عام. يمكنك أيضًا استخدام sessions أو API keys حسب الحاجة.

في Go، ستكتب middleware يتحقق من الـ token المرسل في Authorization header. فإذا كان صحيحًا، يسمح بالمرور. وإذا كان غير صحيح، يعيد 401 Unauthorized.

مثال بسيط جدًا:

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, "unauthorized", http.StatusUnauthorized)
			return
		}

		next.ServeHTTP(w, r)
	})
}

هذا مثال تعليمي فقط، لكنه يوضح الفكرة العامة. في الواقع، ستتحقق من صحة التوكن، وتقرأ الـ claims، وربما تتأكد من الصلاحيات أيضًا.

الفرق بين Authentication وAuthorization

من المهم جدًا التفريق بينهما. Authentication تعني: من أنت؟ Authorization تعني: ماذا يُسمح لك أن تفعل؟ قد يكون المستخدم مسجل الدخول، لكن ليس لديه صلاحية حذف مستخدم آخر. هنا authentication موجودة، لكن authorization تمنعه من العملية.

في الـ API الجيدة يجب أن تبني الاثنين بوضوح. Go تساعدك على ذلك لأن بنيتها الصريحة تجعل من السهل تنظيم middleware وصلاحيات الوصول بشكل واضح.

استهلاك API خارجي من داخل Go

إلى الآن تحدثنا عن بناء API، لكن ماذا عن استهلاك API خارجي؟ هذا جزء مهم جدًا من العمل اليومي أيضًا. قد تحتاج إلى جلب بيانات من خدمة دفع، أو خدمة طقس، أو خدمة خرائط، أو أي منصة ثالثة.

في Go، نستخدم http.Client لإرسال طلبات خارجية.

مثال:

package main

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

func main() {
	resp, err := http.Get("https://jsonplaceholder.typicode.com/users/1")
	if err != nil {
		fmt.Println("request failed:", err)
		return
	}
	defer resp.Body.Close()

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		fmt.Println("read failed:", err)
		return
	}

	fmt.Println(string(body))
}

هذا المثال يجلب بيانات من API خارجي ويطبعها. يمكنك أيضًا فك JSON إلى struct بدل طباعته كنص.

package main

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

type User struct {
	ID    int    `json:"id"`
	Name  string `json:"name"`
	Email string `json:"email"`
}

func main() {
	resp, err := http.Get("https://jsonplaceholder.typicode.com/users/1")
	if err != nil {
		fmt.Println(err)
		return
	}
	defer resp.Body.Close()

	var user User
	if err := json.NewDecoder(resp.Body).Decode(&user); err != nil {
		fmt.Println(err)
		return
	}

	fmt.Printf("%+v\n", user)
}

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

التعامل مع الوقت Timeout

عند استهلاك API خارجي أو حتى في بعض الحالات داخل تطبيقك، من المهم جدًا تحديد timeout. لأن الطلبات التي لا تنتهي قد تستهلك الموارد وتسبب مشاكل كبيرة.

في Go، يمكنك إنشاء client مع timeout:

client := &http.Client{
	Timeout: 5 * time.Second,
}

ثم:

resp, err := client.Get("https://example.com")

هذا مهم جدًا في الإنتاج. الكثير من الأعطال لا تأتي من الكود نفسه، بل من طلبات لا تنتهي أو من خدمات خارجية بطيئة جدًا. لذلك timeout ليس خيارًا ثانويًا، بل ضرورة.

التعامل مع السياق Context

context من أهم الأدوات في Go، خاصة عند بناء API. يساعدك على تمرير الإلغاء، والـ deadlines، والقيم المطلوبة عبر أجزاء التطبيق. في HTTP handlers، يأتي context عادة من r.Context().

مثال:

func handler(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()

	select {
	case <-time.After(2 * time.Second):
		w.Write([]byte("done"))
	case <-ctx.Done():
		http.Error(w, "request canceled", http.StatusRequestTimeout)
	}
}

هذا مهم عندما يتوقف العميل عن انتظار الرد، أو عندما تريد إلغاء عملية طويلة لأن deadline انتهى. في أنظمة الإنتاج، هذا النوع من التحكم يوفر موارد ويمنع تراكم العمليات غير المفيدة.

التعامل مع قواعد البيانات داخل API

معظم الـ APIs الحقيقية تحتاج إلى قاعدة بيانات. سواء كانت MySQL أو PostgreSQL أو MongoDB، ستحتاج إلى ربط الـ handlers بطبقة persistence. الفكرة العامة هي نفسها: يستقبل الـ endpoint الطلب، يتحقق من البيانات، ثم يحفظها أو يقرأها من قاعدة البيانات، ثم يعيد response مناسبًا.

في Go، يمكنك استخدام database/sql أو مكتبات أعلى مستوى. لكن حتى مع database/sql ستجد أن الأمور واضحة.

مثال شبه مبسط:

row := db.QueryRow("SELECT id, name, email FROM users WHERE id = ?", id)

var user User
err := row.Scan(&user.ID, &user.Name, &user.Email)
if err != nil {
	if err == sql.ErrNoRows {
		http.Error(w, "user not found", http.StatusNotFound)
		return
	}
	http.Error(w, "internal server error", http.StatusInternalServerError)
	return
}

هذا الأسلوب يعطيك تحكمًا كبيرًا. ويمكنك لاحقًا تنظيم الوصول إلى البيانات عبر repository pattern أو service layer حتى لا يصبح الـ handler مزدحمًا.

هيكلة مشروع API في Go

واحدة من أكثر الأشياء التي تريحك في Go هي أن البنية ليست معقدة جدًا، لكن هذا لا يعني أن تضع كل شيء في ملف واحد. في المشاريع الصغيرة قد يبدأ البعض بهذا، ثم يفاجأ لاحقًا بأن الصيانة أصبحت صعبة. الأفضل أن تفكر مبكرًا في الهيكلة.

هيكلة بسيطة قد تكون هكذا:

myapi/
  main.go
  handlers/
    users.go
  models/
    user.go
  services/
    user_service.go
  repository/
    user_repository.go
  middleware/
    auth.go
  utils/
    response.go

هذا ليس الشكل الوحيد، لكنه مثال جيد على فصل المسؤوليات. عندما تكبر الـ API، ستشكر نفسك على هذا التنظيم.

أفضل الممارسات عند بناء API في Go

هناك مجموعة من العادات الجيدة التي تصنع فرقًا كبيرًا:

أولًا، اجعل responses موحدة قدر الإمكان.
ثانيًا، استخدم status codes بشكل صحيح.
ثالثًا، لا تهمل validation.
رابعًا، افصل منطق العمل عن الـ handlers.
خامسًا، لا تضع كل شيء في main.go.
سادسًا، استخدم timeout عند الطلبات الخارجية.
سابعًا، وثّق الـ API جيدًا.
ثامنًا، اختبر الـ API قبل الاعتماد عليها في الإنتاج.
تاسعًا، تعامل مع الأخطاء بوضوح، ولا تُخفها.
عاشرًا، حافظ على بساطة التصميم ما أمكن.

هذه النصائح تبدو بسيطة، لكنها تصنع الفرق بين API جميلة وأخرى مرهقة.

كتابة Tests للـ API

الاختبارات مهمة جدًا. في Go، كتابة الاختبارات ليست صعبة، بل هي جزء طبيعي من ثقافة اللغة. يمكنك اختبار الـ handlers، والتحقق من الردود، والـ status codes، والـ JSON body.

مثال بسيط:

func TestHomeHandler(t *testing.T) {
	req := httptest.NewRequest(http.MethodGet, "/", nil)
	w := httptest.NewRecorder()

	homeHandler(w, req)

	resp := w.Result()
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		t.Fatalf("expected 200, got %d", resp.StatusCode)
	}
}

الاختبارات تمنحك ثقة عندما تعدل على الكود. بدل أن تعتمد على “أظن أنه يعمل”، تصبح قادرًا على التأكد. وهذا مهم جدًا مع API لأنه كل تغيير صغير قد يؤثر على المستهلكين الآخرين.

كيف تتعامل مع CORS؟

إذا كانت الـ API ستُستهلك من frontend يعمل على domain مختلف، فقد تحتاج إلى التعامل مع CORS. هذا موضوع مهم جدًا في تطبيقات الويب الحديثة. Go يمكنها إضافة headers مناسبة للسماح أو تقييد الطلبات القادمة من مصادر معينة.

مثال بسيط:

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 == http.MethodOptions {
			w.WriteHeader(http.StatusNoContent)
			return
		}

		next.ServeHTTP(w, r)
	})
}

هذه نقطة مهمة جدًا عندما تربط API بـ React أو Vue أو أي frontend آخر.

الأداء في الإنتاج

Go غالبًا ممتازة في الإنتاج، لكن الأداء لا يعتمد على اللغة وحدها. يعتمد أيضًا على:

  • هيكلة الكود

  • جودة قاعدة البيانات

  • طريقة كتابة queries

  • استخدام caching

  • عدد الاتصالات

  • timeout

  • إدارة الذاكرة

  • logging

  • مراقبة الأخطاء

API سريعة ليست مجرد كود Go، بل نظام متكامل. قد تكتب كودًا ممتازًا، لكن قاعدة البيانات بطيئة، فيظهر التطبيق بطيئًا. لذلك لا تنخدع بفكرة أن اللغة وحدها تحل كل شيء. هي تساعد كثيرًا، لكن التصميم العام هو الأهم.

متى تختار Framework ومتى تكتفي بالمكتبة القياسية؟

إذا كنت تتعلم، أو تبني مشروعًا صغيرًا، أو تريد فهم كيف يعمل HTTP من الأساس، فابدأ بالمكتبة القياسية net/http. هذا أفضل طريق لفهم الجذور.

إذا كنت تبني مشروعًا أكبر وتحتاج إلى routing متقدم، وmiddleware، وbinding، وvalidation، وتجربة أسرع في البناء، فFramework مثل Gin أو Echo أو Fiber قد يوفر عليك الوقت.

القاعدة الذهبية هنا بسيطة: افهم الأساس أولًا، ثم استخدم الأداة التي تخدمك. لا تجعل الأداة تخفي عنك أساسيات التواصل بين العميل والخادم.

مثال API صغيرة منظمة

لنختم بمثال أنظف قليلًا، يجمع عدة أفكار معًا:

package main

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

type APIResponse struct {
	Success bool        `json:"success"`
	Data    interface{} `json:"data,omitempty"`
	Error   string      `json:"error,omitempty"`
}

type User struct {
	ID    int    `json:"id"`
	Name  string `json:"name"`
	Email string `json:"email"`
}

func writeJSON(w http.ResponseWriter, status int, payload APIResponse) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(status)
	json.NewEncoder(w).Encode(payload)
}

func usersHandler(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodGet {
		writeJSON(w, http.StatusMethodNotAllowed, APIResponse{
			Success: false,
			Error:   "method not allowed",
		})
		return
	}

	users := []User{
		{ID: 1, Name: "Hassan", Email: "hassan@example.com"},
		{ID: 2, Name: "Sara", Email: "sara@example.com"},
	}

	writeJSON(w, http.StatusOK, APIResponse{
		Success: true,
		Data:    users,
	})
}

func main() {
	http.HandleFunc("/users", usersHandler)
	http.ListenAndServe(":8080", nil)
}

هذا المثال بسيط، لكنه يوضح روح API جيدة: response موحد، status code مناسب، وبنية واضحة.

الخلاصة

التعامل مع API في تطبيقات Go ليس مجرد كتابة endpoints، بل هو بناء طريقة تواصل منظمة بين أجزاء النظام. عندما تفهم HTTP جيدًا، وتتعامل مع JSON بشكل صحيح، وتستخدم status codes بذكاء، وتبني validation واضحًا، وتفصل منطق العمل عن الـ handlers، فإنك تقترب كثيرًا من بناء API احترافية يمكن الاعتماد عليها.

Go تمنحك البيئة المناسبة لهذا النوع من العمل، لأنها سريعة، واضحة، عملية، ومناسبة جدًا للأنظمة الحديثة. ومع الوقت ستلاحظ أن جمال Go ليس فقط في الأداء، بل في البساطة التي تجعل الكود قابلًا للفهم بعد أشهر من كتابته. وهذا شيء لا يقدَّر بثمن، خاصة عندما تعمل على مشروع حقيقي فيه فريق، ووقت، وضغط، وتوقعات كثيرة.

إذا كنت في بداية الطريق، فلا تتعجل. ابدأ بمثال صغير، ابنِ endpoint بسيطًا، استقبل JSON، أعد JSON، جرّب validation، ثم أضف routing، ثم middleware، ثم authentication، ثم قاعدة بيانات. خطوة خطوة ستجد نفسك تبني APIs قوية فعلًا، لا مجرد أمثلة تعليمية. وفي كل مرة تكتب فيها handler نظيفًا أو response واضحًا، ستشعر بأنك لم تعد فقط تكتب كودًا، بل تبني نظامًا يستطيع الآخرون الاعتماد عليه.

وفي النهاية، Go تعطيك ما يحتاجه أي مطور Backend جيد: وضوح، قوة، انضباط، وسرعة. وإذا جمعت هذا مع فهم جيد للـ API، فستكون قد وضعت قدمك على طريق قوي جدًا في عالم تطوير الخوادم والتطبيقات الحديثة.

#Go API #التعامل مع API في Go #تطوير API بـ Go #REST API في Go #net/http #encoding/json #JSON في Go #بناء API #Go web development #Golang API tutorial #Golang REST API #backend Go