HTTP Updates to Go's Standard Library

March 12, 2024

HTTP Updates to Go's Standard Library
HTTP Updates to Go's Standard Library

Go 1.22 added path parameters to net/http. You no longer need a third-party router for basic routing.

Estimated Reading Time : 7m

What changed

Before Go 1.22, http.ServeMux only supported fixed paths and prefix matching. If you wanted to extract an ID from /users/42, you had to parse the URL yourself or reach for a third-party router like gorilla/mux or chi.

Go 1.22 added two features to the default mux:

  1. Path parameters{name} placeholders in route patterns
  2. Method matching — prefix routes with GET, POST, etc.

Basic usage

mux := http.NewServeMux()

mux.HandleFunc("GET /users/{id}", func(w http.ResponseWriter, r *http.Request) {
    id := r.PathValue("id")
    fmt.Fprintf(w, "User ID: %s", id)
})

http.ListenAndServe(":8080", mux)

{id} captures the path segment and r.PathValue("id") retrieves it. The value is always a string — you parse it to the type you need.

Method matching

You can now prefix a pattern with an HTTP method:

mux.HandleFunc("GET /posts", listPosts)
mux.HandleFunc("POST /posts", createPost)
mux.HandleFunc("GET /posts/{id}", getPost)
mux.HandleFunc("PUT /posts/{id}", updatePost)
mux.HandleFunc("DELETE /posts/{id}", deletePost)

A POST to /posts won’t match the GET /posts handler. Before Go 1.22, you’d handle all methods in one handler and switch on r.Method.

Without a method prefix, the pattern matches all methods — the same behavior as before.

Wildcard matching

A {name...} parameter with a trailing ... matches the rest of the path:

mux.HandleFunc("GET /files/{path...}", func(w http.ResponseWriter, r *http.Request) {
    filePath := r.PathValue("path")
    // /files/images/photo.jpg → filePath = "images/photo.jpg"
    fmt.Fprintf(w, "File: %s", filePath)
})

This is useful for file servers, proxy routes, or any path with variable depth.

Precedence

When multiple patterns could match, the most specific one wins:

mux.HandleFunc("GET /posts/{id}", getPost)       // matches /posts/42
mux.HandleFunc("GET /posts/latest", getLatest)    // matches /posts/latest

/posts/latest matches the literal route, not the parameterized one. The rule is: exact segments beat wildcards.

If two patterns overlap and neither is more specific, ServeMux panics at registration time — you’ll catch it immediately, not at runtime.

A practical example

A minimal REST API:

package main

import (
    "encoding/json"
    "net/http"
    "sync"
)

type User struct {
    ID   string `json:"id"`
    Name string `json:"name"`
}

var (
    mu    sync.Mutex
    users = map[string]User{}
)

func main() {
    mux := http.NewServeMux()

    mux.HandleFunc("GET /users/{id}", func(w http.ResponseWriter, r *http.Request) {
        mu.Lock()
        user, ok := users[r.PathValue("id")]
        mu.Unlock()

        if !ok {
            http.Error(w, "not found", http.StatusNotFound)
            return
        }
        json.NewEncoder(w).Encode(user)
    })

    mux.HandleFunc("POST /users/{id}", func(w http.ResponseWriter, r *http.Request) {
        var user User
        if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
            http.Error(w, err.Error(), http.StatusBadRequest)
            return
        }
        user.ID = r.PathValue("id")

        mu.Lock()
        users[user.ID] = user
        mu.Unlock()

        w.WriteHeader(http.StatusCreated)
        json.NewEncoder(w).Encode(user)
    })

    mux.HandleFunc("DELETE /users/{id}", func(w http.ResponseWriter, r *http.Request) {
        mu.Lock()
        delete(users, r.PathValue("id"))
        mu.Unlock()

        w.WriteHeader(http.StatusNoContent)
    })

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

No dependencies, no router library, just the standard library.

Do you still need a third-party router?

For many applications, no. The new ServeMux covers the common cases: path parameters, method matching, and wildcard segments.

You’d still reach for chi or similar if you need:

  • Middleware chainingServeMux doesn’t have built-in middleware composition
  • Route groups — prefixing a set of routes with a common path
  • Regex constraints{id:[0-9]+} style parameter validation
  • Named routes — generating URLs from route names

But for straightforward APIs and web services, the standard library is now enough.