Menu
flag Ar iconالعربيةdown icon

خادم HTTP في Golang: التوجيه في net/http وJSON ورموز الحالة

كيف تبني خادم ويب بالحزمة القياسية net/http في Go: المعالجات، التوجيه بـ ServeMux مع الطرق ومتغيّرات المسار (Go 1.22)، ردود JSON، رموز الحالة، الطبقات الوسيطة، المهل والإيقاف اللطيف.

تحتوي هذه الصفحة على محررات قابلة للتشغيل - حرّر، شغّل، وشاهد النتيجة فوراً.

خادم في بضعة أسطر

المعالج (handler) دالة تتلقّى الطلب وتكتب الردّ. ويوجّه ServeMux الطلبات إلى المعالجات.

لا يستطيع المحرّر قبول اتصالات من متصفّحك، لذا تشغّل أمثلة هذه الصفحة الخادم بـ httptest.NewServer وتستدعيه من البرنامج نفسه. في برنامج حقيقي يُستبدل الجزء الأخير بسطر واحد يتوقّف ويخدم إلى الأبد:

log.Fatal(http.ListenAndServe(":8080", mux))

وعندها يطبع curl localhost:8080/hello/gopher النص Hello, gopher!. لا تعود ListenAndServe إلا عند خطأ (المنفذ محجوز مثلًا)، ولهذا تُغلّف بـ log.Fatal.

كل طلب يعمل في goroutine خاصة به. أي شيء تتشاركه معالجاتك، مثل خريطة أو عدّاد، يحتاج mutex.

المعالجات

أي شيء له تابع ServeHTTP(http.ResponseWriter, *http.Request) هو http.Handler. يكيّف http.HandlerFunc دالة عادية مع هذه الواجهة، ويجري mux.HandleFunc التحويل نيابة عنك. والمعالج على شكل بنية مفيد عندما تحتاج المعالجات إلى اعتماديات:

type API struct {
	db *sql.DB
}

func (a *API) listItems(w http.ResponseWriter, r *http.Request) { /* uses a.db */ }

mux.HandleFunc("GET /items", api.listItems)

يعطيك *http.Request:

الحقل أو التابعيحتوي
r.MethodGET، POST، ...
r.URL.Pathالمسار، /items/42
r.PathValue("id")متغيّرًا من نمط المسار (Go 1.22)
r.URL.Query().Get("q")معامل من نص الاستعلام
r.Header.Get("Authorization")ترويسة من الطلب
r.Bodyجسم الطلب، من النوع io.ReadCloser (يغلقه الخادم)
r.FormValue("name")حقل نموذج أو معامل استعلام
r.Context()سياقًا يُلغى عندما يقطع العميل الاتصال

أنماط التوجيه (Go 1.22)

منذ Go 1.22 تأخذ أنماط ServeMux الشكل [METHOD ][HOST]/[PATH]، ويمكن أن تحتوي المسارات على متغيّرات.

النمطيطابق
"/items/"/items/ وكل ما تحته (الشرطة المائلة في النهاية = بادئة)
"/items"/items فقط
"GET /items/{id}"GETHEAD) على /items/42؛ وr.PathValue("id") == "42"
"POST /items"POST فقط على /items
"/files/{path...}"/files/a/b/c؛ وpath يساوي "a/b/c"
"/{$}"/ فقط، لا كل مسار
"/"كل مسار لا يطابقه نمط آخر

عندما يتطابق نمطان يفوز الأكثر تحديدًا، فيتقدّم /items/new على /items/{id}. وإذا لم يكن أي منهما أكثر تحديدًا، مثل /items/{id} و/{kind}/new (كلاهما يطابق /items/new)، فإن تسجيل الثاني يسبّب panic برسالة تسمّي النمطين. إذا طابق المسار ولم تطابق الطريقة، يجيب الموجّه بـ 405 Method Not Allowed مع ترويسة Allow، دون أي شيفرة منك.

النمط "/" يلتقط كل شيء. وهذا يفاجئ من يسجّل الصفحة الرئيسية بـ "/" فيجدها تجيب كل عنوان مجهول بـ 200. استخدم "GET /{$}" للصفحة الرئيسية.

تحتاج هذه الأنماط إلى go 1.22 أو أحدث في go.mod. مع سطر إصدار أقدم يرجع الموجّه إلى السلوك القديم: يقرأ "GET /items" كاسم مضيف يليه مسار، فلا يطابق المسار أبدًا ويحصل كل طلب إلى /items على 404.

واجهة JSON صغيرة

يستخدم الطلب الأخير DELETE، التي لا يقبلها أي مسار، فيجيب الموجّه بنفسه بـ 405 مع Allow: GET, HEAD: هذه هي الطرق المسجّلة لذلك المسار (مسار GET يقبل HEAD أيضًا).

رموز الحالة وترتيب الكتابة

للردّ ثلاثة أجزاء، ويجب كتابتها بالترتيب: الترويسات، ثم الحالة، ثم الجسم.

  1. يغيّر w.Header().Set(...) الترويسات. ويكون له أثر فقط حتى تُرسل الحالة.
  2. يرسل w.WriteHeader(code) سطر الحالة والترويسات.
  3. يرسل w.Write(...) (أو fmt.Fprint(w, ...) أو مرمّز) الجسم. إذا لم تُستدعَ WriteHeader، يرسل أول Write الحالة 200 OK أولًا.

