15 مارس 2025·6 دقيقة قراءة

مهلات سياق Go لواجهات API: من معالجات HTTP إلى SQL

مهلات السياق في Go تساعدك على تمرير المهل من معالجات HTTP إلى استدعاءات SQL، منع توقف الطلبات، والحفاظ على استقرار الخدمات تحت الضغط.

مهلات سياق Go لواجهات API: من معالجات HTTP إلى SQL

لماذا تتعلق الطلبات (ولماذا يضر ذلك تحت الضغط)

يُصبح الطلب «معلقًا» عندما ينتظر شيئًا لا يعود: استعلام قاعدة بيانات بطيء، اتصال محجوز من مجموعة الاتصالات، مشكلة DNS، أو خدمة خارجية تقبل الطلب دون أن ترد.

المظهر الخارجي بسيط: بعض الطلبات تستغرق وقتًا طويلًا جدًا، ثم تتراكم خلفها المزيد. غالبًا ستلاحظ ارتفاع الذاكرة، وزيادة عدد goroutine، وطابور اتصالات مفتوحة لا يكاد ينقص.

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

المحاولات الإعادة وذروات المرور تزيد من سوء هذه الدوامة. ينتهي مهلة العميل ويُعيد المحاولة بينما الطلب الأصلي لا يزال يعمل، فتدفع ثمن طلبين. اضرب ذلك في عدة عملاء أثناء تباطؤ قصير، وقد تُحمِّل قاعدة البيانات أو تصل لحدود الاتصالات حتى لو كان متوسط الحمل طبيعيًا.

المهلة هي ببساطة وعد: "لن ننتظر أكثر من X." تساعد على الفشل السريع وتحرير الموارد، لكنها لا تجعل العمل ينتهي أسرع.

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

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

الهدف مع مهلات context في Go هو مهلة مشتركة واحدة من الحافة إلى أعمق استدعاء. عيّنها مرة واحدة عند حد HTTP، مرّر نفس السياق عبر كود الخدمة، واستخدمه في استدعاءات database/sql حتى تُخبَر قاعدة البيانات أيضاً بموعد التوقف.

السياق في Go بمصطلحات بسيطة

context.Context هو كائن صغير تمرره عبر كودك لوصف ما يحدث الآن. يجيب عن أسئلة مثل: "هل هذا الطلب لا يزال صالحًا؟"، "متى يجب أن نستسلم؟"، و"ما القيم الخاصة بهذا الطلب التي ينبغي أن تُنقَل مع العمل؟"

الميزة الكبرى أن قرارًا واحدًا عند حافة النظام (معالج HTTP) يمكن أن يحمي كل خطوة لاحقة، طالما استمررت في تمرير نفس السياق.

ماذا يحمل السياق

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

الفرق بين مهلة وإلغاء بسيط: المهلة سبب واحد للإلغاء. إذا ضبطت مهلة 2 ثانية، يُلغى السياق بعد مرور 2 ثانية. لكن يمكن أيضًا إلغاء السياق مبكرًا إذا أغلق المستخدم التبويب، أو ألقى موازن التحميل الاتصال، أو قرر كودك إيقاف الطلب.

يتدفق السياق عبر استدعاءات الدوال بكونه باراميترًا صريحًا، عادةً الأول: func DoThing(ctx context.Context, ...). هذا هو الهدف. يصعب "نسيانه" عندما يظهر في كل موقع استدعاء.

عند انتهاء الموعد النهائي، يجب على أي شيء يراقب ذلك السياق أن يتوقف بسرعة. على سبيل المثال، استعلام قاعدة بيانات يستخدم QueryContext يجب أن يعود مبكرًا بخطأ مثل context deadline exceeded، ويمكن لمعالجك أن يرد بمهلة بدلاً من الانتظار حتى ينفد خادم العمال.

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

ضبط موعد نهائي واضح عند حدود HTTP

إذا أردت أن تعمل مهلات الطرف إلى الطرف، قرر أين يبدأ العدّ. المكان الأكثر أمانًا هو عند حافة HTTP، حتى ترث كل استدعاءاتك اللاحقة نفس الموعد النهائي.

