Go: خدمات HTTP في Go
آخر تحديث: 2026-08-26
حزمة
net/httpالموجودة في المكتبة القياسية للغة Go مزودة بكافة الميزات، مما يتيح لك إنشاء خدمات ويب جاهزة للاستخدام في بيئة الإنتاج دون الحاجة إلى استخدام أطر عمل تابعة لجهات خارجية.
عندما يقرر فريقك «استخدام المكتبات القياسية فقط دون إدخال أي أطر عمل ويب»، هل يمكنك كتابة مسارات وبرمجيات وسيطة واضحة تمامًا مثل Gin أو Echo؟ في هذا الدرس، ستتقن جميع التقنيات الأساسية لخدمات HTTP بلغة Go.
1. ستتعلم
http.ListenAndServeيبدأ تشغيل الخدمة- واجهة
HandlerومحولHandlerFunc ServeMuxتسجيل «روت»- Go 1.22: تحسين التوجيه: الطريقة + نمط المسار + معلمات المسار
- ترميز استجابة JSON
- تحليل معلمات الطلب (معلمات الاستعلام، معلمات النماذج، معلمات المسار)
- خدمة الملفات الثابتة
2. قصة حقيقية لمهندس برمجيات الخلفية
(1) المشكلة: واجهة برمجة تطبيقات (API) بسيطة — اخترنا Gin، لكن بعد ثلاثة أشهر، واجهنا عقبة أثناء عملية الترقية
أليس هي إحدى أعضاء فريق «الخلفية»، وتحتاج إلى إعداد واجهة برمجة تطبيقات REST لإدارة «المستخدمين»:
"لقد كتبت ثلاثة مسارات باستخدام إطار عمل Gin: GET /users، وPOST /users، وGET /users/:id. لكن بعد ثلاثة أشهر، تم إصدار Go 1.22، وأضيفت إلى المكتبة القياسية دعم أصلي لمعلمات الطرق والمسارات. والآن أرغب في إزالة التبعية لـ Gin، لكن سيتعين عليّ تغيير جميع توقيعات المعالجات — gin.Context مقابل http.ResponseWriter. قال مديري: «لا يستحق الأمر إعادة هيكلة مئات الأسطر من الكود لمجرد التخلص من تبعية واحدة.»"
قرارها في ذلك الوقت:
// Gin dependency version (wanted to migrate three months later)
r := gin.Default()
r.GET("/users", listUsers) // gin.Context
r.POST("/users", createUser) // gin.Context
r.GET("/users/:id", getUser) // gin.Context
// Want to migrate to the standard library? All handler signatures must change!
(2) حل لـ Go 1.22: التوجيه الأصلي في المكتبة القياسية
// Standard library version (Go 1.22+, no dependencies required)
package main
import (
"encoding/json"
"log"
"net/http"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
}
var users = []User{{ID: 1, Name: "Alice"}}
func main() {
mux := http.NewServeMux()
// Go 1.22 enhanced routing: method + path pattern + path parameters
mux.HandleFunc("GET /users", listUsers)
mux.HandleFunc("POST /users", createUser)
mux.HandleFunc("GET /users/{id}", getUser)
log.Println("Server started on :8080")
log.Fatal(http.ListenAndServe(":8080", mux))
}
// Standard handler signature: http.ResponseWriter + *http.Request
func listUsers(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 u User
if err := json.NewDecoder(r.Body).Decode(&u); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
u.ID = len(users) + 1
users = append(users, u)
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(u)
}
func getUser(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id") // Path parameter!
// Look up user...
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(User{ID: 1, Name: "Alice"})
}
(3) الأداء: نمط Gin مقابل المكتبة القياسية (Go 1.22)
| ميزة | Gin (مكتبة خارجية) | المكتبة القياسية (Go < 1.22) | المكتبة القياسية (Go 1.22+) |
|---|---|---|---|
| معلمة المسار | :id ✅ |
❌ يجب تحليلها يدويًّا | {id} ✅ |
| توجيه الأسلوب | ✅ | ❌ التحقق داخل المُعالج | "GET /path" ✅ |
| استجابة JSON | c.JSON() |
تعيين الرأس يدويًّا | تعيين الرأس يدويًّا |
| التبعيات | حزمة خارجية واحدة | 0 | 0 |
| الأداء | أبطأ قليلاً (انعكاس) | أصلي | أصلي |
net/http في Go 1.22 كافية لمعظم مشاريع الويب. إذا لم تكن بحاجة إلى ميزات خاصة بإطار العمل (مثل الربط/التحقق التلقائي أو نظام بيئي غني بالبرمجيات الوسيطة)، فأعطِ الأولوية للمكتبة القياسية.
3. أساسيات HTTP
▶ مثال: أبسط خدمة HTTP
package main
import (
"fmt"
"log"
"net/http"
)
func helloHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, %s!", r.URL.Query().Get("name"))
}
func main() {
http.HandleFunc("/hello", helloHandler)
log.Println("Server started on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
اختبار:
$ curl "http://localhost:8080/hello?name=Alice"
Hello, Alice!
(2) أنواع النوى
| النوع | الوصف |
|---|---|
http.ResponseWriter |
واجهة لكتابة استجابات HTTP |
*http.Request |
طلب HTTP، بما في ذلك عنوان URL والرؤوس والنص الأساسي والنموذج |
http.Handler |
الواجهة: ServeHTTP(w, r) |
http.HandlerFunc |
محول الدالة: يحول دالة عادية إلى معالج |
http.ServeMux |
معدد مسارات |
(3) شرح مفصل لواجهة المعالج
package main
import (
"fmt"
"log"
"net/http"
)
// Method 1: Implement the Handler interface
type Greeter struct {
Greeting string
}
func (g *Greeter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "%s, %s!", g.Greeting, r.URL.Path[1:])
}
// Method 2: HandlerFunc adapter
func helloHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, %s!", r.URL.Query().Get("name"))
}
func main() {
mux := http.NewServeMux()
// Struct Handler
mux.Handle("/greet", &Greeter{Greeting: "Welcome"})
// Function Handler (HandlerFunc automatic conversion)
mux.HandleFunc("/hello", helloHandler)
log.Print(http.ListenAndServe(":8080", mux))
}
4. Go 1.22: تحسينات في التوجيه
▶ مثال: الطريقة + نمط المسار + معلمة المسار
package main
import (
"encoding/json"
"log"
"net/http"
)
type Item struct {
ID int `json:"id"`
Name string `json:"name"`
Price float64 `json:"price"`
}
var items = []Item{
{ID: 1, Name: "Laptop", Price: 999.99},
{ID: 2, Name: "Mouse", Price: 29.99},
}
func main() {
mux := http.NewServeMux()
// Go 1.22 method + path pattern
mux.HandleFunc("GET /items", listItems)
mux.HandleFunc("POST /items", createItem)
mux.HandleFunc("GET /items/{id}", getItem)
mux.HandleFunc("PUT /items/{id}", updateItem)
mux.HandleFunc("DELETE /items/{id}", deleteItem)
// Wildcard suffix: path prefix matching
mux.HandleFunc("GET /items/{path...}", wildcardHandler)
log.Print(http.ListenAndServe(":8080", mux))
}
func listItems(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(items)
}
func createItem(w http.ResponseWriter, r *http.Request) {
var item Item
if err := json.NewDecoder(r.Body).Decode(&item); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
item.ID = len(items) + 1
items = append(items, item)
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(item)
}
func getItem(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
// Look up item...
_ = id
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(items[0])
}
func updateItem(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
_ = id
// Update logic...
w.WriteHeader(http.StatusNoContent)
}
func deleteItem(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
_ = id
// Delete logic...
w.WriteHeader(http.StatusNoContent)
}
func wildcardHandler(w http.ResponseWriter, r *http.Request) {
path := r.PathValue("path")
w.Header().Set("Content-Type", "text/plain")
http.Error(w, "Not found: "+path, http.StatusNotFound)
}
(2) مقارنة بين أوضاع التوجيه في Go 1.22
| الوضع | Go < 1.22 | Go 1.22+ | مثال |
|---|---|---|---|
| مطابقة الطرق | غير مدعوم (عبارة if داخل المعالج) | مدعوم | "GET /items" |
| معلمة المسار | غير مدعومة | {name} صيغة |
"GET /items/{id}" |
| لاحقة البدل | غير مدعومة | {path...} |
"GET /static/{file...}" |
| المسار الدقيق | /items |
"GET /items" |
تطابق تام مع /items |
| مطابقة البادئة | /items/ |
"GET /items/" |
تتطابق مع /items/... |
▶ مثال: أولوية المسار
package main
import (
"fmt"
"log"
"net/http"
)
func main() {
mux := http.NewServeMux()
// Exact path > prefix path
mux.HandleFunc("GET /items", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "items list")
})
mux.HandleFunc("GET /items/{id}", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "item %s\n", r.PathValue("id"))
})
mux.HandleFunc("GET /items/featured", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "featured items")
})
log.Print(http.ListenAndServe(":8080", mux))
}
اختبار:
$ curl localhost:8080/item
item list
$ curl localhost:8080/item/42
item 42
$ curl localhost:8080/item/featured
featured item # Exact match takes priority over {id} wildcard
5. الطلبات والردود
▶ مثال: معلمات الاستعلام، النماذج، JSON
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
)
type Response struct {
Method string `json:"method"`
Path string `json:"path"`
Query interface{} `json:"query,omitempty"`
Form interface{} `json:"form,omitempty"`
JSON interface{} `json:"json,omitempty"`
}
func handler(w http.ResponseWriter, r *http.Request) {
resp := Response{
Method: r.Method,
Path: r.URL.Path,
}
// Query parameters
if r.Method == http.MethodGet {
resp.Query = r.URL.Query()
}
// Form data
if r.Method == http.MethodPost {
contentType := r.Header.Get("Content-Type")
switch {
case contentType == "application/x-www-form-urlencoded":
r.ParseForm()
resp.Form = r.Form
case contentType == "application/json":
var body interface{}
json.NewDecoder(r.Body).Decode(&body)
resp.JSON = body
}
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(resp)
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", handler)
log.Print(http.ListenAndServe(":8080", mux))
}
▶ مثال: دالة مساعدة للاستجابة بتنسيق JSON
package main
import (
"encoding/json"
"log"
"net/http"
)
// JSON response utility function
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]string{"error": message})
}
type Product struct {
ID int `json:"id"`
Name string `json:"name"`
Price float64 `json:"price"`
}
func getProduct(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
// Simulate lookup
if id != "1" {
writeError(w, http.StatusNotFound, "product not found")
return
}
writeJSON(w, http.StatusOK, Product{ID: 1, Name: "Laptop", Price: 999.99})
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /products/{id}", getProduct)
log.Print(http.ListenAndServe(":8080", mux))
}
sequenceDiagram
participant Client as HTTP Client
participant Mux as ServeMux
participant Handler as Handler
Client->>Mux: GET /products/1
Mux->>Mux: Route matching
Mux->>Handler: ServeHTTP(w, r)
Handler->>Handler: r.PathValue("id") → "1"
Handler->>Handler: writeJSON(w, 200, product)
Handler-->>Client: HTTP 200 + JSON body
(3) مرجع سريع لرموز حالة HTTP
| الرمز | الثابت | الغرض |
|---|---|---|
| 200 | http.StatusOK |
نجاح |
| 201 | http.StatusCreated |
تم إنشاء المورد بنجاح |
| 204 | http.StatusNoContent |
نجحت العملية، لكن لا يوجد نص استجابة |
| 400 | http.StatusBadRequest |
خطأ في طلب العميل |
| 401 | http.StatusUnauthorized |
غير مصرح به |
| 403 | http.StatusForbidden |
لا يوجد إذن |
| 404 | http.StatusNotFound |
لم يتم العثور على المورد |
| 500 | http.StatusInternalServerError |
خطأ داخلي في الخادم |
6. خدمة الملفات الثابتة
▶ مثال: الملفات الثابتة
package main
import (
"log"
"net/http"
)
func main() {
mux := http.NewServeMux()
// Static file service: /static/ prefix → ./static/ directory
mux.Handle("GET /static/", http.StripPrefix("/static/",
http.FileServer(http.Dir("./static"))))
// Single file
mux.Handle("GET /favicon.ico", http.FileServer(http.Dir("./static")))
log.Print(http.ListenAndServe(":8080", mux))
}
7. مثال كامل: واجهة برمجة تطبيقات (API) للملاحظات بنمط REST
// notes_api.go
package main
import (
"encoding/json"
"log"
"net/http"
"strconv"
"sync"
"time"
)
// ---------- Model ----------
type Note struct {
ID int `json:"id"`
Title string `json:"title"`
Content string `json:"content"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// ---------- Store ----------
type NoteStore struct {
mu sync.RWMutex
notes map[int]Note
nextID int
}
func NewNoteStore() *NoteStore {
return &NoteStore{
notes: make(map[int]Note),
nextID: 1,
}
}
func (s *NoteStore) Create(title, content string) Note {
s.mu.Lock()
defer s.mu.Unlock()
n := Note{
ID: s.nextID,
Title: title,
Content: content,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
s.nextID++
s.notes[n.ID] = n
return n
}
func (s *NoteStore) List() []Note {
s.mu.RLock()
defer s.mu.RUnlock()
result := make([]Note, 0, len(s.notes))
for _, n := range s.notes {
result = append(result, n)
}
return result
}
func (s *NoteStore) Get(id int) (Note, bool) {
s.mu.RLock()
defer s.mu.RUnlock()
n, ok := s.notes[id]
return n, ok
}
func (s *NoteStore) Update(id int, title, content string) (Note, bool) {
s.mu.Lock()
defer s.mu.Unlock()
n, ok := s.notes[id]
if !ok {
return Note{}, false
}
n.Title = title
n.Content = content
n.UpdatedAt = time.Now()
s.notes[id] = n
return n, true
}
func (s *NoteStore) Delete(id int) bool {
s.mu.Lock()
defer s.mu.Unlock()
_, ok := s.notes[id]
if !ok {
return false
}
delete(s.notes, id)
return true
}
// ---------- API ----------
type NotesAPI struct {
store *NoteStore
}
func NewNotesAPI(store *NoteStore) *NotesAPI {
return &NotesAPI{store: store}
}
func (api *NotesAPI) Register(mux *http.ServeMux) {
mux.HandleFunc("GET /notes", api.ListNotes)
mux.HandleFunc("POST /notes", api.CreateNote)
mux.HandleFunc("GET /notes/{id}", api.GetNote)
mux.HandleFunc("PUT /notes/{id}", api.UpdateNote)
mux.HandleFunc("DELETE /notes/{id}", api.DeleteNote)
}
// Utility functions
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]string{"error": message})
}
// ---------- Handlers ----------
func (api *NotesAPI) ListNotes(w http.ResponseWriter, r *http.Request) {
notes := api.store.List()
writeJSON(w, http.StatusOK, notes)
}
func (api *NotesAPI) CreateNote(w http.ResponseWriter, r *http.Request) {
var input struct {
Title string `json:"title"`
Content string `json:"content"`
}
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
writeError(w, http.StatusBadRequest, "invalid JSON body")
return
}
if input.Title == "" {
writeError(w, http.StatusBadRequest, "title is required")
return
}
note := api.store.Create(input.Title, input.Content)
writeJSON(w, http.StatusCreated, note)
}
func (api *NotesAPI) GetNote(w http.ResponseWriter, r *http.Request) {
idStr := r.PathValue("id")
id, err := strconv.Atoi(idStr)
if err != nil {
writeError(w, http.StatusBadRequest, "invalid note ID")
return
}
note, ok := api.store.Get(id)
if !ok {
writeError(w, http.StatusNotFound, "note not found")
return
}
writeJSON(w, http.StatusOK, note)
}
func (api *NotesAPI) UpdateNote(w http.ResponseWriter, r *http.Request) {
idStr := r.PathValue("id")
id, err := strconv.Atoi(idStr)
if err != nil {
writeError(w, http.StatusBadRequest, "invalid note ID")
return
}
var input struct {
Title string `json:"title"`
Content string `json:"content"`
}
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
writeError(w, http.StatusBadRequest, "invalid JSON body")
return
}
note, ok := api.store.Update(id, input.Title, input.Content)
if !ok {
writeError(w, http.StatusNotFound, "note not found")
return
}
writeJSON(w, http.StatusOK, note)
}
func (api *NotesAPI) DeleteNote(w http.ResponseWriter, r *http.Request) {
idStr := r.PathValue("id")
id, err := strconv.Atoi(idStr)
if err != nil {
writeError(w, http.StatusBadRequest, "invalid note ID")
return
}
if !api.store.Delete(id) {
writeError(w, http.StatusNotFound, "note not found")
return
}
w.WriteHeader(http.StatusNoContent)
}
func main() {
store := NewNoteStore()
api := NewNotesAPI(store)
mux := http.NewServeMux()
api.Register(mux)
log.Println("Note API started on :8080")
log.Println("Available endpoints:")
log.Println(" GET /notes — List all notes")
log.Println(" POST /notes — Create a note")
log.Println(" GET /notes/{id} — Get a single note")
log.Println(" PUT /notes/{id} — Update a note")
log.Println(" DELETE /notes/{id} — Delete a note")
log.Fatal(http.ListenAndServe(":8080", mux))
}
http.Error(w, msg, code) بتعيين Content-Type إلى JSON. إذا كنت تُرجع خطأً في JSON، فاستخدم json.NewEncoder(w).Encode(errResp) وقم بتعيين Header() يدويًّا. يُرجع http.Error() في المكتبة القياسية نصًّا عاديًّا.
❓ أسئلة شائعة
Handler وHandlerFunc؟Handler هي واجهة (تتطلب تنفيذ طريقة ServeHTTP)، بينما HandlerFunc هو مُحَوِّل من نوع الدالة — فهو يسمح للدوال العادية بالامتثال تلقائيًا لواجهة Handler. والاثنان متكافئان تمامًا: mux.Handle("/path", handler) وmux.HandleFunc("/path", handlerFunc) لهما نفس التأثير.r.PathValue("name") لاسترداد معلمات المسار من النوع {name}. في الإصدارات الأقدم، تحتاج إلى تحليلها يدويًا من r.URL.Path أو استخدام مكتبة تابعة لجهة خارجية. يجب أن تتطابق أسماء معلمات المسار مع {name} في نمط الجذر.ListenAndServe وListenAndServeTLS؟http.ListenAndServeTLS(":443", "cert.pem", "key.pem", mux).server.Shutdown(ctx) بالاقتران مع os/signal لالتقاط إشارات الإنهاء. سيقوم Shutdown بالانتظار حتى تتم معالجة جميع الاتصالات النشطة قبل الإغلاق. لا تستخدم server.Close() — فهو سيؤدي إلى إنهاء الطلبات قيد المعالجة حالياً بشكل قسري.nil كمعلمة ثانية إلى http.ListenAndServe؟nil إلى استخدام http.DefaultServeMux (الموجه الافتراضي العام). يُنصح بإنشاء http.NewServeMux() بشكل صريح لتجنب إفساد التوجيه العام — خاصةً عند عزل المسارات أثناء الاختبار.📖 ملخص
http.ListenAndServeيبدأ تشغيل خدمة HTTP- واجهة
Handler+ محولHandlerFunc - Go 1.22: تحسينات التوجيه:
"METHOD /path/{param}"بناء الجملة r.PathValue("name")يسترد معلمة المسار- استجابة JSON: Set
Content-Type+json.NewEncoder - تحليل الطلبات: معلمات الاستعلام، النماذج، نص JSON
- خدمة الملفات الثابتة:
http.FileServer - رمز الحالة: استخدم الثوابت
http.Status*
📝 تمارين
-
تمرين أساسي (مستوى الصعوبة ⭐): أنشئ خدمة HTTP بسيطة وقم بتسجيل ثلاثة مسارات:
GET /timeتعرض الوقت الحالي بتنسيق JSON، وGET /healthتعرض{"status": "ok"}، وGET /versionتعرض رقم الإصدار. استخدم بناء الجملة المحسّن للتوجيه في Go 1.22. -
مشكلة متقدمة (صعوبة ⭐⭐): قم بتنفيذ واجهة برمجة تطبيقات (API) لقائمة المهام. المتطلبات: (1) عمليات CRUD كاملة؛ (2) استخدام طرق Go 1.22 والتوجيه القائم على المسار؛ (3) طلبات واستجابات بتنسيق JSON؛ (4) التخزين في الذاكرة (map + حماية RWMutex)؛ (5) إرجاع رموز حالة HTTP المناسبة.
-
التحدي (الصعوبة: ⭐⭐⭐): قم بتنفيذ خدمة تقصير عناوين URL. المتطلبات: (1) تقبل
POST /shortenعنوان URL طويلًا وتُرجع رمزًا قصيرًا (سلسلة عشوائية مكونة من 6 أحرف)؛ (2) يجب أن تقومGET /{code}بإجراء إعادة توجيه 301 إلى عنوان URL الأصلي؛ (3) إحصائيات الوصول: يجب أن تُرجعGET /stats/{code}العدد للزيارات؛ (4) استخدم-raceللتحقق من أمان التزامن؛ (5) استخدم RWMutex لحماية عداد الإحصائيات.