Serwer w kilku liniach
Handler to funkcja, która dostaje żądanie i zapisuje odpowiedź. ServeMux kieruje żądania do handlerów.
Edytor nie może przyjmować połączeń z twojej przeglądarki, więc przykłady na tej stronie uruchamiają serwer przez httptest.NewServer i wywołują go z tego samego programu. W prawdziwym programie ostatnią część zastępuje jedna linia, która blokuje i obsługuje żądania bez końca:
log.Fatal(http.ListenAndServe(":8080", mux))
Wtedy curl localhost:8080/hello/gopher wypisze Hello, gopher!. ListenAndServe zwraca tylko przy błędzie (np. gdy port jest zajęty), dlatego jest opakowane w log.Fatal.
Każde żądanie działa we własnej gorutynie. Wszystko, co współdzielą twoje handlery, np. mapa albo licznik, potrzebuje mutexu.
Handlery
Wszystko, co ma metodę ServeHTTP(http.ResponseWriter, *http.Request), jest http.Handler. http.HandlerFunc dopasowuje zwykłą funkcję do tego interfejsu, a mux.HandleFunc robi tę konwersję za ciebie. Handler w postaci struktury przydaje się, gdy handlery potrzebują zależności:
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 daje ci:
| Pole lub metoda | Zawiera |
|---|---|
r.Method | GET, POST, ... |
r.URL.Path | ścieżkę, /items/42 |
r.PathValue("id") | wildcard ze wzorca trasy (Go 1.22) |
r.URL.Query().Get("q") | parametr z query stringa |
r.Header.Get("Authorization") | nagłówek żądania |
r.Body | body żądania, io.ReadCloser (zamyka je serwer) |
r.FormValue("name") | pole formularza lub parametr zapytania |
r.Context() | kontekst anulowany, gdy klient się rozłączy |
Wzorce routingu (Go 1.22)
Od Go 1.22 wzorce ServeMux mają postać [METHOD ][HOST]/[PATH], a ścieżki mogą zawierać wildcardy.
| Wzorzec | Dopasowuje |
|---|---|
"/items/" | /items/ i wszystko pod spodem (ukośnik na końcu = prefiks) |
"/items" | tylko /items |
"GET /items/{id}" | GET (i HEAD) na /items/42; r.PathValue("id") == "42" |
"POST /items" | tylko POST na /items |
"/files/{path...}" | /files/a/b/c; path to "a/b/c" |
"/{$}" | tylko /, a nie każdą ścieżkę |
"/" | każdą ścieżkę, której nie dopasował żaden inny wzorzec |
Gdy pasują dwa wzorce, wygrywa bardziej szczegółowy, więc /items/new wygrywa z /items/{id}. Jeśli żaden nie jest bardziej szczegółowy, np. /items/{id} i /{kind}/new (oba pasują do /items/new), rejestracja drugiego wywołuje panikę z komunikatem, który wymienia oba wzorce. Jeśli ścieżka pasuje, ale metoda nie, mux sam odpowiada 405 Method Not Allowed z nagłówkiem Allow, bez żadnego kodu z twojej strony.
Wzorzec "/" łapie wszystko. To zaskakuje osoby, które rejestrują stronę główną pod "/", a potem widzą, że odpowiada ona kodem 200 na każdy nieznany URL. Dla strony głównej użyj "GET /{$}".
Te wzorce wymagają go 1.22 lub nowszego w go.mod. Przy starszej wersji w tej linii mux wraca do dawnego zachowania: czyta "GET /items" jako nazwę hosta, po której następuje ścieżka, więc trasa nigdy nie pasuje i każde żądanie do /items dostaje 404.
Małe API w JSON
Ostatnie żądanie używa DELETE, którego nie przyjmuje żadna trasa, więc mux sam odpowiada 405 z Allow: GET, HEAD: to metody zarejestrowane dla tej ścieżki (trasa GET przyjmuje też HEAD).
Kody statusu i kolejność zapisów
Odpowiedź ma trzy części i trzeba je zapisać w kolejności: nagłówki, status, body.
w.Header().Set(...)zmienia nagłówki. Działa tylko do momentu wysłania statusu.w.WriteHeader(code)wysyła linię statusu i nagłówki.w.Write(...)(albofmt.Fprint(w, ...)lub enkoder) wysyła body. JeśliWriteHeadernie zostało wywołane, pierwszeWritenajpierw wysyła200 OK.
Konsekwencje:
- Ustawienie nagłówka po rozpoczęciu body nic nie daje.
- Dwukrotne wywołanie
WriteHeaderzapisuje w loguhttp: superfluous response.WriteHeader calli zachowuje pierwszy status. - Po odpowiedzi z błędem zrób
return.http.Errornie zatrzymuje handlera, a kod po nim dalej pisze do tej samej odpowiedzi.
Używaj nazwanych stałych (http.StatusOK, http.StatusCreated, http.StatusBadRequest, http.StatusUnauthorized, http.StatusNotFound, http.StatusInternalServerError) zamiast gołych liczb. http.StatusText(404) zwraca "Not Found".
Middleware
Middleware to funkcja, która przyjmuje handler i zwraca handler, robiąc coś przed wywołaniem następnego albo po nim:
Linia log: zawsze pojawia się przed client got: dla tego samego żądania. Gorutyna handlera wypisuje ją, zanim zwróci, a serwer nie kończy odpowiedzi, dopóki handler nie zwróci.
statusRecorder osadza prawdziwy ResponseWriter i nadpisuje tylko WriteHeader, co jest standardowym sposobem na podejrzenie kodu statusu z poziomu middleware.
Ustawienia produkcyjne
http.ListenAndServe używa serwera bez timeoutów, więc wolny klient może trzymać połączenie otwarte w nieskończoność. Dla wszystkiego, co jest wystawione do internetu, skonfiguruj http.Server jawnie i zamykaj go łagodnie:
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 przestaje przyjmować nowe połączenia i czeka, aż aktywne żądania się zakończą, maksymalnie do terminu z kontekstu. ListenAndServe zwraca http.ErrServerClosed, gdy tylko zacznie się Shutdown, dlatego ten błąd nie jest traktowany jako awaria. W długo działających handlerach przekazuj r.Context() dalej, żeby przerywały pracę, gdy klient odejdzie.
Serwowanie plików statycznych to jedna linia: mux.Handle("GET /static/", http.StripPrefix("/static/", http.FileServer(http.Dir("public")))).
Częste błędy
- Brak
returnpohttp.Error. Handler działa dalej i dopisuje kolejne dane do odpowiedzi. - Ustawianie nagłówków po zapisaniu body. Są po cichu pomijane.
- Rejestrowanie strony głównej pod
"/". Staje się ona łapaczem każdej nieznanej ścieżki. Użyj"/{$}". - Współdzielenie stanu między handlerami bez blokady. Żądania działają współbieżnie.
- Używanie domyślnego serwera na produkcji. Nie ma timeoutów; ustaw je na
http.Server. - Ignorowane wzorce routingu. Jeśli
"GET /x/{id}"nigdy nie pasuje, sprawdź, czygo.modzawierago 1.22lub nowsze.
Najczęściej zadawane pytania
Jak utworzyć prosty serwer WWW w Go?
Zarejestruj handler i zacznij nasłuchiwać: http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "hello") }), a potem log.Fatal(http.ListenAndServe(":8080", nil)). Serwer z biblioteki standardowej nadaje się na produkcję; obsługuje HTTP/1.1, HTTP/2 przez TLS, keep-alive i jedną gorutynę na połączenie.
Jak odczytać parametr ścieżki w Go net/http?
Od Go 1.22 wzorce ServeMux mogą zawierać wildcardy: zarejestruj mux.HandleFunc("GET /items/{id}", h) i odczytaj wartość w handlerze przez r.PathValue("id"). Końcowe {path...} dopasowuje resztę ścieżki. Przed 1.22 trzeba było samodzielnie dzielić r.URL.Path albo użyć routera, np. chi.
Czy do REST API w Go potrzebny jest framework taki jak Gin?
Nie. Od Go 1.22 net/http rozróżnia trasy po metodzie i parametrach ścieżki, a to był główny powód sięgania po routery. Z encoding/json do obsługi body i małymi funkcjami middleware do logowania i autoryzacji biblioteka standardowa wystarcza większości API. Frameworki dokładają udogodnienia, takie jak wiązanie i walidacja żądań.
Jak ustawić kod statusu w handlerze HTTP w Go?
Wywołaj w.WriteHeader(http.StatusCreated) przed zapisaniem body. Jeśli najpierw zapiszesz body, Go automatycznie wyśle 200 OK, a późniejsze WriteHeader zostanie zignorowane z komunikatem w logu superfluous response.WriteHeader call. Nagłówki też ustawiaj przez w.Header().Set przed WriteHeader; przy błędach http.Error(w, msg, code) robi to wszystko naraz.