يمكنك ضبط هذا الموعد في عدة مواضع. مهلات على مستوى الخادم تُعد خطًا أساسياً جيدًا وتحميك من العملاء البطيئين. الوسيط (middleware) مفيد للاتساق عبر مجموعات الراوت. ضبطه داخل المعالج جيد أيضًا عندما تريد شيئًا صريحًا ومحليًا.

لأغلب واجهات الـ API، مهلات كل طلب في الوسيط أو المعالج هي الأسهل للفهم. اجعلها واقعية: المستخدمون يفضلون فشلًا واضحًا وسريعًا على طلب يعلق. تستخدم فرق كثيرة ميزانيات أقصر للقراءات (مثل 1-2 ثانية) وقليلاً أطول للكتابات (مثل 3-10 ثوانٍ) حسب ما تفعله نقطة النهاية.

هنا نمط معالج بسيط:

func (s *Server) getReport(w http.ResponseWriter, r *http.Request) {
    ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
    defer cancel()

    report, err := s.reports.Generate(ctx, r.URL.Query().Get("id"))
    if err != nil {
        http.Error(w, err.Error(), http.StatusGatewayTimeout)
        return
    }

    json.NewEncoder(w).Encode(report)
}

قاعدتان تحافظان على فاعلية هذا النمط:

  • استدعِ دائمًا cancel() حتى تتحرر المؤقتات والموارد بسرعة.
  • لا تُسقط سياق الطلب وتستبدله بـ context.Background() أو context.TODO() داخل المعالج. ذلك يقطع السلسلة، وقد تعمل استدعاءات قاعدة البيانات والطلبات الصادرة إلى الأبد حتى بعد اختفاء العميل.

تمرير السياق عبر قاعدة الشفرة

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

قاعدة بسيطة تحافظ على الاتساق: كل دالة قد تنتظر يجب أن تقبل context.Context، ويجب أن يكون الباراميتر الأول. هذا يجعل الأمر واضحًا في مواقع الاستدعاء ويصبح عادة.

نمط توقيع عملي

فضّل توقيعات مثل DoThing(ctx context.Context, ...) للخدمات والمستودعات. تجنب إخفاء السياق داخل الهياكل أو إعادة إنشائه بـ context.Background() في الطبقات السفلى، لأن ذلك يتخلى عن مهلة المستدعي بصمت.

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

    if err := h.svc.CreateOrder(ctx, r.Body); err != nil {
        // map context errors to a clear client response elsewhere
        http.Error(w, err.Error(), http.StatusRequestTimeout)
        return
    }
}

func (s *Service) CreateOrder(ctx context.Context, body io.Reader) error {
    // parsing or validation can still respect cancellation
    select {
    case <-ctx.Done():
        return ctx.Err()
    default:
    }

    return s.repo.InsertOrder(ctx, /* data */)
}

التعامل مع الخروج المبكر بنظافة

عامل ctx.Done() كمسار تحكم عادي. عادتان تساعدان:

  • افحص ctx.Err() قبل بدء عمل مكلف، وبعد الحلقات الطويلة.
  • أعد ctx.Err() للأعلى دون تغيير، حتى يستطيع المعالج الرد بسرعة ويتوقف عن إهدار الموارد.

عندما تمرّر كل طبقة نفس ctx، يمكن لمهلة واحدة أن تقطع التحليل والمنطق وعمليات الانتظار لقاعدة البيانات دفعة واحدة.

تطبيق المهل على استعلامات database/sql

بناء واجهات API بمهل واضحة
أنشئ خلفية Go في AppMaster وحافظ على اتساق المهل من المعالج إلى SQL.
جرّب AppMaster

بعد أن يصبح لمعامل HTTP موعد نهائي، تأكد أن عمل قاعدة البيانات يستمع إليه فعلاً. مع database/sql هذا يعني استخدام الأساليب الواعية بالسياق في كل مرة. إذا استدعيت Query() أو Exec() بدون سياق، قد يظل الـ API ينتظر استعلامًا بطيئًا بعد أن استسلم العميل.

