Untitled

Overview

On February 6, 2024, Golang version 1.22 was released. There are a number of feature updates, but the most interesting ones for me were related to HTTP server routing, so I thought I’d post about them.

Multiplexers

Implementing an HTTP server in Golang is pretty straightforward. However, in the real world, it’s hard to implement it with just the default HTTP multiplexer, and one of the main reasons is that its URL matching capabilities are too limited.

I’ve had to implement things before without using external packages for some reason, and I’ve had a hard time matching path values, etc. So the default package is mostly used for examples, and last year there was a PR like this.

https://github.com/golang/go/issues/61410

This PR adds functionality for the following two elements, and I think the Golang developers were aware of this inconvenience from the beginning, but I think they wanted to keep the standard library extensible through simplicity, which is the philosophy of the Golang language design.

  • Adding Path Value to HTTP Request
  • Added HTTP Method, Path Wildcard pattern matching.

Simple HTTP server example

 1package main
 2
 3import (
 4	"fmt"
 5	"log"
 6	"net/http"
 7)
 8
 9func main() {
10	http.HandleFunc("/", func(w http.ResponseWriter, request *http.Request) {
11		fmt.Fprintf(w, "hello world")
12	})
13
14	fmt.Println("Server is running on http://localhost:8080")
15	if err := http.ListenAndServe(":8080", nil); err != nil {
16		log.Panicf("panic listen and server : %v", err)
17	}
18}

If the request is made to the root path, it will always print hello world. If the call was made with a POST or other method, the router would have to branch and build it based on the method of the request.

Example of a complex HTTP server

 1package main
 2
 3import (
 4	"fmt"
 5	"log"
 6	"net/http"
 7	"strings"
 8)
 9
10// GET request handler that returns "Hello World"
11func helloWorldHandler(w http.ResponseWriter, r *http.Request) {
12	if r.URL.Path == "/" && r.Method == "GET" {
13		fmt.Fprintf(w, "Hello World")
14	} else {
15		http.NotFound(w, r)
16	}
17}
18
19// POST request handler that returns the data contained in the request body verbatim
20func echoPostHandler(w http.ResponseWriter, r *http.Request) {
21	if r.URL.Path == "/echo" && r.Method == "POST" {
22		fmt.Fprintf(w, "Echo")
23	} else {
24		http.NotFound(w, r)
25	}
26}
27
28// GET request handler that extracts the `id` via path analysis and returns "Received ID: [id]"
29func getIdHandler(w http.ResponseWriter, r *http.Request) {
30	if strings.HasPrefix(r.URL.Path, "/get/") && r.Method == "GET" {
31		id := strings.TrimPrefix(r.URL.Path, "/get/")
32		fmt.Fprintf(w, "Received ID: %s", id)
33	} else {
34		http.NotFound(w, r)
35	}
36}
37
38func main() {
39	http.HandleFunc("/", helloWorldHandler) // root path handler
40	http.HandleFunc("/echo", echoPostHandler) // POST request handler
41	http.HandleFunc("/get/", getIdHandler) // GET request handler with an ID
42
43	fmt.Println("Server is running on http://localhost:8080")
44	if err := http.ListenAndServe(":8080", nil); err != nil {
45		log.Panicf("panic listen and server : %v", err)
46	}
47}

The function implemented above is the server’s implementation for the HTTP request defined below.

  • GET /get/{_id}
  • POST /echo
  • GET /

As mentioned above, if you want to get more complicated, you need to handle various tasks inside the router. Creating a real API server is much more complicated than this, and you need to handle many APIs, so it is not practical to develop with the default package.

The Golang Web Framework

**Simple ECHO example

 1package main
 2
 3import (
 4	"net/http"
 5	
 6	"github.com/labstack/echo/v4"
 7)
 8
 9func main() {
10	e := echo.New()
11	e.GET("/users/:id", getUser)
12	e.Logger.Fatal(e.Start(":1323"))
13}
14
15// e.GET("/users/:id", getUser)
16func getUser(c echo.Context) error {
17  	// User ID from path `users/:id`
18  	id := c.Param("id")
19	return c.String(http.StatusOK, id)
20}

Compared to the default http package, you can easily utilize Method or Path Values in a structured form.

Example HTTP server since 1.22

 1package main
 2
 3import (
 4	"fmt"
 5	"log"
 6	"net/http"
 7)
 8
 9func main() {
10
11	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
12		fmt.Fprintf(w, "Hello World")
13	}) // Root path handler
14	http.HandleFunc("POST /echo", func(w http.ResponseWriter, r *http.Request) {
15		fmt.Fprintf(w, "Echo")
16	}) // POST request handler
17	http.HandleFunc("/get/{id}", func(w http.ResponseWriter, r *http.Request) {
18		id := r.PathValue("id")
19		fmt.Fprintf(w, "Received ID: %s", id)
20	}) // GET request handler with id
21
22	fmt.Println("Server is running on http://localhost:8080")
23	if err := http.ListenAndServe(":8080", nil); err != nil {
24		log.Panicf("panic listen and server : %v", err)
25	}
26}

The example above achieves 100% the same result as the complex HTTP server example we wrote earlier. However, you’ll notice that the code is much more concise and readable.

The first parameter of HandleFunc has been changed to accept a pattern, and we’ve made it easier to pass a value from the Request object via a function called PathValue.

Cleanup

When Generics were introduced in Golang in 1.18, there were some people who argued that it was against the philosophy of Golang, but I thought it was great that the developers were listening to the users and improving it, and I also thought it was great that Gophers were so passionate about it.

I think we’ve found a good compromise, and as we add more features here, I think we’ll lose some of the simplicity that I mentioned above. I still think I’ll use external packages in the real world, but I think it’s improved enough that I can use the standard packages in some cases, so I’m happy about this change.