Vanilla GO Path Parameters

Golang introduced easier path parameter based routing as a vanilla language feature with the 1.22 release, which is incredibly handy if you don't have a lot of routes or if a generic http framework does not make sense for your use case.

To create a parameterised router in Go all you need is the standard library and we'll have a look at a minimalist example below.

Path Parameters for Routing

Many webservices adhere to RESTful principles where you categorise your routes into resources and let your clients specify which resource to GET, update (PUT, PATCH), create (POST) or DELETE via the URL.

So if I want to see my signed up users I would issue a GET request to /users and if I want to see a specific user I would do the same with /users/1, the 1 being the relevant path parameter.

ServeMux in Golang

To start routing within your GO app, let's create a route and handler for the /hello-world route:

package main

import (
	"net/http"
)

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

	mux.HandleFunc("/hello-world", helloHandler)

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

func helloHandler(w http.ResponseWriter, r *http.Request) {
	w.Write([]byte("Hello World!"))
}

This will, when run via go run main.go start a server you can access at localhost:3666/hello-world and you should see the text Hello World.

So far so good, we have a web server and now we want to add a route for the users with a dynamic path parameter. Next we need to add a new route that specifies the {id} as a

	mux.HandleFunc("/hello-world", helloHandler)
+	mux.HandleFunc("/users/{id}", userHandler)

Next we're going to add the handler function for the users, which will check if a user exists in our database or not.

func userHandler(w http.ResponseWriter, r *http.Request) {
	userId := r.PathValue("id")
	fmt.Println("got userId:", userId)
	knownUserIds := []string{"1", "2", "3"}
	if slices.Contains(knownUserIds, userId) {
		w.Write([]byte(fmt.Sprintf("user with id %s has been found!", userId)))
		return
	}
	w.WriteHeader(http.StatusNotFound)
	w.Write([]byte(fmt.Sprintf("user with id %s has not been found!", userId)))
}

You can test these routes by accessing localhost:3666/users/1 and localhost:3666/users/4 which should give you different output based on if a user is present in our knownUserIds slice.

To retrieve the path parameter value we use userId := r.PathValue("id"), which needs to specify the same string that we defined in the route ({id}) omitting the curly braces.

Note that the data type is a string and that you might need to type-cast it if you're trying to compare it to numbers.

Next we're faking a database by creating a slice of strings with fake user IDs and check if the requested user ID is among the test values.

knownUserIds := []string{"1", "2", "3"}
if slices.Contains(knownUserIds, userId) {
	w.Write([]byte(fmt.Sprintf("user with id %s has been found!", userId)))
	return
}

This also makes sure the response is written back to the client.

For all user IDs that can NOT be found, you can use WriteHeader to assign the correct HTTP status code of 404:

w.WriteHeader(http.StatusNotFound)

before responding with the relevant text response.

Please note that error handling has largely been omitted for brevity! Web serve responsibly in the wild 😉.

Tagged with: #go #golang

Thank you for reading! If you have any comments, additions or questions, please tweet or toot them at me!