استخدم هذه الأساليب باستمرار: db.QueryContext، db.QueryRowContext، db.ExecContext، وdb.PrepareContext (ثم QueryContext/ExecContext على البيان المرجعي).

func (s *Store) GetUser(ctx context.Context, id int64) (*User, error) {
	row := s.db.QueryRowContext(ctx,
		`SELECT id, email FROM users WHERE id = $1`, id,
	)
	var u User
	if err := row.Scan(&u.ID, &u.Email); err != nil {
		return nil, err
	}
	return &u, nil
}

func (s *Store) UpdateEmail(ctx context.Context, id int64, email string) error {
	_, err := s.db.ExecContext(ctx,
		`UPDATE users SET email = $1 WHERE id = $2`, email, id,
	)
	return err
}

هناك أمران سهلان أن تغفلهما.

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

ثانيًا، فكر في مهلة على جانب قاعدة البيانات كحاجز احتياطي. على سبيل المثال، يمكن لـ Postgres فرض حد لكل بيان (غالبًا ما يُسمى statement timeout). هذا يحمي قاعدة البيانات حتى لو نسي تطبيق ما تمرير السياق في مكان ما.

عندما يتوقف عملية بسبب المهلة، تعامل معها بشكل مختلف عن خطأ SQL عادي. افحص errors.Is(err, context.DeadlineExceeded) وerrors.Is(err, context.Canceled) وأعد استجابة واضحة (مثل 504) بدلاً من التعامل معها على أنها "قاعدة بيانات مكسورة." إذا كنت تولِّد خدمات Go (مثلاً مع AppMaster)، فإن إبقاء مسارات الأخطاء هذه مميزة يسهل تفسير السجلات والـ retries.

الاستدعاءات اللاحقة: عملاء HTTP، الكاش، والخدمات الأخرى

حتى لو كانت وحدات المعالج وSQL تحترم السياق، يمكن للطلب أن يظل معلقًا إذا انتظر استدعاء لاحق إلى أجل غير مسمى. تحت الحمل، قد تتكدس بعض goroutine المعلقة، تستهلك مجموعات الاتصالات، وتحول تباطؤًا صغيرًا إلى انقطاع كامل. الإصلاح هو تمرير السياق باستمرار مع حاجز علوي صارم.

HTTP الصادر

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

req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil { /* handle */ }
resp, err := httpClient.Do(req)

لا تعتمد على السياق فقط. اضبط أيضًا العميل والنقل (transport) بحيث تكون محميًا إذا استُخدم background عن طريق الخطأ، أو توقفت DNS/TLS/الاتصالات الخاملة. عيّن http.Client.Timeout كحد عُلوي للنداء بأكمله، واضبط مهلات النقل (dial، TLS handshake، response header)، وأعد استخدام عميل واحد بدل إنشاء عميل جديد لكل طلب.

الكاش والصفوف

تملك الكاشات، والرسائل الوسيطة (message brokers)، وRPC نقاط انتظار خاصة بها: الحصول على اتصال، انتظار رد، الحجز على صف ممتلئ، أو انتظار قفل. تأكد أن تلك العمليات تقبل ctx، واستخدم مهلات مكتبية حيثما توفّر.

قاعدة عملية: إذا بقي للطلب 800ms فلا تبدأ نداءً قد يستغرق 2 ثانية. تخطّه، حضّر تدهورًا مقبولًا، أو أعد بيانات جزئية.

قرر مسبقًا ماذا تعني المهلة لواجهة برمجتك. أحيانًا الجواب الصحيح خطأ سريع، وأحيانًا بيانات جزئية للحقول الاختيارية، وأحيانًا بيانات مخبّأة قديمة مع وسم واضح.

إذا كنت تبني Backends بـ Go (بما فيها المولدة، مثل AppMaster)، هذا الفرق بين "وجود مهلات" و"حماية المهل للنظام باستمرار" عند ذروة المرور.

خطوة بخطوة: إعادة هيكلة API لاستخدام مهلات من الطرف إلى الطرف

نمذجة بيانات Postgres بصرياً
صمم مخططك في Data Designer واربطه بمنطق واجهة برمجتك.
تصميم البيانات

