קריאת HTTP קצרה ב-Go נראית תמיד תמימה. אתם שולחים בקשת GET, בודקים את הסטטוס, וקוראים ל-Close על גוף התגובה. מה שקרה מתחת למכסה המנוע עד עכשיו היה רחוק מלהיות תמים. בפוסט שפורסם אתמול, 14 באוקטובר 2026, חשפו Nicholas Husin ו-Damien Neil בבלוג הרשמי של Go שינוי פנימי קטן שמשנה את התמונה מהיסוד.
התוצאה במספרים חדה. בנצ'מרק של 100 בקשות עוקבות מול index.golang.org בחיבור HTTP/1 ארך כ-710 מילי-שניות בגרסה 1.26. אותה בדיקה בדיוק בגרסה Go 1.27 ארכה כ-150 מילי-שניות בלבד. פי חמישה מהר יותר, על אותו מחשב, בלי לשנות שורת קוד אחת.
סגירה פשוטה כבר לא מנתקת
עד היום, הפונקציה net/http.Get הייתה קוראת את הכותרות ומחזירה מיד את המבנה של net/http.Response. גוף התגובה נשאר על שקע הרשת והמתין למימוש. אם סגרתם את הבקשה מבלי לקרוא את התוכן עד הבייט האחרון, הספרייה הניחה שאי אפשר לסנכרן את החיבור לבקשה הבאה. במקום למחזר את ערוץ התקשורת הקיים, המערכת זרקה אותו לפח ופתחה חיבור חדש לגמרי.
שדרוג ל-Go 1.27 מבצע ריקון אסינכרוני אוטומטי של תגובות HTTP/1 ברקע ומאפשר שימוש חוזר בחיבור.
מי שמרוויח מזה הם בעיקר שירותי ענן ומיקרו-סרוויסים שמבצעים שיחות פנימיות מרובות ובודקים רק קודי סטטוס, או מקבלים גופי JSON קצרים בלי לקרוא אותם עד תומם.
המחיר של בייט אחד בודד
בפרוטוקול HTTP/1, כל הבקשות והתשובות על אותו חיבור זורמות ברצף אחד אחרי השני. כדי לדעת איפה מתחילה התשובה הבאה, המערכת חייבת לוודא שהתשובה הקודמת נקראה במלואה, לפי Content-Length או קידוד Chunked. ברגע שנשאר אפילו תו ירידת שורה בודד שלא נקרא, החיבור נחשב לא מתואם ומתנתק.
זה קורה לעיתים קרובות גם כשבטוחים שקראתם הכל. מפענח כמו encoding/json.Decoder עוצר בדיוק בסוגר המסולסל שסוגר את האובייקט. אם השרת שלח אחריו תו רווח או ירידת שורה, החיבור נזנח. פתיחת חיבור חדש דורשת לחיצת יד של TCP שנמשכת סבב רשת אחד (1 RTT), ולאחריה לחיצת יד של TLS שגוזלת עוד סבב אחד או שניים. בתקשורת בין אזורים גאוגרפיים שונים, השהיית הרשת מגיעה בקלות ל-50 מילי-שניות לכל כיוון. שרפתם 300 מילי-שניות שלמות רק על הקמת החיבור.
המנגנון האוטומטי מאחורי הקלעים
בגרסה 1.27, רכיב net/http.Transport לוקח את העבודה לידיים. כשקוראים לפעולת Close, המערכת מרוקנת את גוף התגובה ברקע בצורה אסינכרונית, מבלי לחסום את התוכנית הראשית. המנגנון מוגבל בדיוק לעד 256 קילובייט ועד 50 מילי-שניות של המתנה. אם הריקון מסתיים לפני הבקשה הבאה, החיבור ממוחזר מיד; אם לא, הוא ממתין במאגר החיבורים הלא-פעילים לשימוש עתידי.
ב-Go 1.27, רכיב net/http.Transport ירוקן אוטומטית ברקע גוף תגובה שלא נקרא ב-HTTP/1 למשך עד 50 מילי-שניות או 256 קילובייט.פוסט רשמי בבלוג של Go
יש מקרים שבהם המנוע מוותר מראש. אם התגובה כוללת כותרת Connection: close, המערכת יודעת שהשרת ממילא מתכוון לנתק את השקע, ונמנעת מריקון מיותר. חשוב לדעת: ב-HTTP/2 התופעה הזו לא קיימת מלכתחילה, מפני שהוא מנהל ריבוי ערוצים במקביל על גבי חיבור בודד, כך שתגובה פתוחה אחת לא חוסמת בקשות אחרות.
אפשר למחוק את קוד המעקף
לאורך השנים, מפתחי Go התרגלו להדביק פונקציות עזר כמו drainAndCloseBody. הן כללו שימוש ב-io.Copy לתוך io.Discard, עטיפה ב-goroutine עם ערוצים וטיימרים כדי למנוע דליפות זיכרון. הקוד הזה היה מכוער, מסורבל ונטה להישבר.
עם השדרוג לגרסה החדשה, מומלץ פשוט למחוק את המעקפים הללו ולחזור לקרוא ל-Body.Close ישירות. עם זאת, אם אתם עובדים מול נקודות קצה שמחזירות קבצים כבדים ונותרו מאות מגה-בייטים לקריאה, או שהשרת מזרים מידע באיטיות קיצונית מעבר ל-50 מילי-שניות, האופטימיזציה תיעצר והחיבור ייסגר. במקרים כאלה עדיין תצטרכו לנהל את המשאבים בעצמכם.
| תרחיש שימוש | התנהגות עד Go 1.26 | התנהגות ב-Go 1.27 |
|---|---|---|
| קריאת קוד סטטוס ללא קריאת הגוף (עד 256KB) | החיבור נסגר ונפתח מחדש | ריקון אסינכרוני ושימוש חוזר בחיבור |
| פענוח JSON שנעצר לפני תו ירידת שורה בסוף | החיבור נסגר עקב בייט שלא נקרא | ריקון אוטומטי של השארית ושימוש חוזר |
| גוף תגובה שנותרו בו מעל 256KB | החיבור נסגר | החיבור נסגר לאחר חריגה מהמגבלה |
| תגובה עם כותרת Connection: close | החיבור נסגר מיידית | דילוג על ניסיון הריקון וסגירה מיידית |
| תקשורת בפרוטוקול HTTP/2 | ריבוי ערוצים ללא תלות בריקון הגוף | ריבוי ערוצים ללא שינוי בהתנהגות |
השדרוג הזה מוריד עוד דאגה אחת מהראש של מי שמנהל שירותי רשת עמוסים.
לא הייתי ממהר להמציא מנגנונים חדשים לסגירת שקעים בכוח, אלא פשוט נותן לספריית הבסיס לעשות את מה שהיא סוף סוף יודעת לעשות לבד. בינתיים, אפשר לסמן עוד פיסת קוד היסטורית למחיקה ב-PR הבא.
מקורות
- Better HTTP/1 connection reusehttps://go.dev/blog/http-connection-reuse
- `net/http.Get`https://go.dev/pkg/net/http#Get
- `net/http.Response`https://go.dev/pkg/net/http#Response
- `encoding/json.Decoder`https://go.dev/pkg/encoding/json#Decoder
- `net/http.Transport`https://go.dev/pkg/net/http#Transport
הידיעה נכתבה מהמקורות שלמעלה בתאריך הפרסום, וכל טענה בה נבדקה מולם. מצאתם טעות? כתבו.