net/http

Guided tour · Networking · pkg.go.dev →

HTTP server and client. Batteries included — no framework required.

HTTP client + server in the same package. The zero-value http.Client.Get works for quick scripts; for production wire your own Client with a Timeout.

Quick GET
resp, err := http.Get(url)
if err != nil { return err }
defer resp.Body.Close()
b, _ := io.ReadAll(resp.Body)
Client with timeout
c := &http.Client{Timeout: 10 * time.Second}
resp, err := c.Get(url)
POST JSON
body, _ := json.Marshal(payload)
resp, err := http.Post(url, "application/json", bytes.NewReader(body))
Build a request with headers
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
req.Header.Set("Authorization", "Bearer "+token)
resp, err := http.DefaultClient.Do(req)
Tiny server
http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, "hello")
})
log.Fatal(http.ListenAndServe(":8080", nil))
Path values (Go 1.22+)
mux.HandleFunc("GET /users/{id}", func(w http.ResponseWriter, r *http.Request) {
    id := r.PathValue("id")
    fmt.Fprintln(w, id)
})

Client — making requests

http.Get — the simplest thing

http.Get, Post, and Head use http.DefaultClient. Fine for scripts; avoid in production — no timeout.

resp, err := http.Get("https://example.com")
if err != nil { log.Fatal(err) }
defer resp.Body.Close()

b, _ := io.ReadAll(resp.Body)
fmt.Println(resp.StatusCode, len(b))

Always use a client with a timeout

The zero-value http.Client has NO timeout and will hang forever on a bad network. Always set Timeout.

client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Get(url)

NewRequestWithContext — cancellable requests

req, _ := http.NewRequestWithContext(ctx, "POST", url, body)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)

resp, err := client.Do(req)

Always drain and close the body

If you Close without draining, the underlying TCP connection can't be reused. io.Copy(io.Discard, body) drains it cheaply.

defer func() {
    io.Copy(io.Discard, resp.Body)
    resp.Body.Close()
}()

Server — handling requests

Minimal server

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintln(w, "hello world")
    })
    log.Fatal(http.ListenAndServe(":8080", nil))
}

ServeMux patterns (Go 1.22+)

ServeMux now supports method matching and wildcards. For most apps you don't need a third-party router anymore.

mux := http.NewServeMux()
mux.HandleFunc("GET /users/{id}", getUser)
mux.HandleFunc("POST /users", createUser)
mux.HandleFunc("GET /static/", serveStatic)   // subtree
mux.HandleFunc("GET /{$}", home)              // exact "/"

http.ListenAndServe(":8080", mux)

Path parameters

func getUser(w http.ResponseWriter, r *http.Request) {
    id := r.PathValue("id")
    ...
}

Production server — explicit timeouts and graceful shutdown

srv := &http.Server{
    Addr:              ":8080",
    Handler:           mux,
    ReadHeaderTimeout: 5 * time.Second,
    ReadTimeout:       15 * time.Second,
    WriteTimeout:      30 * time.Second,
    IdleTimeout:       60 * time.Second,
}

ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()

go srv.ListenAndServe()
<-ctx.Done()

shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
srv.Shutdown(shutdownCtx)

Reading and writing bodies

Decode JSON request body

func createUser(w http.ResponseWriter, r *http.Request) {
    var u User
    dec := json.NewDecoder(r.Body)
    dec.DisallowUnknownFields()
    if err := dec.Decode(&u); err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }
    ...
}

Write JSON response

w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(u)

Form values and query params

r.ParseForm()            // populates r.Form from URL + body
q := r.URL.Query()       // just query params
name := q.Get("name")
tags := q["tag"]         // repeated ?tag=a&tag=b

Middleware

Wrap a handler

Middleware is just a func(Handler) Handler. Chain by composition.

func logging(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        next.ServeHTTP(w, r)
        slog.Info("served", "method", r.Method, "path", r.URL.Path, "took", time.Since(start))
    })
}

http.ListenAndServe(":8080", logging(mux))

Testing with httptest

Spin up a real server in a test

srv := httptest.NewServer(http.HandlerFunc(myHandler))
defer srv.Close()

resp, err := http.Get(srv.URL + "/hello")

Call a handler directly without listening

rec := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/", nil)
myHandler(rec, req)

fmt.Println(rec.Code, rec.Body.String())