Een HTTP server bouwen in Go is verrassend eenvoudig dankzij het krachtige net/http pakket uit de standaardbibliotheek. Je hebt geen framework nodig om een productie-klare webserver op te zetten, en met een paar regels code heb je al een werkende API draaien.
In deze handleiding leer je stap voor stap hoe je een HTTP server bouwt, routes definieert, middleware schrijft en een nette shutdown implementeert. We bouwen voort op de basics uit installatie en eerste Go programma en duiken in praktische patronen die je direct kunt gebruiken.
Waarom Go ideaal is voor HTTP servers
Go is ontworpen met netwerkapplicaties in gedachten. De combinatie van goroutines, een uitstekende standaardbibliotheek en gecompileerde binaries maakt het een populaire keuze voor backend services.
Elke inkomende request wordt automatisch afgehandeld in een eigen goroutine. Dat betekent dat je server zonder extra configuratie duizenden gelijktijdige verbindingen kan bedienen. Meer achtergrond hierover lees je in waarom Go populair is voor backends in 2026.
Het net/http pakket is battle-tested: bedrijven als Cloudflare, Uber en Dropbox vertrouwen er in productie op. Geen frameworks, geen magie, gewoon een solide, idiomatische API.
Je eerste HTTP server in Go
Laten we klein beginnen. Een minimale HTTP server in Go ziet er zo uit:
package main
import (
"fmt"
"log"
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hallo vanuit Go!")
})
log.Println("Server draait op http://localhost:8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
Sla dit op als main.go en draai het met go run main.go. Open daarna http://localhost:8080 in je browser, je ziet "Hallo vanuit Go!".
Wat gebeurt er hier precies? http.HandleFunc registreert een handler voor een URL-pad. http.ListenAndServe start de server op poort 8080. De nil als tweede argument betekent: gebruik de standaard ServeMux (de router).
Handlers en de http.Handler interface
In Go is alles wat HTTP-requests afhandelt een http.Handler. Dit is simpelweg een interface met één methode:
type Handler interface {
ServeHTTP(w http.ResponseWriter, r *http.Request)
}
Omdat het een interface is, kan elk type dat deze methode implementeert als handler dienen. Meer over hoe interfaces werken vind je in structs en interfaces in Go.
HandleFunc vs Handle
http.HandleFunc is een gemak-wrapper die een functie omzet naar een Handler. Voor complexere handlers met state gebruik je vaak een struct:
type UserHandler struct {
db *Database
}
func (h *UserHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
user, err := h.db.GetUser(r.URL.Query().Get("id"))
if err != nil {
http.Error(w, "Niet gevonden", http.StatusNotFound)
return
}
fmt.Fprintf(w, "Gebruiker: %s", user.Name)
}
func main() {
handler := &UserHandler{db: newDatabase()}
http.Handle("/user", handler)
http.ListenAndServe(":8080", nil)
}
Deze aanpak werkt goed wanneer je dependencies zoals een database of logger moet doorgeven. Foutafhandeling werkt hier idiomatisch, zoals beschreven in functions en error handling in Go.
Routing met ServeMux
De standaard ServeMux ondersteunt sinds Go 1.22 method-based routing en path parameters. Dat maakt een externe router vaak overbodig:
mux := http.NewServeMux()
mux.HandleFunc("GET /users", listUsers)
mux.HandleFunc("GET /users/{id}", getUser)
mux.HandleFunc("POST /users", createUser)
mux.HandleFunc("DELETE /users/{id}", deleteUser)
log.Fatal(http.ListenAndServe(":8080", mux))
Je haalt path parameters op met r.PathValue("id"):
func getUser(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
fmt.Fprintf(w, "Gebruiker met ID: %s", id)
}
Voor complexere routing (regex patterns, subroutes) kun je alsnog kiezen voor een library zoals chi, maar voor veel projecten voldoet de standaard mux prima.
JSON responses afhandelen
De meeste moderne API's praten JSON. Het encoding/json pakket maakt dit simpel:
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
func getUser(w http.ResponseWriter, r *http.Request) {
user := User{ID: 1, Name: "Sander", Email: "[email protected]"}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(user); err != nil {
http.Error(w, "Fout bij encoderen", http.StatusInternalServerError)
return
}
}
Voor het inlezen van een JSON request body:
func createUser(w http.ResponseWriter, r *http.Request) {
var user User
if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
http.Error(w, "Ongeldige JSON", http.StatusBadRequest)
return
}
defer r.Body.Close()
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(user)
}
Let op: zet altijd eerst headers en daarna pas WriteHeader. Daarna kun je de body schrijven. Deze volgorde is belangrijk.
Middleware patronen
Middleware is een functie die een handler wikkelt om er gedrag aan toe te voegen. Een typisch voorbeeld is een logging middleware:
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
log.Printf("%s %s - %v", r.Method, r.URL.Path, time.Since(start))
})
}
Je past middleware toe door handlers in elkaar te wikkelen:
mux := http.NewServeMux()
mux.HandleFunc("GET /health", healthCheck)
handler := loggingMiddleware(mux)
log.Fatal(http.ListenAndServe(":8080", handler))
Middleware chainen
Meerdere middlewares combineren is gewoon geneste functie-calls:
handler := loggingMiddleware(authMiddleware(corsMiddleware(mux)))
Voor overzicht kun je een kleine helper bouwen:
func chain(h http.Handler, middlewares ...func(http.Handler) http.Handler) http.Handler {
for i := len(middlewares) - 1; i >= 0; i-- {
h = middlewares[i](h)
}
return h
}
handler := chain(mux, loggingMiddleware, authMiddleware, corsMiddleware)
Configureerbare http.Server
Voor productie wil je meer controle dan http.ListenAndServe biedt. Gebruik daarom een http.Server instantie:
server := &http.Server{
Addr: ":8080",
Handler: handler,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 120 * time.Second,
}
log.Fatal(server.ListenAndServe())
Timeouts zijn cruciaal. Zonder timeouts kan een kwaadwillende client connecties openen en je server laten vastlopen. De officiële Go documentatie raadt sterk aan deze altijd te zetten.
Graceful shutdown implementeren
Wanneer je server stopt, wil je dat lopende requests nog kunnen afronden. Dat heet graceful shutdown:
func main() {
server := &http.Server{
Addr: ":8080",
Handler: setupRoutes(),
}
go func() {
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("Server fout: %v", err)
}
}()
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("Server wordt afgesloten...")
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
log.Fatalf("Geforceerde shutdown: %v", err)
}
log.Println("Server netjes gestopt")
}
We gebruiken hier goroutines en channels om signalen op te vangen. Hoe die samenwerken lees je in goroutines en concurrency basics en channels diep uitgelegd.
Context gebruiken in handlers
Elke request in Go heeft een context.Context die wordt geannuleerd als de client de verbinding verbreekt. Gebruik die context voor database queries en externe calls:
func getUser(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
user, err := db.QueryUserContext(ctx, r.PathValue("id"))
if err != nil {
if ctx.Err() != nil {
return
}
http.Error(w, "Database fout", http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(user)
}
Zo voorkom je dat je server werk blijft doen voor clients die allang zijn weggelopen.
Best practices samengevat
Een paar richtlijnen die je projecten direct verbeteren:
- Zet altijd timeouts op je
http.Serverom resource leaks te voorkomen - Gebruik structured logging (bijvoorbeeld met
log/slog) voor productie - Valideer input aan de serverzijde, vertrouw nooit client data
- Retourneer juiste status codes, 201 voor created, 400 voor bad request, 404 voor not found
- Test handlers met
net/http/httptest, dat maakt unit tests triviaal - Draai achter een reverse proxy zoals Nginx of Caddy voor TLS en rate limiting
Voor verdere verdieping is Effective Go een onmisbare bron, net als Go by Example voor praktische snippets.
Veelgestelde vragen
Heb ik een framework nodig om een HTTP server te bouwen in Go?
Nee, de standaardbibliotheek net/http bevat alles wat je nodig hebt voor productie-klare servers. Frameworks zoals Gin of Echo voegen gemak toe, maar zijn niet verplicht en vaak zelfs overbodig voor eenvoudige API's.
Wat is het verschil tussen http.Handle en http.HandleFunc?
http.Handle accepteert een type dat de Handler interface implementeert, terwijl http.HandleFunc een gewone functie met de juiste signatuur accepteert. HandleFunc is korter voor simpele gevallen, Handle is handig voor complexere handlers met state.
Hoe voeg ik middleware toe aan mijn Go HTTP server?
Middleware in Go is een functie die een http.Handler ontvangt en een nieuwe http.Handler teruggeeft. Je wikkelt handlers in elkaar om functionaliteit zoals logging, authenticatie of CORS toe te voegen.
Hoe implementeer ik graceful shutdown in Go?
Gebruik http.Server met de Shutdown methode in combinatie met signal.Notify om SIGINT en SIGTERM op te vangen. Zo kunnen lopende requests netjes afronden voordat het proces stopt.
Is de standaard Go HTTP server snel genoeg voor productie?
Ja, de net/http server is zeer performant en wordt door grote bedrijven gebruikt in productie. Hij gebruikt goroutines per request, waardoor hij moeiteloos duizenden gelijktijdige verbindingen aankan.