net/http Basics
9 examples to get you started with net/http - 7 basic and 2 intermediate.
Prerequisites
- Install Go 1.26.x or later from go.dev/dl.
- Create a module:
mkdir httplab && cd httplab && go mod init example.com/httplab. - Save each snippet as
main.go(or separate files in one package) and run withgo run ..
Basic Examples
1. HandlerFunc and a Minimal Server
A function with signature func(http.ResponseWriter, *http.Request) can serve HTTP directly.
package main
import (
"fmt"
"net/http"
)
func main() {
http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "hello")
})
http.ListenAndServe(":8080", nil)
}http.HandleFuncregisters onhttp.DefaultServeMux.- Passing
niltoListenAndServeuses that default mux. - Prefer a dedicated
ServeMuxin real services to avoid global route collisions.
Related: net/http: Go's Batteries-Included HTTP Stack - conceptual overview
2. Dedicated ServeMux
Isolate routes in a mux you own instead of the global default.
package main
import (
"net/http"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("ok"))
})
http.ListenAndServe(":8080", mux)
}http.NewServeMux()returns an empty router.- Pass the mux as the second argument to
ListenAndServe. - Tests can construct the same mux without touching package globals.
Related: HTTP Servers, Handlers & ServeMux Patterns - Go 1.22+ patterns
3. Method and Path Patterns (Go 1.22+)
Register routes with HTTP method and {param} segments in one call.
package main
import (
"fmt"
"net/http"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /users/{id}", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "user=%s", r.PathValue("id"))
})
http.ListenAndServe(":8080", mux)
}- Pattern syntax is
"METHOD /path/{name}"on Go 1.22 and later. r.PathValue("id")reads captured segments.- Unmatched method/path pairs return 404 automatically.
Related: HTTP Servers, Handlers & ServeMux Patterns - routing patterns
4. Setting Status and Headers
Set headers before writing the body; the first Write sends headers implicitly.
package main
import (
"net/http"
)
func main() {
http.HandleFunc("/json", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
w.Write([]byte(`{"ok":true}`))
})
http.ListenAndServe(":8080", nil)
}- Call
WriteHeaderonly once per response. Header().Setis case-canonicalized by net/http.- Use
http.Errorfor quick plain-text error responses.
5. Reading the Request Body
Decode JSON from r.Body and always close what you open on the client side.
package main
import (
"encoding/json"
"io"
"net/http"
)
type Payload struct {
Name string `json:"name"`
}
func main() {
http.HandleFunc("/echo", func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
var p Payload
if err := json.NewDecoder(r.Body).Decode(&p); err != nil && err != io.EOF {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
json.NewEncoder(w).Encode(p)
})
http.ListenAndServe(":8080", nil)
}r.Bodymust be read or closed to reuse keep-alive connections.- Limit body size with
http.MaxBytesReaderin production handlers. - Prefer
json.NewDecoderfor streaming large payloads.
6. http.Client GET with Context
Outbound calls should use a client with timeout and context cancellation.
package main
import (
"context"
"fmt"
"io"
"net/http"
"time"
)
func main() {
client := &http.Client{Timeout: 5 * time.Second}
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, "https://example.com", nil)
resp, err := client.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
b, _ := io.ReadAll(resp.Body)
fmt.Println(resp.Status, len(b))
}- Never use
http.Getin servers; it useshttp.DefaultClientwithout timeout. - Always
defer resp.Body.Close()to return connections to the pool. NewRequestWithContextties the call to caller cancellation.
Related: HTTP Clients, Transport & Connection Pooling - transport tuning
7. Simple Middleware Wrapper
Middleware wraps an inner handler and runs code before or after it.
package main
import (
"log"
"net/http"
"time"
)
func logging(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 %s", r.Method, r.URL.Path, time.Since(start))
})
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/work", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("done"))
})
http.ListenAndServe(":8080", logging(mux))
}- Middleware signature is
func(http.Handler) http.Handler. - Order matters: outer middleware runs first on the way in.
- Pass the wrapped mux to
ListenAndServe, not the bare inner handler.
Related: Middleware Chains & Request Context - chains and values
Intermediate Examples
8. http.Server with ReadHeaderTimeout
Configure timeouts on http.Server instead of bare ListenAndServe.
package main
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/api", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("api"))
})
srv := &http.Server{
Addr: ":8080",
Handler: mux,
ReadHeaderTimeout: 5 * time.Second,
IdleTimeout: 60 * time.Second,
}
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatal(err)
}
}()
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
_ = srv.Shutdown(ctx)
}ReadHeaderTimeoutmitigates slowloris-style header attacks.Shutdowndrains in-flight requests during deploys.- Run the listener in a goroutine so
maincan handle signals.
Related: Production net/http Configuration Checklist - full production list
9. httptest.ResponseRecorder
Test handlers in memory without opening a TCP port.
package main
import (
"net/http"
"net/http/httptest"
"testing"
)
func greet(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("hi"))
}
func TestGreet(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/greet", nil)
rec := httptest.NewRecorder()
greet(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status=%d", rec.Code)
}
if rec.Body.String() != "hi" {
t.Fatalf("body=%q", rec.Body.String())
}
}httptest.NewRecorderimplementshttp.ResponseWriterand records status/body.httptest.NewRequestbuilds a*http.Requestwith optional body.- Use table tests to cover methods, headers, and error paths.
Related: httptest for Handler Integration Tests - fuller testing patterns
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).