Go 1.22's HTTP Package Updates

10 February 2024
Cover image

Recently, I found myself among the many affected by the latest round of layoffs, a situation that has kept me occupied with job hunting, interviews, and adapting to these new circumstances. Amid this whirlwind of activity, I nearly missed an update I've been eagerly anticipating: the release of Go version 1.22. This post is a quick dive into the features of this new release that excite me the most.

Whenever I come across discussions online or get asked about which package to use for creating an HTTP server in Go, my answer remains consistent. It really depends on the complexity of the HTTP service you're planning to build, but I always suggest starting with net/http from the standard package. The Go language boasts an impressive standard library, and net/http is a standout example of its strength. With the 1.22 release, it's gotten even better.

The needs

As I've mentioned before, I'm a big advocate for using net/http for simple services. However, I know that as your project grows and you start juggling multiple endpoints—each with its own set of parameters and needing to handle various HTTP methods—things can get a bit tangled. Your once neat HTTP handler begins to resemble a cluttered room, filled with repetitive code and becoming a bit harder to navigate.

func handlerUser(w http.ResponseWriter, r *http.Request) {
	userID := getPathValue(r)
	...
    switch r.Method {
    case "GET":
        // Handle GET request
    case "POST":
        // Handle POST request
    case "PUT":
	    // Handle PUT request
    default:
        // Handle other HTTP methods or respond with an error
        http.Error(w, "Unsupported HTTP method", http.StatusMethodNotAllowed)
    }
}

Now when managing various HTTP methods for a single endpoint, you might find yourself longing for the simplicity of frameworks like Chi or Echo. These packages allow you to assign specific handlers for each HTTP method, streamlining your code. However, sticking to the standard net/http package means your handler needs to accommodate all allowed methods itself, which can get a bit cumbersome as we can see above.

Moreover, dealing with path parameters introduces another layer of complexity. Without the built-in support found in more sophisticated frameworks, you're left to employ workarounds. Techniques like using http.StripPrefix or manually trimming path prefixes with strings.TrimPrefix become necessary. It's a bit of a makeshift approach, requiring some extra code and effort to pull off what's relatively straightforward with specialized routing libraries.

The changes

While handling multiple HTTP methods and extracting path parameters could feel like a bit of a chore, Go 1.22 has turned the tables. The latest update introduces enhanced routing capabilities to the ServeMux handler, making these tasks much less of a headache.

Now, you can specify HTTP methods directly when registering a handler, such as POST or GET. Even more impressive is the introduction of wildcards in URL patterns. This means you can define a pattern like /orders/{id} to match specific segments of a URL path, a feature that previously would have required a third-party router or some custom parsing logic.

Retrieving the actual value from these path segments is straightforward, too. Just use the new Request.PathValue method. This addition significantly simplifies the code needed for dynamic routing, making the development of more complex web applications smoother and more intuitive.

...
mux.HandleFunc("GET /orders", func(w http.ResponseWriter, r *http.Request) {
	w.Write([]byte("Get all orders"))
})

mux.HandleFunc("POST /orders", func(w http.ResponseWriter, r *http.Request) {
	w.Write([]byte("Create an order"))
})

mux.HandleFunc("GET /orders/{id}", func(w http.ResponseWriter, r *http.Request) {
	id := r.PathValue("id")
	fmt.Fprintf(w, "Get order with id: %s", id)
})

mux.HandleFunc("PUT /orders/{id}", func(w http.ResponseWriter, r *http.Request) {
	id := r.PathValue("id")
	fmt.Fprintf(w, "Update order with id: %s", id)
})
...

Testing our endpoints just got easier.

$ curl localhost:8080/orders
Get all orders

$ curl -X POST localhost:8080/orders
Create an order

$ curl -X PUT localhost:8080/orders/2
Update order with id: 2

However, not every method is allowed for each endpoint. If you try using a method that's not supported, like DELETE in this example:

$ curl -X DELETE localhost:8080/orders/2
Method Not Allowed

You'll get a clear response: Method Not Allowed. This response helps you quickly understand that the action you're trying to perform isn't permitted for that endpoint.

Also, the new wildcard feature, marked by {$}, is a game-changer for matching URL patterns right down to the trailing slash. In the past, a route like "/" acted as a catch-all, snagging any path that began with "/" and didn't have another route defined. So, both "/" and "/this/does/not/exist" would unintentionally end up at the same place.

Here's a simple workaround some developers use to manage both the home page and unrecognized paths gracefully:

mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
    if req.URL.Path != "/" {
        http.NotFound(w, req)
        return
    }
    fmt.Fprintf(w, "Welcome to the home page!")
})

With the new {$} feature, exact matching is now straightforward. Here’s how to use it:

mux.HandleFunc("/{$}", func(w http.ResponseWriter, r *http.Request) {
	w.Write([]byte("Welcome to the home page!"))
})

To conclude

I'm aware that these examples might seem quite basic, but they're meant to provide a quick snapshot and spark ideas on why these updates are fantastic and how they can lead to improved implementations.

Thank you for reading along. This blog is a part of my learning journey and your feedback is highly valued. There's more to explore and share, so stay tuned for upcoming posts. Your insights and experiences are welcome as we learn and grow together in this domain. Happy coding!

Share article