العواقب:

  • ضبط ترويسة بعد بدء الجسم لا يفعل شيئًا.
  • استدعاء WriteHeader مرتين يسجّل http: superfluous response.WriteHeader call ويحتفظ بالحالة الأولى.
  • بعد ردّ خطأ، نفّذ return. لا توقف http.Error معالجك، والشيفرة التي بعدها تستمر في الكتابة في الردّ نفسه.

استخدم الثوابت المسمّاة (http.StatusOK وhttp.StatusCreated وhttp.StatusBadRequest وhttp.StatusUnauthorized وhttp.StatusNotFound وhttp.StatusInternalServerError) بدل الأرقام المجرّدة. تعيد http.StatusText(404) النص "Not Found".

الطبقات الوسيطة (Middleware)

الطبقة الوسيطة دالة تأخذ معالجًا وتعيد معالجًا، وتفعل شيئًا قبل استدعاء التالي أو بعده:

يظهر سطر log: دائمًا قبل client got: للطلب نفسه. تطبعه goroutine المعالج قبل أن تعود، ولا ينهي الخادم الردّ حتى يعود المعالج.

تضمّن statusRecorder الـ ResponseWriter الحقيقي وتعيد تعريف WriteHeader وحدها، وهذه الطريقة المعتادة لمراقبة رمز الحالة من طبقة وسيطة.

إعدادات الإنتاج

تستخدم http.ListenAndServe خادمًا بلا مهل، فيستطيع عميل بطيء إبقاء اتصال مفتوحًا بلا حد. اضبط http.Server صراحة لأي شيء معرّض للإنترنت، وأوقفه بلطف:

srv := &http.Server{
	Addr:              ":8080",
	Handler:           mux,
	ReadHeaderTimeout: 5 * time.Second,
	ReadTimeout:       10 * time.Second,
	WriteTimeout:      30 * time.Second,
	IdleTimeout:       120 * time.Second,
}

go func() {
	if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
		log.Fatal(err)
	}
}()

ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
<-ctx.Done() // wait for Ctrl+C or a SIGTERM from the orchestrator

shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := srv.Shutdown(shutdownCtx); err != nil { // finish in-flight requests
	log.Println("shutdown:", err)
}

يتوقّف Shutdown عن قبول اتصالات جديدة وينتظر انتهاء الطلبات النشطة، حتى الموعد النهائي للسياق. تعيد ListenAndServe الخطأ http.ErrServerClosed فور بدء Shutdown، ولهذا لا يُعامل ذلك الخطأ كفشل. للمعالجات طويلة التشغيل مرّر r.Context() إلى الأسفل لتتوقّف عندما يغادر العميل.

خدمة الملفات الثابتة سطر واحد: mux.Handle("GET /static/", http.StripPrefix("/static/", http.FileServer(http.Dir("public")))).

أخطاء شائعة

  • عدم العودة بعد http.Error. يستمر المعالج في العمل ويكتب المزيد في الردّ.
  • ضبط الترويسات بعد كتابة الجسم. تُهمل بصمت.
  • تسجيل الصفحة الرئيسية على "/". تصبح ملتقطة كل مسار مجهول. استخدم "/{$}".
  • مشاركة الحالة بين المعالجات دون قفل. الطلبات تعمل بالتزامن.
  • استخدام الخادم الافتراضي في الإنتاج. لا مهل له؛ اضبطها على http.Server.
  • تجاهل أنماط التوجيه. إذا لم يطابق "GET /x/{id}" أبدًا، فتحقّق من أن go.mod يذكر go 1.22 أو أحدث.

الأسئلة الشائعة

كيف أنشئ خادم ويب بسيطًا في Go؟

سجّل معالجًا وابدأ الاستماع: http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "hello") }) ثم log.Fatal(http.ListenAndServe(":8080", nil)). خادم المكتبة القياسية جاهز للإنتاج؛ يتعامل مع HTTP/1.1 وHTTP/2 عبر TLS وkeep-alive وgoroutine لكل اتصال.

كيف أحصل على متغيّر من المسار في net/http في Go؟

منذ Go 1.22 يمكن أن تحتوي أنماط ServeMux على متغيّرات: سجّل mux.HandleFunc("GET /items/{id}", h) واقرأ القيمة داخل المعالج بـ r.PathValue("id"). والنمط {path...} في النهاية يطابق بقية المسار. قبل 1.22 كان عليك تقسيم r.URL.Path بنفسك أو استخدام موجّه مثل chi.

هل أحتاج إطار عمل مثل Gin لبناء REST API في Go؟

لا. منذ Go 1.22 توجّه net/http حسب الطريقة ومتغيّرات المسار، وهذا ما كان السبب الأساسي لاستخدام الموجّهات. مع encoding/json للأجسام ودوال وسيطة صغيرة للتسجيل والمصادقة، تكفي المكتبة القياسية لمعظم الواجهات البرمجية. تضيف أطر العمل وسائل راحة مثل ربط الطلبات والتحقّق منها.

كيف أضبط رمز الحالة في معالج HTTP في Go؟

استدعِ w.WriteHeader(http.StatusCreated) قبل كتابة الجسم. إذا كتبت الجسم أولًا ترسل Go تلقائيًا 200 OK، وتُتجاهل WriteHeader اللاحقة مع رسالة سجل superfluous response.WriteHeader call. اضبط الترويسات بـ w.Header().Set قبل WriteHeader أيضًا؛ وللأخطاء تفعل http.Error(w, msg, code) كل ذلك.

Coddy programming languages illustration

تعلّم البرمجة مع Coddy

ابدأ الآن