Go 1.22 brings two enhancements to the net/http package’s router: method matching and wildcards. These features let you express common routes as patterns instead of Go code. Although they are simple to explain and use, it was a challenge to come up with the right rules for selecting the winning pattern when several match a request.
Go 1.22 added new features to their net/http
package to make it a good alternative to using third-party libraries. In this article, we will look at how to handle routing using Golang's net/http
package. We will start with the basic route handling and then move on to grouping those routes.
Notes
- This assumes you're using Go version >= 1.22
- The repo for more details
Basic Routing
Let us start by looking at how to register your routes.
// main.go
package main
import (
"log"
"net/http"
)
func main() {
router := http.NewServeMux()
router.HandleFunc("GET /users/", getUsers)
router.HandleFunc("POST /users/", createUser)
router.HandleFunc("GET /users/{id}/", getUser)
router.HandleFunc("DELETE /users/{id}/", deleteUser)
err := http.ListenAndServe(":8000", router)
if err != nil {
log.Fatal(err)
}
}
// Here goes the implementation for getUsers, getUser, createUser, deleteUser
// Check the repo in services/users/routes.go
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
var users []User = []User{
{ID: 1, Name: "Bumblebee", Email: "bumblebee@autobots.com"},
{ID: 2, Name: "Optimus Prime", Email: "optimus.prime@autobots.com"},
{ID: 3, Name: "Ironhide", Email: "ironhide@autobots.com"},
{ID: 4, Name: "Hot Rod", Email: "hot.rod@autobots.com"},
}
func getUsers(w http.ResponseWriter, r *http.Request) {
response := map[string]any{
"message": "Done",
"users": users,
}
utils.WriteJSONResponse(w, http.StatusOK, response)
}
Let's go through the above code:
-
router := http.NewServeMux()
This creates a new request multiplexer. When a request is made, therouter
examines the request’s URL and selects the most appropriate handler to serve the request. -
router.HandleFunc("GET /users/", getUsers)
This registers the/users/
route and indicates that this will be aGET
method route. -
utils.WriteJSONResponse
This is a utility function to create a JSON response and can be found in the repo inutils/utils.go
Note: When making requests make sure to add the trailing slash. Doing otherwise will return a 404 not found
response
Example:
-
http://localhost:8000/users
returns 404 -
http://localhost:8000/users/
returns the correct server response
Sample Request:
- Request:
GET http://localhost:8000/users/
- Response:
// statusCode: 200
{
"message": "Done",
"users": [
{
"id": 1,
"name": "Bumblebee",
"email": "bumblebee@autobots.com"
},
{
"id": 2,
"name": "Optimus Prime",
"email": "optimus.prime@autobots.com"
},
{
"id": 3,
"name": "Ironhide",
"email": "ironhide@autobots.com"
},
{
"id": 4,
"name": "Hot Rod",
"email": "hot.rod@autobots.com"
}
]
}
Grouping Routes
As we can see from the above, this would require us to register all endpoints in the same place and can get out of hand quickly. Grouping routes helps you keep your code organized, scalable, and maintainable by putting related routes and logic together. It allows you to apply middleware selectively, encourages reusability, and improves readability, especially as your application grows.
Now let us look at how we can group routes
We will start by registering routes locally in the package where their handler functions are defined. The next step is to bring all those various routes together and start the server.
// services/users/routes.go
package user
import (
"fmt"
"net/http"
"strconv"
"<your-project-name>/gorouting/utils"
)
type Handler struct{}
func NewHandler() *Handler {
return &Handler{}
}
func (h *Handler) RegisterRoutes() *http.ServeMux {
r := http.NewServeMux()
r.HandleFunc("GET /", getUsers)
r.HandleFunc("POST /", createUser)
r.HandleFunc("GET /{id}/", getUser)
r.HandleFunc("DELETE /{id}/", deleteUser)
return r
}
// ...
Let's go through the code.
-
func NewHandler() *Handler
This creates a new Handler that is used for dependency injection such as adding access to a database should one be present. -
func (h *Handler) RegisterRoutes() *http.ServeMux
Here we create a newServeMux
and register routes.
// cmd/api/api.go
package api
import (
"log"
"net/http"
"<your-project-name>/services/user"
)
type APIServer struct {
addr string
}
func NewAPIServer(addr string) *APIServer {
return &APIServer{addr: addr}
}
func (s *APIServer) Run() error {
userHandler := user.NewHandler()
userRouter := userHandler.RegisterRoutes()
router := http.NewServeMux()
router.Handle("/users/", http.StripPrefix("/users", userRouter))
log.Println("Starting server on port", s.addr)
return http.ListenAndServe(s.addr, router)
}
Here we are going to focus on the Run
method.
-
userHandler := user.NewHandler()
This creates a new handler and this is the point where things like database connections can be passed along to the endpoints that need them. This is called dependency injection. -
userRouter := userHandler.RegisterRoutes()
This creates aServeMux
containing the user routes. -
router.Handle("/api/v1/users/", http.StripPrefix("/api/v1/users", userRouter))
This registers the users with the base URL of/users/
.StripPrefix
removes the specified prefix from the request URL before routing it touserRouter
.
// cmd/main.go
package main
import (
"log"
"<your-project-name>/cmd/api"
)
func main() {
server := api.NewAPIServer(":8000")
if err := server.Run(); err != nil {
log.Fatal(err)
}
}
“With Go 1.22, net/http is now more versatile, offering route patterns that improve clarity and efficiency. This approach to grouping routes shows how easy it is to maintain scalable code while taking advantage of Go’s built-in routing capabilities.” ChatGPT
Now that we have managed to group the user
routes. Clone the repo and try adding another service.