net/http
net/http is Go’s built-in HTTP server and client — production-ready out of the box. With ServeMux, middleware via handler wrappers, and context.Context threaded through requests, you can build APIs without a framework.
Server, mux, middleware, client
EXAMPLE
package main
import (
"context"
"encoding/json"
"errors"
"log/slog"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
// 1) Minimal server
func hello(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain")
w.Write([]byte("hello world"))
}
func main_simple() {
http.HandleFunc("/hello", hello)
http.ListenAndServe(":8080", nil)
}
// 2) ServeMux 1.22+ — pattern routing with methods + path params
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /", home)
mux.HandleFunc("GET /users", listUsers)
mux.HandleFunc("GET /users/{id}", getUser)
mux.HandleFunc("POST /users", createUser)
mux.HandleFunc("DELETE /users/{id}", deleteUser)
srv := &http.Server{
Addr: ":8080",
Handler: logging(recovery(mux)),
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 15 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 60 * time.Second,
}
go srv.ListenAndServe()
// Graceful shutdown
sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
<-sig
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
srv.Shutdown(ctx)
}
func getUser(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
w.Write([]byte("user " + id))
}
// 3) JSON responses
func writeJSON(w http.ResponseWriter, status int, v any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(v)
}
func listUsers(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, []map[string]any{
{"id": 1, "name": "Mara"},
{"id": 2, "name": "Sam"},
})
}
func createUser(w http.ResponseWriter, r *http.Request) {
var body struct {
Name string `json:"name"`
Email string `json:"email"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"})
return
}
writeJSON(w, http.StatusCreated, map[string]any{"id": 42, "name": body.Name})
}
// 4) Middleware via handler wrappers
func logging(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
sw := &statusWriter{ResponseWriter: w, status: 200}
next.ServeHTTP(sw, r)
slog.Info("http", "method", r.Method, "path", r.URL.Path, "status", sw.status, "ms", time.Since(start).Milliseconds())
})
}
func recovery(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if rec := recover(); rec != nil {
slog.Error("panic", "err", rec)
http.Error(w, "internal error", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
type statusWriter struct {
http.ResponseWriter
status int
}
func (s *statusWriter) WriteHeader(code int) {
s.status = code
s.ResponseWriter.WriteHeader(code)
}
// 5) Auth middleware
func requireAuth(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
if token == "" {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
// verify token, attach user to context
ctx := context.WithValue(r.Context(), "userID", "42")
next.ServeHTTP(w, r.WithContext(ctx))
})
}
// 6) HTTP client
func fetchUser(ctx context.Context, id string) (*User, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.example.com/users/"+id, nil)
if err != nil { return nil, err }
req.Header.Set("Accept", "application/json")
client := &http.Client{ Timeout: 10 * time.Second }
resp, err := client.Do(req)
if err != nil { return nil, err }
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, errors.New("non-200: " + resp.Status)
}
var u User
if err := json.NewDecoder(resp.Body).Decode(&u); err != nil {
return nil, err
}
return &u, nil
}
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
// 7) Custom Transport — connection pool tuning
client := &http.Client{
Timeout: 30 * time.Second,
Transport: &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 10,
IdleConnTimeout: 90 * time.Second,
DisableCompression: false,
},
}
// REUSE a single client across the program. Don't create per request.
// 8) CORS — write your own (or use 'rs/cors')
func cors(allowed []string) func(http.Handler) http.Handler {
set := make(map[string]bool)
for _, o := range allowed { set[o] = true }
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
origin := r.Header.Get("Origin")
if set[origin] {
w.Header().Set("Access-Control-Allow-Origin", origin)
w.Header().Set("Vary", "Origin")
w.Header().Set("Access-Control-Allow-Credentials", "true")
}
if r.Method == http.MethodOptions {
w.Header().Set("Access-Control-Allow-Methods", "GET,POST,PUT,PATCH,DELETE")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}
}
// 9) Static files
fs := http.FileServer(http.Dir("./public"))
mux.Handle("GET /static/", http.StripPrefix("/static/", fs))
// 10) HTTPS with autocert
import "golang.org/x/crypto/acme/autocert"
m := &autocert.Manager{
Cache: autocert.DirCache("/var/www/.cache"),
Prompt: autocert.AcceptTOS,
HostPolicy: autocert.HostWhitelist("example.com"),
}
srv := &http.Server{
Addr: ":443",
Handler: mux,
TLSConfig: m.TLSConfig(),
}
srv.ListenAndServeTLS("", "")
// 11) Frameworks — when stdlib isn't enough
// • chi — lightweight router, idiomatic Go
// • gin — fast, opinionated, middleware ecosystem
// • echo — high performance, batteries-included
// • Fiber — Express-like API, fasthttp under the hood
//
// stdlib + a router (chi/mux) is plenty for most APIs. Skip frameworks until proven need.
// 12) Common bugs
// • Forgetting defer resp.Body.Close() → connection leak
// • Reusing http.Request for retries — body is consumed; clone or recreate
// • No timeouts on http.Client → hangs forever on slow upstream
// • Mux without method routing — handle 405 manually pre Go 1.22
// • Reading r.Body after calling r.ParseForm — body already consumed
// • Writing headers after WriteHeader → silently dropped; set headers FIRST
// • Forgot WithContext when calling external APIs → no cancellation
// • DefaultClient + DefaultTransport — fine for scripts; tune for prod
// • Sharing context across requests — each request has its own; use request context
// • Returning errors as 200 + JSON body — set proper status codes
Why it matters
Go’s net/http is production-ready: ServeMux 1.22+ handles method routing and path params, middleware via handler wrappers covers logging/recovery/CORS, and context.Context threads cancellation everywhere. Always set server timeouts, set http.Client.Timeout, reuse a single client, and defer resp.Body.Close().
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
http.HandleFunc("/hi", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello")
})
log.Fatal(http.ListenAndServe(":8080", nil))
Try it Yourself »
Discussion
Loading…