Menu

HTTP Client ב-Golang: GET, POST עם JSON, timeouts ו-headers

איך שולחים בקשות HTTP ב-Go עם net/http: http.Get, קריאה וסגירה של ה-body, בדיקת קודי סטטוס, timeouts ב-http.Client, בקשות עם context ו-headers, פרמטרים של query ושליחת JSON ב-POST.

בדף הזה יש עורכים שאפשר להריץ - לערוך, להריץ ולראות את הפלט מיד.

בקשת GET

כל דוגמה בעמוד הזה מפעילה שרת משלה עם httptest.NewServer וקוראת לו דרך חיבור TCP אמיתי ב-localhost. החליפו את srv.URL בכתובת אמיתית כמו https://api.example.com וקוד ה-client לא ישתנה.

שלושת הכללים שרואים כאן:

  1. בדקו קודם את err. הוא מדווח על כישלון לקבל תשובה כלשהי: שגיאות DNS, חיבורים שנדחו, timeouts, בעיות TLS.
  2. defer resp.Body.Close() מיד אחרי בדיקת השגיאה. ה-body מחזיק את החיבור פתוח. body שלא נסגר מדליף את החיבור.
  3. קראו או פענחו את ה-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 דולפים עד שהתוכנית מגיעה למגבלה שלה. סגרו אותו גם כשאתם לא קוראים אותו, וקראו אותו עד הסוף אם אתם רוצים שהחיבור ישמש שוב.

איור של שפות התכנות ב-Coddy

ללמוד תכנות עם Coddy

להתחיל