إعادة الهيكلة للمهلات تختزل في عادة واحدة: مرّر نفس context.Context من حافة HTTP إلى كل استدعاء قد يحجب.

طريقة عملية للقيام بذلك هي العمل من الأعلى إلى الأسفل:

  • غيّر معالجك والطرق الأساسية للخدمة لقبول ctx context.Context.
  • حدّث كل استدعاء قاعدة بيانات لاستخدام QueryContext أو ExecContext.
  • افعل الشيء نفسه مع الاستدعاءات الخارجية (عملاء HTTP، الكاش، الصفوف). إذا كانت مكتبة لا تقبل ctx، غلفها أو استبدلها.
  • قرّر من يملك المهلة. قاعدة شائعة: المعالج يضع الموعد النهائي العام؛ الطبقات الأدنى تضع مهلات أقصر عندما يكون هناك سبب واضح.
  • اجعل الأخطاء متوقعة عند الحافة: طوّع context.DeadlineExceeded وcontext.Canceled لاستجابات HTTP واضحة.

هذا هو الشكل الذي تريده عبر الطبقات:

func (h *Handler) GetOrder(w http.ResponseWriter, r *http.Request) {
    ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
    defer cancel()

    order, err := h.svc.GetOrder(ctx, r.PathValue("id"))
    if errors.Is(err, context.DeadlineExceeded) {
        http.Error(w, "request timed out", http.StatusGatewayTimeout)
        return
    }
    if err != nil {
        http.Error(w, "internal error", http.StatusInternalServerError)
        return
    }
    _ = json.NewEncoder(w).Encode(order)
}

func (r *Repo) GetOrder(ctx context.Context, id string) (Order, error) {
    row := r.db.QueryRowContext(ctx, `SELECT id,total FROM orders WHERE id=$1`, id)
    // scan...
}

قيمة المهل يجب أن تكون مملة ومتناسقة. إذا منح المعالج 2 ثانية كميزانية كلية، فضع استعلامات DB تحت 1 ثانية لتترك مجالًا للترميز JSON وأعمال أخرى.

لإثبات أنها تعمل، أضف اختبارًا يفرض مهلة. نهج بسيط هو وجود طريقة مستودع زائفة تحجب حتى ctx.Done() ثم تعيد ctx.Err(). يجب أن يثبت الاختبار أن المعالج يعيد 504 بسرعة، لا بعد التأخير المزيف الطويل.

إذا كنت تبني Backends مولدة (مثل AppMaster)، القاعدة نفسها: سياق واحد، يُمرّر في كل مكان، مع وضوح من يملك المهلة.

القابلية للرصد: إثبات أن المهل تعمل

ربط الخدمات الشائعة بسرعة
ادمج المصادقة والمدفوعات والرسائل دون إعادة كتابة نفس الطبقة المساعدة.
استكشف الوحدات

المهل تساعد فقط إذا كنت قادرًا على رؤيتها تعمل. الهدف بسيط: لكل طلب موعد نهائي، وعندما يفشل يمكنك معرفة أين انصرف الوقت.

ابدأ بسجلات آمنة ومفيدة. بدل أن تسكب أجساد الطلبات كاملة، سجّل ما يكفي لربط النقاط واكتشاف مسارات البطء: معرف الطلب (أو معرف التتبع)، هل تم ضبط مهلة وكم تبقى عند نقاط مفتاحية، اسم العملية (معالج، اسم استعلام SQL، اسم نداء خارج)، وفئة النتيجة (ok، timeout، canceled، خطأ آخر).

أضف بعض المقاييس المركزة لبيان السلوك تحت الحمل:

  • عدد المهلات حسب نقطة النهاية والاعتماد
  • زمن الطلب (p50/p95/p99)
  • الطلبات الجارية
  • زمن استعلامات قاعدة البيانات (p95/p99)
  • معدل الأخطاء مفصولًا حسب النوع

عند التعامل مع الأخطاء، وسمها بشكل صحيح. context.DeadlineExceeded عادةً يعني أنك أتممت الميزانية. context.Canceled غالبًا يعني أن العميل غادر أو أن مهلة في مكان آخر نفذت أولًا. افرّق بينهما لأن الإصلاحات تختلف.

