שרת בכמה שורות
handler הוא פונקציה שמקבלת את הבקשה וכותבת את התשובה. ServeMux מנתב בקשות ל-handlers.
העורך לא יכול לקבל חיבורים מהדפדפן שלכם, ולכן הדוגמאות בעמוד הזה מפעילות את השרת עם httptest.NewServer וקוראות לו מאותה תוכנית. בתוכנית אמיתית, החלק האחרון מוחלף בשורה אחת שחוסמת ומגישה לנצח:
log.Fatal(http.ListenAndServe(":8080", mux))
אחר כך curl localhost:8080/hello/gopher מדפיס Hello, gopher!. ListenAndServe חוזרת רק בשגיאה (למשל כשהפורט תפוס), ולכן היא עטופה ב-log.Fatal.
כל בקשה רצה ב-goroutine משלה. כל מה שה-handlers שלכם חולקים, כמו map או מונה, צריך mutex.
Handlers
כל דבר עם מתודה ServeHTTP(http.ResponseWriter, *http.Request) הוא http.Handler. http.HandlerFunc מתאים פונקציה רגילה ל-interface הזה, ו-mux.HandleFunc עושה את ההמרה בשבילכם. handler מסוג struct נוח כש-handlers צריכים תלויות:
type API struct {
db *sql.DB
}
func (a *API) listItems(w http.ResponseWriter, r *http.Request) { /* uses a.db */ }
mux.HandleFunc("GET /items", api.listItems)
ה-*http.Request נותן לכם:
| שדה או מתודה | מכיל |
|---|---|
r.Method | GET, POST, ... |
r.URL.Path | הנתיב, /items/42 |
r.PathValue("id") | wildcard מתבנית הנתיב (Go 1.22) |
r.URL.Query().Get("q") | פרמטר מה-query string |
r.Header.Get("Authorization") | header של הבקשה |
r.Body | ה-body של הבקשה, io.ReadCloser (השרת סוגר אותו) |
r.FormValue("name") | שדה בטופס או פרמטר query |
r.Context() | context שמבוטל כשהלקוח מתנתק |
תבניות ניתוב (Go 1.22)
מאז Go 1.22, לתבניות של ServeMux יש הצורה [METHOD ][HOST]/[PATH], ונתיבים יכולים להכיל wildcards.
| תבנית | מתאימה ל |
|---|---|
"/items/" | /items/ וכל מה שמתחתיו (לוכסן בסוף = prefix) |
"/items" | רק /items |
"GET /items/{id}" | GET (וגם HEAD) על /items/42; r.PathValue("id") == "42" |
"POST /items" | רק POST על /items |
"/files/{path...}" | /files/a/b/c; path הוא "a/b/c" |
"/{$}" | רק /, לא כל נתיב |
"/" | כל נתיב שאף תבנית אחרת לא מתאימה לו |
כששתי תבניות מתאימות, המדויקת יותר מנצחת, ולכן /items/new גוברת על /items/{id}. אם אף אחת מהן לא מדויקת יותר, למשל /items/{id} ו-/{kind}/new (שתיהן מתאימות ל-/items/new), רישום השנייה נכנס ל-panic עם הודעה שמציינת את שתי התבניות. אם נתיב מתאים אבל המתודה לא, ה-mux עונה 405 Method Not Allowed עם header של Allow, בלי שום קוד מצדכם.
התבנית "/" היא ה-catch-all. זה מפתיע אנשים שרושמים דף בית עם "/" ומגלים שהוא עונה 200 לכל URL לא מוכר. השתמשו ב-"GET /{$}" לדף הבית.
התבניות האלה דורשות go 1.22 ומעלה ב-go.mod. עם שורת גרסה ישנה יותר, ה-mux חוזר להתנהגות הישנה: הוא קורא את "GET /items" כשם host ואחריו נתיב, ולכן הנתיב לא מתאים אף פעם וכל בקשה ל-/items מקבלת 404.
JSON API קטן
הבקשה האחרונה משתמשת ב-DELETE, שאף נתיב לא מקבל, וה-mux עונה 405 עם Allow: GET, HEAD בעצמו: אלה המתודות שרשומות לנתיב הזה (נתיב של GET מקבל גם HEAD).
קודי סטטוס וסדר הכתיבה
לתשובה יש שלושה חלקים, וצריך לכתוב אותם לפי הסדר: headers, סטטוס, body.
w.Header().Set(...)משנה headers. יש לזה השפעה רק עד שהסטטוס נשלח.w.WriteHeader(code)שולח את שורת הסטטוס ואת ה-headers.w.Write(...)(אוfmt.Fprint(w, ...), או encoder) שולח את ה-body. אם לא קראו ל-WriteHeader, ה-Writeהראשון שולח קודם200 OK.
ההשלכות:
- הגדרת header אחרי שה-body כבר התחיל לא עושה כלום.
- קריאה ל-
WriteHeaderפעמיים כותבת ללוגhttp: superfluous response.WriteHeader callושומרת את הסטטוס הראשון. - אחרי תשובת שגיאה,
return.http.Errorלא עוצרת את ה-handler שלכם, וקוד שאחריה ממשיך לכתוב לאותה תשובה.
השתמשו בקבועים בעלי השם (http.StatusOK, http.StatusCreated, http.StatusBadRequest, http.StatusUnauthorized, http.StatusNotFound, http.StatusInternalServerError) ולא במספרים חשופים. http.StatusText(404) מחזירה "Not Found".
Middleware
middleware היא פונקציה שמקבלת handler ומחזירה handler, ועושה משהו לפני או אחרי הקריאה ל-handler הבא:
השורה log: תמיד מופיעה לפני client got: באותה בקשה. ה-goroutine של ה-handler מדפיסה אותה לפני שהיא חוזרת, והשרת לא מסיים את התשובה עד שה-handler חוזר.
ה-statusRecorder מטמיע את ה-ResponseWriter האמיתי ודורס רק את WriteHeader, וזו הדרך המקובלת לראות את קוד הסטטוס מתוך middleware.
הגדרות ל-production
http.ListenAndServe משתמשת בשרת בלי timeouts, כך שלקוח איטי יכול להחזיק חיבור פתוח ללא הגבלה. הגדירו http.Server במפורש לכל דבר שחשוף לאינטרנט, וכבו אותו בצורה מסודרת:
srv := &http.Server{
Addr: ":8080",
Handler: mux,
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 10 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 120 * time.Second,
}
go func() {
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatal(err)
}
}()
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
<-ctx.Done() // wait for Ctrl+C or a SIGTERM from the orchestrator
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := srv.Shutdown(shutdownCtx); err != nil { // finish in-flight requests
log.Println("shutdown:", err)
}
Shutdown מפסיקה לקבל חיבורים חדשים ומחכה שבקשות פעילות יסתיימו, עד ה-deadline של ה-context. ListenAndServe מחזירה http.ErrServerClosed ברגע ש-Shutdown מתחילה, ולכן השגיאה הזאת לא נחשבת לכישלון. ל-handlers שרצים זמן רב, העבירו את r.Context() הלאה כדי שייעצרו כשהלקוח עוזב.
הגשת קבצים סטטיים היא שורה אחת: mux.Handle("GET /static/", http.StripPrefix("/static/", http.FileServer(http.Dir("public")))).
טעויות נפוצות
- לא לעשות return אחרי
http.Error. ה-handler ממשיך לרוץ וכותב עוד לתוך התשובה. - הגדרת headers אחרי כתיבת ה-body. הם נזרקים בשקט.
- רישום דף הבית על
"/". הוא הופך ל-catch-all של כל נתיב לא מוכר. השתמשו ב-"/{$}". - שיתוף state בין handlers בלי נעילה. בקשות רצות במקביל.
- שימוש בשרת ברירת המחדל ב-production. אין לו timeouts; הגדירו אותם ב-
http.Server. - תבניות ניתוב שמתעלמים מהן. אם
"GET /x/{id}"אף פעם לא מתאימה, בדקו שב-go.modכתובgo 1.22ומעלה.
שאלות נפוצות
איך יוצרים שרת web פשוט ב-Go?
רושמים handler ומתחילים להאזין: http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "hello") }) ואחר כך log.Fatal(http.ListenAndServe(":8080", nil)). השרת של הספרייה הסטנדרטית מתאים ל-production; הוא מטפל ב-HTTP/1.1, ב-HTTP/2 מעל TLS, ב-keep-alive וב-goroutine אחת לכל חיבור.
איך מקבלים פרמטר מהנתיב ב-net/http של Go?
מאז Go 1.22, תבניות של ServeMux יכולות להכיל wildcards: רשמו mux.HandleFunc("GET /items/{id}", h) וקראו את הערך בתוך ה-handler עם r.PathValue("id"). {path...} בסוף תופס את שאר הנתיב. לפני 1.22 הייתם צריכים לפצל את r.URL.Path בעצמכם או להשתמש ב-router כמו chi.
האם צריך framework כמו Gin כדי לבנות REST API ב-Go?
לא. מאז Go 1.22, net/http מנתב לפי מתודה ולפי פרמטרים בנתיב, וזו הייתה הסיבה העיקרית שבגללה אנשים השתמשו ב-routers. עם encoding/json ל-bodies ועם פונקציות middleware קטנות ללוגים ולהרשאות, הספרייה הסטנדרטית מספיקה לרוב ה-APIs. frameworks מוסיפים נוחויות כמו binding של בקשות ו-validation.
איך מגדירים את קוד הסטטוס ב-handler של HTTP ב-Go?
קראו ל-w.WriteHeader(http.StatusCreated) לפני שכותבים את ה-body. אם כותבים קודם את ה-body, Go שולחת 200 OK אוטומטית ו-WriteHeader מאוחר יותר מתעלם, עם הודעת לוג superfluous response.WriteHeader call. הגדירו headers עם w.Header().Set גם הם לפני WriteHeader; לשגיאות, http.Error(w, msg, code) עושה את כל זה.