
Go 1.27 يفرّغ تلقائيًا نصوص استجابات HTTP/1 غير المقروءة لإعادة استخدام الاتصال أسرع
Go 1.27 يحسِّن إعادة استخدام اتصالات HTTP/1 عبر التخلّص التلقائي من نصوص الاستجابات غير المقروءة. تُظهر معايير الأداء على index.golang.org أن البرنامج نفسه أصبح أسرع بنحو خمس مرات مقارنةً مع Go 1.26.
نصوص الاستجابات غير المقروءة كانت تعيق إعادة استخدام اتصالات HTTP/1
يدعم حزمة net/http في Go بروتوكول HTTP/1.0 وHTTP/1.1 وHTTP/2، والتي تعمل عبر TCP، بينما تعمل عناوين HTTPS عادةً عبر TLS. إنشاء اتصال جديد مكلف؛ تحتاج مصافحة TCP إلى زمن ذهاب وإياب كامل (RTT) واحد، وتحتاج مصافحة TLS 1.2 إلى 2 RTT، ولا يمكن إرسال أول طلب HTTP إلا بعد 3 RTT فقط. غالبًا ما يصل أو يتجاوز التأخير أحادي الاتجاه بين المناطق 50 مللي ثانية، لذا قد يكون إنشاء الاتصال أغلى بكثير من تسليم استجابة صغيرة.
لذلك يحاول عملاء HTTP وخوادمه إعادة استخدام الاتصالات الموجودة. في HTTP/1.1 تكون الاتصالات مستمرة افتراضيًا، لكن الاستجابات تُرسل تباعًا على نفس مجرى البايتات. يعني هذا أن net/http لن تجد بداية الاستجابة التالية إلا عند قراءة نص body الحالي حتى النهاية، وهو ما يحدده ترويسة Content-Length أو Transfer-Encoding.
في مقال مدونة Go، يشير نيكولاس هوسيني وداميان نيلي إلى خطأ شائع: دالة تستدعي http.Get، وتؤجل resp.Body.Close باستخدام defer، ثم تعيد رمز الحالة فقط دون قراءة body إطلاقًا. حتى مع Go 1.26، إذا بقي نص body غير مقروء عند Close، فإن net/http تتخلى عن الاتصال وتفتح اتصالًا جديدًا للطلب التالي، وتدفع بذلك كلفة الإنشاء مجددًا. تؤثر المشكلة أيضًا على البرامج التي تقرأ body لكنها تفتقر إلى النهاية بقليل. على سبيل المثال، يوقف encoding/json.Decoder القراءة عند أقواس إغلاق كائن JSON، لذلك يترك الخادم الذي ينهي استجابته بـ newline ذلك البايت غير مقروء ولا يمكن إعادة استخدام الاتصال. لا تواجه HTTP/2 هذه المشكلة لأنها تعدد الإطارات على اتصال واحد ولا ترسل الاستجابات تباعًا صارمًا.
Go 1.27 يفرّغ نصوص body تلقائيًا
بدءًا من Go 1.27، يعالج net/http.Transport هذا تلقائيًا. يفرّغ نص استجابة HTTP/1 غير المقروء بشكل غير متزامن حتى 50 مللي ثانية أو 256 كيلوبايت، أيهما يُبلَغ أولًا. إذا انتهى التفريغ قبل إرسال الطلب التالي، يُعاد استخدام الاتصال؛ وإلا يُنشأ اتصال جديد، بينما يُخزَّن القديم في مخزن الاتصالات غير النشطة وقد يظهر مجددًا لطلب لاحق. لا يحاول التنفيذ التفريغ إطلاقًا عندما تحمل الاستجابة ترويسة Connection: close، لأن التفريغ سيكون عملًا فاقدًا في هذه الحالة.
يشير المؤلفان إلى أن هذا السلوك قد يتغير في إصدارات Go المستقبلية عندما تظهر تحسينات أخرى ممكنة.
نتائج معيار المقارنة والتوصية
التحسين مهم. برنامج الاختبار الذي يستدعي دالة فحص حالة بسيطة 100 مرة تجاه index.golang.org، وهو موقع موزع عالميًا يدعم TLS 1.3، يحتاج على Go 1.26 نحو 710 مللي ثانية، بينما يحتاج على Go 1.27 نحو 150 مللي ثانية فقط، أي أسرع بنحو خمس مرات.
مع السلوك الجديد، يوصي فريق Go المطورين بإغلاق نصوص استجابات HTTP ببساطة عندما لا تعود مطلوبة، باستخدام القالب المألوف defer resp.Body.Close(). لم تعد الحيل اليدوية لتفريغ النصوص، التي استخدمها كثير من المطورين عبر سنوات — مثل تفريغ نصوص body عبر io.Copy، أو تقييد البايتات المفرّغة، أو منافسة التفريغ مع timeout — ضرورية. يعرض المؤلفان هذا الأمر بوصفه جزءًا من فلسفة بساطة Go: دع الكود نظيفًا وبسيطًا، ودع net/http تعالج الجزء الصعب نيابةً عنك.
المصادر: Hacker News
SiTech — تطوير ويب مدعوم بالذكاء الاصطناعي
نبني مواقع سريعة وعصرية وندمج الذكاء الاصطناعي في سير عمل الشركات. لديك مشروع أو سؤال؟ يسعدنا مساعدتك.