Networking with net/http
Go's net/http is production-grade out of the box. Many
companies run real-world HTTP services against it with no framework
layered on top. This chapter gives you enough to ship a real JSON API.
Learning objectives
- Write a minimal HTTP server.
- Understand
http.Handlerandhttp.HandlerFunc. - Route requests with
http.ServeMux(including Go 1.22+ method + path-param patterns). - Parse JSON bodies and write JSON responses.
- Compose middleware with a clean handler-wrapping pattern.
- Make HTTP calls with
http.Client.
Hello server
package main
import (
"fmt"
"net/http"
)
func main() {
http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "hi,", r.URL.Query().Get("name"))
})
http.ListenAndServe(":8080", nil)
}
Hit http://localhost:8080/hello?name=Alice. That's
all it takes, no framework, no config file.
http.Handler
type Handler interface {
ServeHTTP(w ResponseWriter, r *Request)
}
Everything HTTP in Go boils down to this interface.
http.HandlerFunc is a type that adapts a
func(w, r) into a Handler:
var h http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("hello"))
})
Routing with http.ServeMux
mux := http.NewServeMux()
mux.HandleFunc("/users", listUsers) // any method
mux.HandleFunc("/users/{id}", getUser) // Go 1.22+ path params
mux.HandleFunc("POST /users", createUser) // Go 1.22+ method binding
http.ListenAndServe(":8080", mux)
Since Go 1.22, the built-in mux supports method verbs and path wildcards, fewer teams now need a 3rd-party router for basic cases.
Path params (Go 1.22+)
mux.HandleFunc("GET /users/{id}", func(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
fmt.Fprintf(w, "user %s\n", id)
})
JSON in / JSON out
type User struct { Name string `json:"name"` }
func createUser(w http.ResponseWriter, r *http.Request) {
var u User
if err := json.NewDecoder(r.Body).Decode(&u); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// ... save u ...
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(u)
}
Middleware
Middleware is a func(http.Handler) http.Handler: a
handler that wraps another handler. Log every request:
func logger(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))
})
}
// Chain them, innermost runs first:
handler := logger(auth(mux))
http.ListenAndServe(":8080", handler)
http.Client
client := &http.Client{Timeout: 5 * time.Second}
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
req.Header.Set("Accept", "application/json")
resp, err := client.Do(req)
if err != nil { /* handle */ }
defer resp.Body.Close()
var users []User
json.NewDecoder(resp.Body).Decode(&users)
http.Client has no timeout, a
hanging server will keep your goroutine blocked forever. Always
create your own client with a timeout, or use
http.NewRequestWithContext with a context deadline.
Graceful shutdown
srv := &http.Server{Addr: ":8080", Handler: mux}
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatal(err)
}
}()
// Wait for SIGINT/SIGTERM
stop := make(chan os.Signal, 1)
signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)
<-stop
// Give in-flight requests 10 seconds to finish
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
srv.Shutdown(ctx)
Production notes
- Always set read/write timeouts on
http.Server:ReadHeaderTimeoutat minimum. - Don't reinvent the wheel.
chiorgorilla/muxare popular if you need more routing power. For most services Go 1.22's mux is plenty. - Pass the request context (
r.Context()) into any call that could outlive the request, DB queries, HTTP calls, goroutines. - Use
httptestfor fast, deterministic handler tests (Chapter 31).
Check your understanding
Practice exercises
Tiny JSON API
Build a server that stores an in-memory list of "notes" (a note has an ID, Title, Body). Support GET /notes (list), POST /notes (create), and GET /notes/{id} (fetch one). Use sync.Mutex around the store.
Show sketch solution
package main
import (
"encoding/json"
"net/http"
"strconv"
"sync"
)
type Note struct {
ID int `json:"id"`
Title string `json:"title"`
Body string `json:"body"`
}
var (
mu sync.Mutex
notes = map[int]Note{}
nextID = 1
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /notes", func(w http.ResponseWriter, r *http.Request) {
mu.Lock(); defer mu.Unlock()
out := make([]Note, 0, len(notes))
for _, n := range notes { out = append(out, n) }
json.NewEncoder(w).Encode(out)
})
mux.HandleFunc("POST /notes", func(w http.ResponseWriter, r *http.Request) {
var n Note
if err := json.NewDecoder(r.Body).Decode(&n); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest); return
}
mu.Lock(); defer mu.Unlock()
n.ID = nextID; nextID++
notes[n.ID] = n
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(n)
})
mux.HandleFunc("GET /notes/{id}", func(w http.ResponseWriter, r *http.Request) {
id, _ := strconv.Atoi(r.PathValue("id"))
mu.Lock(); defer mu.Unlock()
n, ok := notes[id]
if !ok { http.NotFound(w, r); return }
json.NewEncoder(w).Encode(n)
})
http.ListenAndServe(":8080", mux)
}
Further reading
net/httppackage- Writing Web Applications (official tutorial)
- Go 1.22 routing enhancements
- chi: a popular 3rd-party router, if you outgrow the stdlib
Shippable web service, done.