בקשת GET
כל דוגמה בעמוד הזה מפעילה שרת משלה עם httptest.NewServer וקוראת לו דרך חיבור TCP אמיתי ב-localhost. החליפו את srv.URL בכתובת אמיתית כמו https://api.example.com וקוד ה-client לא ישתנה.
שלושת הכללים שרואים כאן:
- בדקו קודם את
err. הוא מדווח על כישלון לקבל תשובה כלשהי: שגיאות DNS, חיבורים שנדחו, timeouts, בעיות TLS. defer resp.Body.Close()מיד אחרי בדיקת השגיאה. ה-body מחזיק את החיבור פתוח. body שלא נסגר מדליף את החיבור.- קראו או פענחו את ה-body.
io.ReadAllלטקסט,json.NewDecoder(resp.Body).Decode(&v)ל-JSON.
קודי סטטוס הם לא שגיאות
http.Get מחזירה err == nil לכל תשובה שהשרת שולח, כולל 404 ו-500. צריך לבדוק את הסטטוס בעצמכם:
io.LimitReader מגביל כמה מה-body של שגיאה אתם קוראים, כך ששרת שמתנהג לא כראוי לא יוכל לגרום לכם לטעון מגהבייטים לתוך הודעת שגיאה. הרבה APIs מחזירים 201, 202 או 204 כהצלחה; בדקו resp.StatusCode >= 200 && resp.StatusCode < 300 כשכל 2xx מתאים לכם.
Timeouts: אף פעם לא להשתמש ב-client ברירת המחדל לתעבורה אמיתית
ל-http.Get, ל-http.Post ול-http.DefaultClient אין timeout כולל (ה-transport ברירת המחדל מגביל רק את יצירת החיבור ואת ה-TLS handshake). שרת שמקבל את החיבור ולא עונה אף פעם חוסם את ה-goroutine שלכם לנצח. צרו client משלכם:
Client.Timeout מכסה את כל חילופי הדברים: התחברות, redirects וקריאת ה-body. השגיאה שהוא מחזיר היא *url.Error שמדווחת Timeout() == true, ו-errors.As מוצאת אותה דרך ה-interface net.Error. צרו client אחד והשתמשו בו בכל מקום; http.Client בטוח לשימוש מקבילי. ה-pool של החיבורים נמצא ב-Transport שלו, לא ב-Client: http.Client{} חדש לכל בקשה עדיין חולק את http.DefaultTransport ואת ה-pool שלו, אבל http.Transport חדש לכל בקשה מתחיל כל פעם עם pool ריק.
בקשות עם context ו-headers
http.NewRequestWithContext בונה בקשה שאפשר להתאים לפני ששולחים אותה עם client.Do. ה-context נותן deadline או ביטול לכל בקשה, מעבר ל-timeout של ה-client, וקושר קריאות יוצאות למשך החיים של בקשה נכנסת בשרת.
שרשור קלט של משתמש לתוך URL ("?q=" + q) נשבר על &, על # ועל רווחים. url.Values והמתודה Encode שלו מבצעים את ה-escaping וממיינים את המפתחות. לחלקים של ה-path, השתמשו ב-url.PathEscape.
POST עם JSON
client.Post(url, contentType, body) הוא קיצור לבקשה עם ה-header Content-Type הזה. לכל דבר אחר (header של הרשאה, PUT, PATCH, DELETE) השתמשו ב-http.NewRequestWithContext עם המתודה וב-client.Do. לשליחת טפסים, client.PostForm(url, url.Values{...}) מקודד את הערכים ומגדיר application/x-www-form-urlencoded.
שימוש חוזר בחיבורים
ה-client משתמש שוב בחיבור TCP רק כשקוראים את ה-body הקודם עד הסוף וסוגרים אותו. אם מפסיקים לקרוא מוקדם (למשל אחרי סטטוס של שגיאה), רוקנו אותו לפני הסגירה כששולחים הרבה בקשות לאותו host:
defer func() {
io.Copy(io.Discard, resp.Body)
resp.Body.Close()
}()
ל-bodies שעלולים להיות ענקיים, רוקנו דרך io.LimitReader, או השלימו עם אובדן החיבור.
Redirects
ה-client עוקב אוטומטית אחרי עד 10 redirects, ו-resp.Request.URL אומר לכם לאן הגעתם בסוף. כדי לעצור ב-redirect הראשון, למשל כדי לקרוא header של Location, הגדירו CheckRedirect:
client := &http.Client{
Timeout: 10 * time.Second,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse // return the 3xx response itself
},
}
טעויות נפוצות
- שימוש ב-
http.Getאו ב-http.DefaultClientב-production. אין timeout. צרוhttp.ClientעםTimeout. - לשכוח את
resp.Body.Close(). חיבורים דולפים עד שבקשות מתחילות להיכשל. - סגירת ה-body לפני בדיקת
err. כש-errהוא לאnil,respהואnilו-resp.Body.Close()נכנס ל-panic. - להתייחס ל-
err == nilכהצלחה. בדקו אתresp.StatusCode. - יצירת
http.Transportחדש לכל בקשה. לכל transport יש pool חיבורים משלו, ולכן כל בקשה פותחת חיבור חדש (ו-TLS handshake חדש). שתפו transport אחד, בדרך כלל דרךhttp.Clientמשותף אחד. - בניית query strings ביד. השתמשו ב-
url.Values.
שאלות נפוצות
איך שולחים בקשת GET ב-Go?
resp, err := http.Get(url), בודקים את err, אחר כך defer resp.Body.Close() וקוראים את ה-body עם io.ReadAll(resp.Body) או מפענחים אותו עם json.NewDecoder(resp.Body).Decode(&v). בדקו גם את resp.StatusCode: 404 או 500 הם לא שגיאה מבחינת http.Get.
איך מגדירים timeout לבקשת HTTP ב-Go?
צרו client עם timeout, client := &http.Client{Timeout: 10 * time.Second}, והשתמשו בו במקום http.Get או http.DefaultClient, שאין להם timeout כולל: שרת שמקבל את החיבור ולא עונה אף פעם חוסם את הקריאה לנצח. למגבלה לכל בקשה בנפרד, בנו את הבקשה עם http.NewRequestWithContext ועם context מ-context.WithTimeout.
איך שולחים בקשת POST עם body של JSON ב-Go?
עשו marshal לערך ואז שלחו אותו: body, _ := json.Marshal(v) ו-http.Post(url, "application/json", bytes.NewReader(body)). כדי להוסיף headers כמו Authorization, בנו את הבקשה עם http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body)), הגדירו req.Header.Set("Content-Type", "application/json") ושלחו אותה עם client.Do(req).
למה צריך לסגור את resp.Body ב-Go?
ה-body הוא stream על חיבור פתוח. סגירה שלו מחזירה את החיבור ל-pool לשימוש חוזר; אם שוכחים, חיבורים ו-file descriptors דולפים עד שהתוכנית מגיעה למגבלה שלה. סגרו אותו גם כשאתם לא קוראים אותו, וקראו אותו עד הסוף אם אתם רוצים שהחיבור ישמש שוב.