التتبّع: اكتشف أين ضاع الوقت

يجب أن تتبع التتبّعات نفس السياق من معالج HTTP إلى استدعاءات database/sql مثل QueryContext. على سبيل المثال، تنتهي مهلة الطلب عند 2 ثانية ويُظهر التتبّع 1.8 ثانية كانت تنتظر اتصال DB. هذا يشير إلى حجم المجموعة أو معاملات طويلة، لا نص الاستعلام.

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

أخطاء شائعة تبطل المهل

معظم أخطاء "لا تزال معلقة أحيانًا" تأتي من بعض الأخطاء الصغيرة:

  • إعادة ضبط الساعة أثناء التنفيذ. يضع المعالج مهلة 2s، لكن المستودع ينشئ سياقًا جديدًا بمهلة خاصة به (أو بلا مهلة). الآن يمكن لقاعدة البيانات أن تستمر بعد اختفاء العميل. مرّر ctx الوارد وضيّق المهلة فقط عند وجود سبب واضح.
  • بدء goroutine لا تتوقف أبدًا. تشغيل عمل بـ context.Background() (أو إسقاط ctx نهائيًا) يعني أنه سيستمر حتى بعد إلغاء الطلب. مرّر سياق الطلب إلى goroutine واستخدم select على ctx.Done().
  • مهلات قصيرة جدًا لحركة المرور الحقيقية. مهلة 50ms قد تعمل على جهازك وتفشل في الإنتاج أثناء ذروة بسيطة، مسببة إعادة محاولات وحمل إضافي وانقطاع صغير تقوده بنفسك. اختر مهلات بناءً على الكمون الطبيعي زائد هامش.
  • إخفاء الخطأ الحقيقي. التعامل مع context.DeadlineExceeded كخطأ عام 500 يجعل التصحيح وسلوك العميل أسوأ. حوله إلى استجابة مهلة واضحة وسجّل الفرق بين "أُلغي بواسطة العميل" و"انتهت المهلة".
  • ترك الموارد مفتوحة عند الخروج المبكر. إذا رجعت مبكرًا، تأكد أنك لا تزال defer rows.Close() واستدعاء cancel من context.WithTimeout. الصفوف المتسربة أو الأعمال المتبقية قد تستنزف الاتصالات تحت الحمل.

مثال سريع: نقطة نهاية تشغل استعلام تقرير. إذا أغلق المستخدم التبويب، يُلغى سياق المعالج. إذا كان استدعاء SQL استعمل سياقًا جديدًا background، سيستمر الاستعلام، ويشغل اتصالًا ويبطئ الجميع. عندما تنقل نفس ctx إلى QueryContext، ينقطع استدعاء DB ويتعافى النظام أسرع.

قائمة تحقق سريعة لسلوك مهلات موثوق

إطلاق لوحة إدارة بسرعة
استخدم AppMaster لبناء أدوات داخلية تكتشف الطلبات البطيئة والمهل مبكراً.
ابدأ الآن

المهل تساعد فقط إذا كانت متسقة. استدعاء واحد مفقود يمكن أن يبقي goroutine مشغولًا، ويحجز اتصال DB، ويبطئ الطلبات التالية.

  • ضع موعدًا واحدًا واضحًا عند الحافة (عادةً معالج HTTP). يجب أن يرثه كل شيء داخل الطلب.
  • مرّر نفس ctx عبر طبقات الخدمة والمستودع. تجنب context.Background() في كود الطلب.
  • استخدم طرق DB الواعية بالسياق في كل مكان: QueryContext، QueryRowContext، وExecContext.
  • أربط نفس ctx بالنداءات الصادرة (عملاء HTTP، الكاش، الصفوف). إذا أنشأت سياقًا فرعيًا، اجعله أقصر، لا أطول.
  • تعامل مع الإلغاءات والمهل بثبات: أعد خطأً نظيفًا، أوقف العمل، وتجنب حلقات إعادة المحاولة داخل طلب مُلغى.

بعد ذلك، تحقق من السلوك تحت الضغط. مهلة تُفعَّل لكنها لا تُحرر الموارد بسرعة كافية لا تزال تؤذي الاعتمادية.

