Menu

Golang HTTP-Server: Routing mit net/http, JSON und Statuscodes

So baust du einen Webserver mit dem Standardpaket net/http von Go: Handler, Routing mit ServeMux samt Methoden und Pfad-Wildcards (Go 1.22), JSON-Antworten, Statuscodes, Middleware, Timeouts und Graceful Shutdown.

Diese Seite enthält ausführbare Editoren - bearbeiten, ausführen und Ausgabe sofort sehen.

Ein Server in wenigen Zeilen

Ein Handler ist eine Funktion, die den Request bekommt und die Antwort schreibt. ServeMux leitet Requests an Handler weiter.

Der Editor kann keine Verbindungen von deinem Browser annehmen, also starten die Beispiele auf dieser Seite den Server mit httptest.NewServer und rufen ihn aus demselben Programm auf. In einem echten Programm ersetzt eine einzige Zeile den letzten Teil, die blockiert und für immer bedient:

log.Fatal(http.ListenAndServe(":8080", mux))

Dann gibt curl localhost:8080/hello/gopher Hello, gopher! aus. ListenAndServe kehrt nur bei einem Fehler zurück (etwa wenn der Port belegt ist), deshalb steht es in log.Fatal.

Jeder Request läuft in seiner eigenen Goroutine. Alles, was deine Handler teilen, etwa eine Map oder ein Zähler, braucht einen Mutex.

Handler

Alles mit einer Methode ServeHTTP(http.ResponseWriter, *http.Request) ist ein http.Handler. http.HandlerFunc passt eine einfache Funktion an dieses Interface an, und mux.HandleFunc übernimmt die Konvertierung für dich. Ein Struct als Handler ist praktisch, wenn Handler Abhängigkeiten brauchen:

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)

Der *http.Request liefert dir:

Feld oder MethodeEnthält
r.MethodGET, POST, ...
r.URL.Pathden Pfad, /items/42
r.PathValue("id")eine Wildcard aus dem Routenmuster (Go 1.22)
r.URL.Query().Get("q")einen Parameter aus dem Query-String
r.Header.Get("Authorization")einen Request-Header
r.Bodyden Request-Body, einen io.ReadCloser (der Server schließt ihn)
r.FormValue("name")ein Formularfeld oder einen Query-Parameter
r.Context()einen Context, der abgebrochen wird, wenn der Client die Verbindung trennt

Routenmuster (Go 1.22)

Seit Go 1.22 haben Muster von ServeMux die Form [METHOD ][HOST]/[PATH], und Pfade können Wildcards enthalten.

MusterPasst auf
"/items/"/items/ und alles darunter (Schrägstrich am Ende = Präfix)
"/items"nur /items
"GET /items/{id}"GET (und HEAD) auf /items/42; r.PathValue("id") == "42"
"POST /items"nur POST auf /items
"/files/{path...}"/files/a/b/c; path ist "a/b/c"
"/{$}"nur /, nicht jeden Pfad
"/"jeden Pfad, auf den kein anderes Muster passt

Passen zwei Muster, gewinnt das spezifischere, also schlägt /items/new das Muster /items/{id}. Ist keines spezifischer, etwa /items/{id} und /{kind}/new (beide passen auf /items/new), löst das Registrieren des zweiten eine Panic mit einer Meldung aus, die beide Muster nennt. Passt ein Pfad, aber nicht die Methode, antwortet der Mux ohne jeden Code von dir mit 405 Method Not Allowed und einem Allow-Header.

Das Muster "/" fängt alles auf. Das überrascht alle, die eine Startseite mit "/" registrieren und dann feststellen, dass sie jede unbekannte URL mit 200 beantwortet. Nimm "GET /{$}" für die Startseite.

Diese Muster brauchen go 1.22 oder höher in go.mod. Mit einer älteren Versionszeile fällt der Mux auf das alte Verhalten zurück: Er liest "GET /items" als Hostnamen gefolgt von einem Pfad, also passt die Route nie, und jeder Request an /items bekommt ein 404.

Eine kleine JSON-API

Der letzte Request verwendet DELETE, das keine Route akzeptiert, und der Mux antwortet von selbst mit 405 und Allow: GET, HEAD: Das sind die Methoden, die für diesen Pfad registriert sind (eine GET-Route akzeptiert auch HEAD).

Statuscodes und die Reihenfolge der Schreibzugriffe

Eine Antwort hat drei Teile, und sie müssen in dieser Reihenfolge geschrieben werden: Header, Status, Body.

  1. w.Header().Set(...) ändert Header. Das wirkt nur, bis der Status gesendet ist.
  2. w.WriteHeader(code) sendet die Statuszeile und die Header.
  3. w.Write(...) (oder fmt.Fprint(w, ...) oder ein Encoder) sendet den Body. Wurde WriteHeader nicht aufgerufen, sendet das erste Write zuerst 200 OK.

