Menu

Serwer HTTP w Golang: routing net/http, JSON i kody statusu

Jak zbudować serwer WWW na standardowym pakiecie net/http w Go: handlery, routing ServeMux z metodami i wildcardami w ścieżce (Go 1.22), odpowiedzi JSON, kody statusu, middleware, timeouty i łagodne zamykanie.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

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 metodaZawiera
r.MethodGET, 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.Bodybody żą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.

WzorzecDopasowuje
"/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.

  1. w.Header().Set(...) zmienia nagłówki. Działa tylko do momentu wysłania statusu.
  2. w.WriteHeader(code) wysyła linię statusu i nagłówki.
  3. w.Write(...) (albo fmt.Fprint(w, ...) lub enkoder) wysyła body. Jeśli WriteHeader nie zostało wywołane, pierwsze Write najpierw wysyła 200 OK.

Konsekwencje:

  • Ustawienie nagłówka po rozpoczęciu body nic nie daje.
  • Dwukrotne wywołanie WriteHeader zapisuje w logu http: superfluous response.WriteHeader call i zachowuje pierwszy status.
  • Po odpowiedzi z błędem zrób return. http.Error nie 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 return po http.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ź, czy go.mod zawiera go 1.22 lub 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.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