يجب أن تجعل اللوحات المهلات واضحة، لا مخفية داخل المتوسطات. تعقّب بعض الإشارات التي تجيب على "هل تُطبّق المهل فعلاً؟": مهلات الطلب ومهلات DB (منفصلتان)، مقاطع الكمون (p95, p99)، إحصاءات مجموعة DB (الاتصالات قيد الاستخدام، عدد الانتظار، مدة الانتظار)، وتحليل أسباب الأخطاء (context deadline exceeded مقابل أخطاء أخرى).

إذا بنيت أدوات داخلية على منصة مثل AppMaster، تنطبق نفس قائمة التحقق على أي خدمات Go تتصل بها: عرّف المهل عند الحافة، مرّرها، وتأكد عبر المقاييس أن الطلبات المعلقة تحولت إلى إخفاقات سريعة بدلًا من تكدسات بطيئة.

سيناريو نموذجي وخطوات تالية

مكان شائع يجني ثمار ذلك هو نقطة بحث. تخيل GET /search?q=printer تتباطأ عندما تكون قاعدة البيانات مشغولة باستعلام تقرير كبير. بدون مهلة، كل طلب وارد قد ينتظر استعلام SQL طويل. تحت الحمل، تتكدس الطلبات المعلقة، وتشغل goroutine والاتصالات، ويشعر API كله بالتجمّد.

مع مهلة واضحة في معالج HTTP ونقل نفس ctx إلى المستودع، يتوقف النظام عن الانتظار عندما تنفد الميزانية. عند وصول الموعد النهائي، يلغي درايفر قاعدة البيانات الاستعلام (عند الدعم)، يعود المعالج، ويستمر الخادم في خدمة طلبات جديدة بدل انتظار الأقدم لفترة طويلة.

سلوك المستخدم يتحسّن حتى عندما تسوء الأمور. بدل الدوران لمدة 30 إلى 120 ثانية ثم الفشل بطريقة فوضوية، يحصل العميل على خطأ سريع ومتوقع (غالبًا 504 أو 503 مع رسالة قصيرة مثل "request timed out"). والأهم أن النظام يتعافى بسرعة لأن الطلبات الجديدة لا تُحجب خلف القديمة.

خطوات تالية لتعميم ذلك عبر نقاط النهاية والفرق:

  • اختر مهلات قياسية لكل نوع نقطة نهاية (بحث مقابل كتابة مقابل تصدير).
  • اشترط استخدام QueryContext وExecContext في مراجعات الكود.
  • اجعل أخطاء المهلة صريحة عند الحافة (كود حالة واضح، رسالة بسيطة).
  • أضف مقاييس للمهلات والإلغاءات حتى تلاحظ التراجعات مبكرًا.
  • اكتب مساعدة واحدة تغلف إنشاء السياق والسجل حتى يتصرف كل معالج بنفس الطريقة.

إذا كنت تبني خدمات وأدوات داخلية بـ AppMaster، يمكنك تطبيق قواعد المهل هذه باستمرار عبر Backends المولدة في Go، تكاملات API، ولوحات المراقبة في مكان واحد. AppMaster متاح على appmaster.io (no-code، مع توليد شفرة Go حقيقية)، لذا قد يكون مناسبًا عمليا إذا أردت سلوك طلب متسق وقابلية للرصد دون بناء كل أداة إدارية يدويًا.

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

ماذا يعني أن يَتعلّق طلب في واجهة Go API؟

الطلب «يعلق» عندما ينتظر شيئًا لا يعود، مثل استعلام SQL بطيء، أو اتصال محجوز من مجموعة الاتصالات، أو مشاكل DNS، أو خدمة خارجية تستقبل الطلب دون أن ترد. تحت الحمل، تتراكم الطلبات المعلقة، وتشغل العمال والاتصالات، وقد تحوّل تباطؤًا صغيرًا إلى عطل أوسع.

أين يجب أن أضع المهل: في الوسيط (middleware)، أم في المعالج (handler)، أم في طبقة أعمق؟

