Go: تطبيق واجهة برمجة التطبيقات REST عمليًّا
واجهة برمجة التطبيقات (REST API) لا تقتصر على عمليات CRUD فحسب — بل تشمل تصميم الموارد، واختيار رموز الحالة، وتوحيد تنسيق الأخطاء، وسلاسل البرامج الوسيطة: فكل تفصيل من هذه التفاصيل يحدد جودة واجهة برمجة التطبيقات على مستوى الإنتاج.
عندما يتعين استدعاء واجهة برمجة التطبيقات (API) الخاصة بك من قِبل كل من تطبيقات الواجهة الأمامية وخدمات الجهات الخارجية، فإن وجود تنسيق موحد للأخطاء، ورموز حالة مناسبة، وإدارة واضحة للإصدارات لم يعد مجرد «ميزات إضافية» — بل أصبح «متطلبات أساسية».
1. ستتعلم
- مبادئ تصميم RESTful (الموارد/الأفعال/رموز الحالة)
- إنشاء واجهة برمجة تطبيقات REST باستخدام مسارات Go 1.22
- التحقق من صحة معلمات الطلب
- تكامل سلسلة البرامج الوسيطة
- توحيد تنسيق استجابة الأخطاء
- سياسة إدارة إصدارات واجهة برمجة التطبيقات (API)
2. قصة حقيقية من أحد المتعاونين في مجال الواجهة الأمامية
(1) المشكلة: تختلف تنسيقات أخطاء واجهة برمجة التطبيقات (API) باختلاف الواجهة، مما يتسبب في تعطل الواجهة الأمامية
تتعاون فرق «الخلفية» و«الواجهة» في شركة «أليس» على مشروع للتجارة الإلكترونية:
«قال زملائي في قسم الواجهة الأمامية: "تنسيقات الأخطاء تختلف من واجهة برمجة تطبيقات (API) إلى أخرى. فقائمة المستخدمين تُرجع
{"error":"not found"}، وواجهة برمجة تطبيقات الطلبات تُرجع{"message":"Order not found","code":404}، أما واجهة برمجة تطبيقات المنتجات فتُرجع صفحة خطأ 500 فحسب. وأنا مضطر لكتابة كود مختلف لمعالجة الأخطاء لكل واجهة برمجة تطبيقات!"»
// Bad code: inconsistent error formats
// GET /users/1 → {"error":"not found"} ← Format A
// GET /orders/1 → {"message":"Order not found","code":404} ← Format B
// GET /products → <html>500 Internal Error</html> ← Format C
(2) حل Go: استجابات الخطأ الموحدة
// Unified error format
type APIError struct {
Code int `json:"code"`
Message string `json:"message"`
Detail string `json:"detail,omitempty"`
}
func (e *APIError) Error() string {
return e.Message
}
// Factory functions
func NotFound(msg string) *APIError {
return &APIError{Code: 404, Message: "not_found", Detail: msg}
}
func BadRequest(msg string) *APIError {
return &APIError{Code: 400, Message: "bad_request", Detail: msg}
}
func InternalError(msg string) *APIError {
return &APIError{Code: 500, Message: "internal_error", Detail: msg}
}
(3) الإيرادات: قبل التوحيد وبعده
| البعد | غير متسق | التنسيق الموحد |
|---|---|---|
| المعالجة الأمامية | كتابة منطق مختلف لكل واجهة برمجة تطبيقات | if (resp.error) handleError(resp) |
| تكلفة التوثيق | توثيق منفصل لكل واجهة برمجة تطبيقات | تنسيق الوصف المكون من جملة واحدة |
| إنشاء SDK | لا يمكن أتمتته | إنشاء عميل مباشرةً عبر OpenAPI |
| تكاليف تصحيح الأخطاء | التحقق من التنسيق المحدد في كل مرة | أسماء الحقول الموحدة |
3. مبادئ تصميم RESTful
(1) تصميم الموارد
// Good RESTful URL design:
// Resource (noun) + Verb (HTTP method)
// Single resource
GET /users → List (collection)
POST /users → Create
GET /users/{id} → View single
PUT /users/{id} → Full update
PATCH /users/{id} → Partial update
DELETE /users/{id} → Delete
// Sub-resource
GET /users/{id}/orders → User's order list
POST /users/{id}/orders → Create order for user
GET /users/{id}/orders/{oid} → User's specific order
// Actions (use verbs for non-CRUD)
POST /users/{id}/activate → Activate user
POST /orders/{id}/cancel → Cancel order
(2) اختيار رمز الحالة
package main
import (
"encoding/json"
"fmt"
"net/http"
)
// Unified response
type Response struct {
Data interface{} `json:"data,omitempty"`
Error *APIError `json:"error,omitempty"`
Meta *Meta `json:"meta,omitempty"`
}
type APIError struct {
Code int `json:"code"`
Message string `json:"message"`
Detail string `json:"detail,omitempty"`
}
type Meta struct {
Total int `json:"total"`
Page int `json:"page"`
PerPage int `json:"per_page"`
}
func writeJSON(w http.ResponseWriter, status int, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(data)
}
func main() {
fmt.Println("Response types: Response, APIError, Meta")
fmt.Println("Helper: writeJSON(w, status, data)")
}
| رمز الحالة | الطريقة | المعنى |
|---|---|---|
| 200 OK | GET | تم تنفيذ الاستعلام بنجاح |
| 201 تم الإنشاء | نشر | تم الإنشاء بنجاح |
| 204 لا يوجد محتوى | حذف | تم الحذف بنجاح |
| 400 طلب غير صحيح | — | معلمات طلب غير صالحة |
| 401 غير مصرح به | — | غير مصرح به |
| 403 ممنوع | — | لا يوجد إذن |
| 404 غير موجود | — | المورد غير موجود |
| 409 تعارض | POST/PUT | تعارض في الموارد (مثل إنشاء نسخة مكررة) |
| 422 غير قابل للمعالجة | POST/PUT | خطأ دلالي في نص الطلب |
| 429 عدد زائد | — | حد عدد الطلبات |
| 500 خطأ داخلي | — | خطأ في الخادم |
4. تجربة عملية مع واجهة برمجة التطبيقات REST
▶ مثال: واجهة برمجة تطبيقات CRUD للمستخدم
⚙️ المتطلبات المسبقة: قم بتشغيل
go get github.com/mattn/go-sqlite3(في حالة استخدام SQLite)
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"strconv"
"sync"
"time"
)
// ---------- Model ----------
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
CreatedAt time.Time `json:"created_at"`
}
type CreateUserRequest struct {
Name string `json:"name"`
Email string `json:"email"`
}
type UpdateUserRequest struct {
Name string `json:"name"`
Email string `json:"email"`
}
// ---------- Store ----------
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) List() []User {
s.mu.RLock()
defer s.mu.RUnlock()
result := make([]User, 0, len(s.users))
for _, u := range s.users {
result = append(result, u)
}
return result
}
func (s *UserStore) Create(req CreateUserRequest) User {
s.mu.Lock()
defer s.mu.Unlock()
u := User{
ID: s.nextID,
Name: req.Name,
Email: req.Email,
CreatedAt: time.Now(),
}
s.nextID++
s.users[u.ID] = u
return u
}
func (s *UserStore) Get(id int) (User, bool) {
s.mu.RLock()
defer s.mu.RUnlock()
u, ok := s.users[id]
return u, ok
}
func (s *UserStore) Update(id int, req UpdateUserRequest) (User, bool) {
s.mu.Lock()
defer s.mu.Unlock()
u, ok := s.users[id]
if !ok {
return User{}, false
}
u.Name = req.Name
u.Email = req.Email
s.users[id] = u
return u, true
}
func (s *UserStore) Delete(id int) bool {
s.mu.Lock()
defer s.mu.Unlock()
_, ok := s.users[id]
if !ok {
return false
}
delete(s.users, id)
return true
}
// ---------- Handler ----------
type UserHandler struct {
store *UserStore
}
func NewUserHandler(store *UserStore) *UserHandler {
return &UserHandler{store: store}
}
func (h *UserHandler) Register(mux *http.ServeMux) {
mux.HandleFunc("GET /api/v1/users", h.ListUsers)
mux.HandleFunc("POST /api/v1/users", h.CreateUser)
mux.HandleFunc("GET /api/v1/users/{id}", h.GetUser)
mux.HandleFunc("PUT /api/v1/users/{id}", h.UpdateUser)
mux.HandleFunc("DELETE /api/v1/users/{id}", h.DeleteUser)
}
// Unified response
func writeJSON(w http.ResponseWriter, status int, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(data)
}
func writeError(w http.ResponseWriter, status int, message string) {
writeJSON(w, status, map[string]interface{}{
"error": map[string]interface{}{
"code": status,
"message": message,
},
})
}
func (h *UserHandler) ListUsers(w http.ResponseWriter, r *http.Request) {
users := h.store.List()
writeJSON(w, http.StatusOK, map[string]interface{}{
"data": users,
"meta": map[string]int{"total": len(users)},
})
}
func (h *UserHandler) CreateUser(w http.ResponseWriter, r *http.Request) {
var req CreateUserRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid JSON body")
return
}
// Parameter validation
if req.Name == "" {
writeError(w, http.StatusBadRequest, "name is required")
return
}
if req.Email == "" {
writeError(w, http.StatusBadRequest, "email is required")
return
}
user := h.store.Create(req)
writeJSON(w, http.StatusCreated, map[string]interface{}{
"data": user,
})
}
func (h *UserHandler) GetUser(w http.ResponseWriter, r *http.Request) {
id, err := strconv.Atoi(r.PathValue("id"))
if err != nil {
writeError(w, http.StatusBadRequest, "invalid user ID")
return
}
user, ok := h.store.Get(id)
if !ok {
writeError(w, http.StatusNotFound, "user not found")
return
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"data": user,
})
}
func (h *UserHandler) UpdateUser(w http.ResponseWriter, r *http.Request) {
id, err := strconv.Atoi(r.PathValue("id"))
if err != nil {
writeError(w, http.StatusBadRequest, "invalid user ID")
return
}
var req UpdateUserRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid JSON body")
return
}
user, ok := h.store.Update(id, req)
if !ok {
writeError(w, http.StatusNotFound, "user not found")
return
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"data": user,
})
}
func (h *UserHandler) DeleteUser(w http.ResponseWriter, r *http.Request) {
id, err := strconv.Atoi(r.PathValue("id"))
if err != nil {
writeError(w, http.StatusBadRequest, "invalid user ID")
return
}
if !h.store.Delete(id) {
writeError(w, http.StatusNotFound, "user not found")
return
}
w.WriteHeader(http.StatusNoContent)
}
func main() {
store := NewUserStore()
handler := NewUserHandler(store)
mux := http.NewServeMux()
handler.Register(mux)
log.Println("User API started on :8080")
log.Fatal(http.ListenAndServe(":8080", mux))
}
5. التحقق من صحة المعلمات
▶ مثال: التحقق المنظم
package main
import (
"encoding/json"
"fmt"
"net/http"
"regexp"
"strings"
)
// Validator
type Validator struct {
errors []string
}
func (v *Validator) Required(field, value string) {
if strings.TrimSpace(value) == "" {
v.errors = append(v.errors, fmt.Sprintf("%s is required", field))
}
}
func (v *Validator) MinLength(field, value string, min int) {
if len(value) < min {
v.errors = append(v.errors, fmt.Sprintf("%s must be at least %d characters", field, min))
}
}
func (v *Validator) MaxLength(field, value string, max int) {
if len(value) > max {
v.errors = append(v.errors, fmt.Sprintf("%s must be at most %d characters", field, max))
}
}
func (v *Validator) Email(field, value string) {
pattern := `^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`
matched, _ := regexp.MatchString(pattern, value)
if !matched {
v.errors = append(v.errors, fmt.Sprintf("%s is not a valid email", field))
}
}
func (v *Validator) Valid() bool {
return len(v.errors) == 0
}
func (v *Validator) Errors() []string {
return v.errors
}
// ---------- Usage ----------
type SignupRequest struct {
Name string `json:"name"`
Email string `json:"email"`
Password string `json:"password"`
Age int `json:"age"`
}
func validateSignup(req SignupRequest) *Validator {
v := &Validator{}
v.Required("name", req.Name)
v.MinLength("name", req.Name, 2)
v.MaxLength("name", req.Name, 50)
v.Required("email", req.Email)
v.Email("email", req.Email)
v.Required("password", req.Password)
v.MinLength("password", req.Password, 8)
return v
}
func signupHandler(w http.ResponseWriter, r *http.Request) {
var req SignupRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid JSON")
return
}
if v := validateSignup(req); !v.Valid() {
writeJSON(w, http.StatusUnprocessableEntity, map[string]interface{}{
"error": map[string]interface{}{
"code": 422,
"message": "validation failed",
"details": v.Errors(),
},
})
return
}
writeJSON(w, http.StatusCreated, map[string]string{"status": "ok"})
}
func main() {
v := validateSignup(SignupRequest{Name: "A", Email: "bad", Password: "123"})
if !v.Valid() {
fmt.Println("Validation errors:", v.Errors())
}
}
6. تكامل سلسلة البرامج الوسيطة
▶ مثال: سلسلة برامج الوسيطة لواجهة برمجة التطبيقات (API)
package main
import (
"context"
"encoding/json"
"log"
"net/http"
"strings"
"time"
)
type contextKey string
const UserContextKey contextKey = "user"
// ---------- Middleware ----------
type Middleware func(http.Handler) http.Handler
func Chain(h http.Handler, mws ...Middleware) http.Handler {
for i := len(mws) - 1; i >= 0; i-- {
h = mws[i](h)
}
return h
}
// Request logging
func RequestLogging(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
log.Printf("[%s] %s %s", r.Method, r.URL.Path, r.RemoteAddr)
next.ServeHTTP(w, r)
log.Printf("[%s] %s → %v", r.Method, r.URL.Path, time.Since(start))
})
}
// Auth (simple Token)
func Auth(token string) Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
auth := r.Header.Get("Authorization")
if !strings.HasPrefix(auth, "Bearer ") {
writeError(w, http.StatusUnauthorized, "missing or invalid token")
return
}
if auth[7:] != token {
writeError(w, http.StatusForbidden, "invalid token")
return
}
// Inject user info into Context
ctx := context.WithValue(r.Context(), UserContextKey, "admin")
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
// Request timeout
func RequestTimeout(timeout time.Duration) Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), timeout)
defer cancel()
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
// CORS
func CORS(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)
})
}
// Recovery
func Recovery(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)
writeError(w, http.StatusInternalServerError, "internal server error")
}
}()
next.ServeHTTP(w, r)
})
}
// Helper functions
func writeError(w http.ResponseWriter, status int, msg string) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(map[string]interface{}{
"error": map[string]interface{}{
"code": status,
"message": msg,
},
})
}
func writeJSON(w http.ResponseWriter, status int, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(data)
}
// ---------- Main program ----------
func helloHandler(w http.ResponseWriter, r *http.Request) {
user := r.Context().Value(UserContextKey)
writeJSON(w, http.StatusOK, map[string]interface{}{
"message": "Hello, " + user.(string),
})
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /api/hello", helloHandler)
// Middleware chain
handler := Chain(mux,
Recovery,
RequestLogging,
CORS,
Auth("secret-token"),
RequestTimeout(5*time.Second),
)
log.Println("API started on :8080")
log.Fatal(http.ListenAndServe(":8080", handler))
}
7. مثال كامل: واجهة برمجة تطبيقات نظام إدارة المكتبات
// book_api.go
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"strconv"
"strings"
"sync"
"time"
)
// ---------- Models ----------
type Book struct {
ID int `json:"id"`
Title string `json:"title"`
Author string `json:"author"`
ISBN string `json:"isbn"`
Year int `json:"year"`
Available bool `json:"available"`
CreatedAt time.Time `json:"created_at"`
}
type CreateBookRequest struct {
Title string `json:"title"`
Author string `json:"author"`
ISBN string `json:"isbn"`
Year int `json:"year"`
}
type UpdateBookRequest struct {
Title string `json:"title"`
Author string `json:"author"`
Available *bool `json:"available"`
}
// ---------- Validator ----------
type ValidationError struct {
Field string `json:"field"`
Message string `json:"message"`
}
func validateCreateBook(req CreateBookRequest) []ValidationError {
var errs []ValidationError
if strings.TrimSpace(req.Title) == "" {
errs = append(errs, ValidationError{"title", "title is required"})
}
if strings.TrimSpace(req.Author) == "" {
errs = append(errs, ValidationError{"author", "author is required"})
}
if strings.TrimSpace(req.ISBN) == "" {
errs = append(errs, ValidationError{"isbn", "ISBN is required"})
}
if req.Year < 1000 || req.Year > 2100 {
errs = append(errs, ValidationError{"year", "year must be between 1000 and 2100"})
}
return errs
}
// ---------- Store ----------
type BookStore struct {
mu sync.RWMutex
books map[int]Book
nextID int
}
func NewBookStore() *BookStore {
return &BookStore{
books: make(map[int]Book),
nextID: 1,
}
}
func (s *BookStore) List() []Book {
s.mu.RLock()
defer s.mu.RUnlock()
result := make([]Book, 0, len(s.books))
for _, b := range s.books {
result = append(result, b)
}
return result
}
func (s *BookStore) Create(req CreateBookRequest) (Book, error) {
s.mu.Lock()
defer s.mu.Unlock()
// Check ISBN uniqueness
for _, b := range s.books {
if b.ISBN == req.ISBN {
return Book{}, fmt.Errorf("ISBN already exists: %s", req.ISBN)
}
}
book := Book{
ID: s.nextID,
Title: req.Title,
Author: req.Author,
ISBN: req.ISBN,
Year: req.Year,
Available: true,
CreatedAt: time.Now(),
}
s.nextID++
s.books[book.ID] = book
return book, nil
}
func (s *BookStore) Get(id int) (Book, bool) {
s.mu.RLock()
defer s.mu.RUnlock()
b, ok := s.books[id]
return b, ok
}
func (s *BookStore) Update(id int, req UpdateBookRequest) (Book, bool, error) {
s.mu.Lock()
defer s.mu.Unlock()
b, ok := s.books[id]
if !ok {
return Book{}, false, nil
}
if req.Title != "" {
b.Title = req.Title
}
if req.Author != "" {
b.Author = req.Author
}
if req.Available != nil {
b.Available = *req.Available
}
s.books[id] = b
return b, true, nil
}
func (s *BookStore) Delete(id int) bool {
s.mu.Lock()
defer s.mu.Unlock()
_, ok := s.books[id]
if !ok {
return false
}
delete(s.books, id)
return true
}
// ---------- API Response ----------
type APIResponse struct {
Data interface{} `json:"data,omitempty"`
Error *APIError `json:"error,omitempty"`
Meta *Meta `json:"meta,omitempty"`
}
type APIError struct {
Code int `json:"code"`
Message string `json:"message"`
Details []ValidationError `json:"details,omitempty"`
}
type Meta struct {
Total int `json:"total"`
}
func respond(w http.ResponseWriter, status int, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(APIResponse{Data: data})
}
func respondError(w http.ResponseWriter, status int, msg string, details ...[]ValidationError) {
err := APIError{Code: status, Message: msg}
if len(details) > 0 {
err.Details = details[0]
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(APIResponse{Error: &err})
}
// ---------- Handlers ----------
type BookHandler struct {
store *BookStore
}
func NewBookHandler(store *BookStore) *BookHandler {
return &BookHandler{store: store}
}
func (h *BookHandler) Register(mux *http.ServeMux, basePath string) {
mux.HandleFunc("GET "+basePath, h.ListBooks)
mux.HandleFunc("POST "+basePath, h.CreateBook)
mux.HandleFunc("GET "+basePath+"/{id}", h.GetBook)
mux.HandleFunc("PUT "+basePath+"/{id}", h.UpdateBook)
mux.HandleFunc("DELETE "+basePath+"/{id}", h.DeleteBook)
}
func (h *BookHandler) ListBooks(w http.ResponseWriter, r *http.Request) {
books := h.store.List()
respond(w, http.StatusOK, map[string]interface{}{
"items": books,
"meta": Meta{Total: len(books)},
})
}
func (h *BookHandler) CreateBook(w http.ResponseWriter, r *http.Request) {
var req CreateBookRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondError(w, http.StatusBadRequest, "invalid JSON body")
return
}
if errs := validateCreateBook(req); len(errs) > 0 {
respondError(w, http.StatusUnprocessableEntity, "validation failed", errs)
return
}
book, err := h.store.Create(req)
if err != nil {
respondError(w, http.StatusConflict, err.Error())
return
}
respond(w, http.StatusCreated, book)
}
func (h *BookHandler) GetBook(w http.ResponseWriter, r *http.Request) {
id, err := strconv.Atoi(r.PathValue("id"))
if err != nil {
respondError(w, http.StatusBadRequest, "invalid book ID")
return
}
book, ok := h.store.Get(id)
if !ok {
respondError(w, http.StatusNotFound, "book not found")
return
}
respond(w, http.StatusOK, book)
}
func (h *BookHandler) UpdateBook(w http.ResponseWriter, r *http.Request) {
id, err := strconv.Atoi(r.PathValue("id"))
if err != nil {
respondError(w, http.StatusBadRequest, "invalid book ID")
return
}
var req UpdateBookRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondError(w, http.StatusBadRequest, "invalid JSON body")
return
}
book, ok, err := h.store.Update(id, req)
if err != nil {
respondError(w, http.StatusInternalServerError, err.Error())
return
}
if !ok {
respondError(w, http.StatusNotFound, "book not found")
return
}
respond(w, http.StatusOK, book)
}
func (h *BookHandler) DeleteBook(w http.ResponseWriter, r *http.Request) {
id, err := strconv.Atoi(r.PathValue("id"))
if err != nil {
respondError(w, http.StatusBadRequest, "invalid book ID")
return
}
if !h.store.Delete(id) {
respondError(w, http.StatusNotFound, "book not found")
return
}
w.WriteHeader(http.StatusNoContent)
}
// ---------- Middleware ----------
func RequestLogging(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
log.Printf("[%s] %s %s", r.Method, r.URL.Path, r.RemoteAddr)
next.ServeHTTP(w, r)
log.Printf("[%s] %s → %v", r.Method, r.URL.Path, time.Since(start))
})
}
func Recovery(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)
respondError(w, http.StatusInternalServerError, "internal server error")
}
}()
next.ServeHTTP(w, r)
})
}
// ---------- Main ----------
func main() {
store := NewBookStore()
handler := NewBookHandler(store)
mux := http.NewServeMux()
handler.Register(mux, "/api/v1/books")
// Health check
mux.HandleFunc("GET /api/health", func(w http.ResponseWriter, r *http.Request) {
respond(w, http.StatusOK, map[string]string{"status": "ok"})
})
// Middleware chain
app := Recovery(RequestLogging(mux))
log.Println("Book API started on :8080")
log.Println("Endpoints:")
log.Println(" GET /api/v1/books — Book list")
log.Println(" POST /api/v1/books — Create book")
log.Println(" GET /api/v1/books/{id} — View book")
log.Println(" PUT /api/v1/books/{id} — Update book")
log.Println(" DELETE /api/v1/books/{id} — Delete book")
log.Fatal(http.ListenAndServe(":8080", app))
}
graph LR
subgraph API [REST API Layer]
H[Handler]
M[Middleware Chain]
V[Validator]
end
subgraph Store [Store Layer]
R[Repository<br/>map + RWMutex]
end
subgraph Model [Model Layer]
Book
User
Order
end
Request --> M
M --> H
H --> V
V --> R
R --> Book
interface{} كنوع استجابة — حيث يتم تسلسل القيمة الصفرية للخريطة إلى null في JSON، وليس {}. من أفضل الممارسات تعريف بنية استجابة صريحة (مثل APIResponse{Data, Error, Meta}) لضمان ظهور الحقول عندما تحتوي على قيم وحذفها عندما تكون فارغة (omitempty).
❓ أسئلة شائعة
/api/v1/ (الأكثر شيوعًا)؛ (2) رأس الطلب Accept: application/vnd.api+json; version=1؛ (3) معلمة الاستعلام ?v=1؛ (4) النطاق الفرعي v1.api.example.com. يُوصى باستخدام مسار URL — فهو الأكثر بديهية ويتطلب أقل تكاليف للتطوير وتصحيح الأخطاء.APIError تتضمن حقولًا للرمز والرسالة والتفاصيل. يجب على جميع معالجات الأخطاء استخدام الدالة الموحدة respondError(w, status, msg). ولا يتعين على الواجهة الأمامية سوى التحقق من if resp.error لعرض الخطأ، دون الحاجة إلى الاهتمام بواجهة برمجة التطبيقات (API) المحددة.mux.HandleFunc("/", func(w, r) { writeError(w, 404, "not found") }). لاحظ أنه يجب تسجيل هذا المعالج في النهاية، لأن ServeMux يستخدم التوجيه القائم على أفضل تطابق.?page=1&per_page=20. يقوم المعالج (Handler) بتحليل المعلمات، وتقوم طبقة التخزين (Store) بتنفيذ الاستعلام باستخدام LIMIT و OFFSET. وتُرجع الاستجابة meta: {total, page, per_page} إلى الواجهة الأمامية لحساب مكون ترقيم الصفحات. لا تناسب صيغة {path...} في توجيه Go 1.22 معلمات ترقيم الصفحات — يجب أن تكون المعلمات في سلسلة الاستعلام.📖 ملخص
- تصميم REST: المورد (اسم) + الطريقة (فعل) + رمز الحالة
- إدارة إصدارات عناوين URL: يُوصى بشدة باتباع نهج
/api/v1/ - توحيد تنسيق الأخطاء:
{error: {code, message, details}} - التحقق من صحة المعلمات: التحقق الأساسي (المعالج) + التحقق من صحة البيانات التجارية (الخدمة)
- سلسلة البرامج الوسيطة: الاستعادة → التسجيل → CORS → المصادقة → انتهاء المهلة → المعالج
- وظائف الاستجابة الموحدة:
respond()/respondError() - ترقيم الصفحات:
?page=N&per_page=N+ المعلومات الوصفية
📝 تمارين
-
تمرين أساسي (مستوى الصعوبة ⭐): قم بإنشاء واجهة برمجة تطبيقات REST خاصة بالمؤلف. المتطلبات: (1) وظائف CRUD كاملة؛ (2) استخدام المسار
/api/v1/authors؛ (3) تنسيق أخطاء موحد؛ (4) التحقق الأساسي من صحة المعلمات (الاسم مطلوب). اختبر جميع نقاط النهاية باستخدام curl. -
تمرين متقدم (مستوى الصعوبة ⭐⭐): قم بتنفيذ واجهة برمجة تطبيقات (API) للمقالات تربط المقالات بالمؤلفين. المتطلبات: (1)
POST /articlesلإنشاء مقال (مرتبط بمؤلف موجود)؛ (2)GET /articles?author_id=Xللتصفية حسب المؤلف؛ (3) دعم ترقيم الصفحات (باستخدام المعلمتينpageوper_page)؛ (4) استخدام تنسيق استجابة موحد:{data, meta}؛ (5) التحقق من أنtitleوcontentغير فارغتين. -
التحدي (الصعوبة: ⭐⭐⭐): تنفيذ نظام لإدارة المستخدمين مع سلسلة كاملة من البرمجيات الوسيطة. المتطلبات: (1) المسارات: المستخدمون (CRUD) + المصادقة (تسجيل الدخول/التسجيل)؛ (2) البرامج الوسيطة: الاسترداد → التسجيل → CORS → RateLimit (token bucket) → المصادقة (Bearer Token) → انتهاء المهلة؛ (3) تشفير كلمات المرور باستخدام bcrypt أثناء التسجيل؛ إرجاع JWT عند تسجيل الدخول؛ (4) يجب أن تقوم البرمجيات الوسيطة للمصادقة بتحليل معرّف المستخدم من JWT وإدراجه في السياق؛ (5) استخدم
-raceللتحقق من أمان التزامن.