Die Folgen:

  • Einen Header zu setzen, nachdem der Body begonnen hat, bewirkt nichts.
  • WriteHeader zweimal aufzurufen loggt http: superfluous response.WriteHeader call und behält den ersten Status.
  • Nach einer Fehlerantwort: return. http.Error stoppt deinen Handler nicht, und Code danach schreibt weiter in dieselbe Antwort.

Nimm die benannten Konstanten (http.StatusOK, http.StatusCreated, http.StatusBadRequest, http.StatusUnauthorized, http.StatusNotFound, http.StatusInternalServerError) statt nackter Zahlen. http.StatusText(404) gibt "Not Found" zurück.

Middleware

Middleware ist eine Funktion, die einen Handler nimmt und einen Handler zurückgibt und vor oder nach dem Aufruf des nächsten etwas tut:

Die Zeile log: erscheint für denselben Request immer vor client got:. Die Handler-Goroutine gibt sie aus, bevor sie zurückkehrt, und der Server schließt die Antwort erst ab, wenn der Handler zurückkehrt.

Der statusRecorder bettet den echten ResponseWriter ein und überschreibt nur WriteHeader. Das ist der Standardweg, um den Statuscode aus einer Middleware heraus zu beobachten.

Einstellungen für die Produktion

http.ListenAndServe nutzt einen Server ohne Timeouts, also kann ein langsamer Client eine Verbindung unbegrenzt offen halten. Konfigurier für alles, was im Internet erreichbar ist, explizit einen http.Server und fahr ihn geordnet herunter:

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 nimmt keine neuen Verbindungen mehr an und wartet, bis laufende Requests fertig sind, höchstens bis zur Deadline des Context. ListenAndServe gibt http.ErrServerClosed zurück, sobald Shutdown beginnt, deshalb wird dieser Fehler nicht als Fehlschlag behandelt. Reich bei lange laufenden Handlern r.Context() nach unten weiter, damit sie aufhören, wenn der Client weg ist.

Statische Dateien auszuliefern ist eine Zeile: mux.Handle("GET /static/", http.StripPrefix("/static/", http.FileServer(http.Dir("public")))).

Häufige Fehler

  • Nach http.Error nicht zurückkehren. Der Handler läuft weiter und schreibt mehr in die Antwort.
  • Header nach dem Body setzen. Sie werden still verworfen.
  • Die Startseite auf "/" registrieren. Sie fängt dann jeden unbekannten Pfad auf. Nimm "/{$}".
  • Zustand zwischen Handlern ohne Lock teilen. Requests laufen nebenläufig.
  • Den Standard-Server in Produktion verwenden. Er hat keine Timeouts; setz sie auf einem http.Server.
  • Ignorierte Routenmuster. Passt "GET /x/{id}" nie, prüf, ob in go.mod go 1.22 oder höher steht.

Häufig gestellte Fragen

Wie erstelle ich in Go einen einfachen Webserver?

Registrier einen Handler und starte das Lauschen: http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "hello") }), dann log.Fatal(http.ListenAndServe(":8080", nil)). Der Server der Standardbibliothek ist produktionstauglich; er beherrscht HTTP/1.1, HTTP/2 über TLS, Keep-Alive und eine Goroutine pro Verbindung.

Wie bekomme ich in Go mit net/http einen Pfadparameter?

Seit Go 1.22 können Muster von ServeMux Wildcards enthalten: Registrier mux.HandleFunc("GET /items/{id}", h) und lies den Wert im Handler mit r.PathValue("id"). Ein {path...} am Ende passt auf den Rest des Pfads. Vor 1.22 musstest du r.URL.Path selbst zerlegen oder einen Router wie chi verwenden.

Brauche ich in Go ein Framework wie Gin, um eine REST-API zu bauen?

Nein. Seit Go 1.22 routet net/http nach Methode und Pfadparametern, und das war der Hauptgrund, warum Leute Router verwendet haben. Mit encoding/json für die Bodys und kleinen Middleware-Funktionen für Logging und Authentifizierung reicht die Standardbibliothek für die meisten APIs. Frameworks bringen Annehmlichkeiten wie Request-Binding und Validierung mit.

Wie setze ich in einem Go-HTTP-Handler den Statuscode?

Ruf w.WriteHeader(http.StatusCreated) auf, bevor du den Body schreibst. Schreibst du zuerst den Body, sendet Go automatisch 200 OK, und ein späteres WriteHeader wird mit der Log-Meldung superfluous response.WriteHeader call ignoriert. Setz auch Header mit w.Header().Set vor WriteHeader; für Fehler erledigt http.Error(w, msg, code) all das.

Coddy programming languages illustration

Lerne mit Coddy zu programmieren

LOS GEHT'S