ضع الموعد النهائي العام عند حدود HTTP ومرّر نفس ctx لكل طبقة قد تنتظر. هذه المهلة المشتركة تمنع بعض العمليات البطيئة من الاحتفاظ بالموارد طويلاً بما يؤدي إلى انتشار التأخيرات.

لماذا يجب أن أستدعي `cancel()` إذا كانت المهلة ستنتهي لوحدها؟

استخدم ctx, cancel := context.WithTimeout(r.Context(), d) ودوّن دائمًا defer cancel() في المعالج أو الوسيط. استدعاء cancel يحرر المؤقتات ويساعد على إيقاف الانتظار بسرعة عندما ينتهي الطلب مبكرًا.

ما أكبر خطأ يجعل المهل عديمة الجدوى؟

لا تستبدلها بـ context.Background() أو context.TODO() في كود الطلب؛ ذلك يكسر الإلغاء والمهل. إن حذف سياق الطلب يعني أن العمل السفلي مثل SQL أو HTTP الخارجي قد يستمر حتى بعد اختفاء العميل.

كيف أتعامل مع `context deadline exceeded` مقابل `context canceled`؟

عامل context.DeadlineExceeded و context.Canceled كنتائج تحكم عادية ومرّرها للأعلى كما هي. عند الحافة، حولها إلى استجابات واضحة (غالبًا 504 للمهل) حتى لا يعيد العملاء المحاولات تلقائيًا على أنها خطأ داخلي عشوائي.

أي استدعاءات `database/sql` يجب أن تستخدم السياق؟

استخدم طرق database/sql المدعومة بالسياق في كل مكان: QueryContext، QueryRowContext، ExecContext، وPrepareContext. إذا استعملت Query() أو Exec() بلا سياق، قد ينتهي مهلة المعالج بينما يظل استدعاء قاعدة البيانات عالقًا ويحجز goroutine واتصالًا.

هل إلغاء السياق يوقف فعليًا استعلام PostgreSQL جارٍ؟

تعمل أغلب الدرايفرات على إيقاف استعلام PostgreSQL عند إلغاء السياق، لكن تحقق من الستاك الخاص بك عبر استعلام بطئ متعمد والتأكد أنه يلغى بسرعة بعد انتهاء المهلة. من الذكاء أيضًا وضع مهلة على مستوى قاعدة البيانات كخط دفاع تحسّبي إذا نُسِي تمرير ctx في بعض المسارات.

كيف أطبق نفس المهلة على استدعاءات HTTP الخارجية؟

أنشئ الطلب الخارجي باستخدام http.NewRequestWithContext(ctx, ...) حتى يسري نفس الموعد النهائي والإلغاء. أيضاً، اضبط http.Client.Timeout ووقت الاتصال ووقت مصافحة TLS ووقت انتظار رؤوس الاستجابة كحد عُلوي صارم، لأن السياق لا يحميك إذا استعمل أحدهم background بالخطأ أو حدث عطل منخفض المستوى.

هل يجب على الطبقات السفلى (repo/services) إنشاء مهلاتها الخاصة؟

تجنب إنشاء سياقات جديدة تمتدّ ميزانية الوقت في الطبقات السفلى؛ يجب أن تكون مهلات الأطفال أقصر، لا أطول. إذا بقي وقت قليل للطلب، فتخطى استدعاءات خارجية اختيارية، أو قدّم استجابة جزئية، أو أفِش بسرعة بخطأ واضح.

ماذا أراقب لإثبات أن المهل تعمل من الطرف إلى الطرف؟

راقب المهل والإلغاءات منفصلة لكل نقطة نهاية واعتماد، إلى جانب درجات الكمون ونسبة الطلبات الجارية. في التتبّع (tracing)، اتبع نفس السياق من المعالج إلى استدعاءات QueryContext لرؤية أين ضاع الوقت—هل كان في انتظار اتصال لقاعدة البيانات، أم تنفيذ الاستعلام، أم انتظار خدمة أخرى؟

من السهل أن تبدأ
أنشئ شيئًا رائعًا

تجربة مع AppMaster مع خطة مجانية.
عندما تكون جاهزًا ، يمكنك اختيار الاشتراك المناسب.

البدء