Back

Exciting Go Update - v 1.22 Change Log With Examples

Go version 1.22 is out and it has some amazing changes. In this article, we take a look at them!

by Percy Bolmér, January 29, 2024

Go Version 1.22 is finally out
Go Version 1.22 is finally out

Go made a change in my life as a developer. It is a language that resonates with me, especially the ideology of keeping things simple.

All Images in this article are made by Percy Bolmér. Gopher by Takuya Ueda, Original Go Gopher by Renée French (CC BY 3.0)

When developing in Go everything feels easy and uncomplex, and allows me to maintain a high development speed.

Not only is the language easy. It also has good development speed, and it is simple to understand. There is also the fact that Go is performant even though having a garbage collector.

And now, with the release of V1.22, we get some amazing, long-awaited fixes!

Let’s stop beating around the bush, and talk about the elephant.

If you still haven’t updated, make sure to use GoTip to update your version easily to the latest and greatest!

go install golang.org/dl/gotip@latest
gotip download
Updating to the latest Go development branch
You can watch this article as an video on YouTube

Fixing the “Broken” For loop - One of the most common bugs

One of the most common bugs in Go - Finally Burning In Hell
One of the most common bugs in Go - Finally Burning In Hell

The for loop in Go hasn’t been broken, but it is safe to say that the implementation has confused a lot of developers. This confusion has been causing MANY bugs in the history of Go.

Actually Lets Encrypt needed to revoke 3 million certificates due to this bug

Let us take a look at a simple code snippet that does not do what you might expect.

package main

import (
 "fmt"
 "time"
)

func main() {
        // A list of names to print
       names := []string{"Percy", "Gopher", "Santa"}
              
       // We will For loop over the names 
       for _, v := range names {
          // Imagine that we have some concurrent function running here
          // We are Printing the variable v
          go func() {
           fmt.Println(v)
          }()
       }

     time.Sleep(1 * time.Second)
}
Example of the FOR loop

You can try the code in Go Playground if you use versions lower than 1.22.

You would think that this would print all three names in the list, but if we inspect the code closely and think about what is happening. The variable v is being referenced in the goroutine, but each iteration of the for loop will change what v points towards.

The image shows how the Goroutines are queued, but the for loop runs before the Goroutines are triggered and overwrites the variable v.
The image shows how the Goroutines are queued, but the for loop runs before the Goroutines are triggered and overwrites the variable v.

Simply put, the reason is that the for loop queues the Goroutines in the scheduler to execute. Before the Goroutines are executed, the next iteration has proceeded and overwritten the variable v.

In Go 1.22, the for loops will instead handle variables differently and avoid overriding them each time. 

If we execute the same code in Go 1.22 instead, it will print all the names.

$: go run main.go
Santa
Percy
Gopher
Running the For loop after 1.22

To ensure compatibility between programs, the Go team solved it by making sure projects using versions before v1.21 will not attempt to compile code that uses 1.22 or forward.

This change is MAJOR, and will prevent a ton of bugs.

Better HTTP Routing In Stdlib

Path parameters will finally be added
Path parameters will finally be added

In Go, the std lib goes a long way. I feel that it is true for most cases, but the HTTP library is often replaced due to the lack of built-in ability to handle routing patterns and method declarations.

What this means is that the http.ServeMux does only accepts a regular path, without parameters and the HTTP methods that are allowed.

Let us view the differences between the old and new ways of handling these things.

Method-Sepcific Routing

Here is an example of an HTTP endpoint, that only accepts HTTP GET methods.

package main

import (
 "net/http"
)

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

 mux.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
  if r.Method != http.MethodGet {
   // If the request is not a GET request, write a 'Method Not Allowed' response
   w.WriteHeader(http.StatusMethodNotAllowed)
   w.Write([]byte(""))
   return
  }

  w.Write([]byte(`Hello`))
 })

 if err := http.ListenAndServe(":8000", mux); err != nil {
  panic(err)
 }
}
The old HTTP routing for method checking

While this is still very little code for setting up an HTTP endpoint, it is mostly boilerplate to only allow GET requests on the endpoint. 

Try sending curl requests to the endpoint to ensure it works as expected.

curl -v -X POST localhost:8000/hello
*   Trying 127.0.0.1:8000...
* Connected to localhost (127.0.0.1) port 8000 (#0)
> POST /old-hello HTTP/1.1
> Host: localhost:8000
> User-Agent: curl/7.81.0
> Accept: */*
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 405 Method Not Allowed
< Date: Sun, 28 Jan 2024 15:03:01 GMT
< Content-Length: 0
< 
* Connection #0 to host localhost left intact
An example of curling

In Go 1.22 the ServeMux is changed to allow method declarations inside the route path.

That means we can set a route as GET /hello and it will automatically only accept GET requests and return HTTP 405 otherwise.

package main

import (
 "net/http"
)

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

 mux.HandleFunc("GET /hello", func(w http.ResponseWriter, r *http.Request) {
  w.Write([]byte(`Hello`))
 })

 if err := http.ListenAndServe(":8000", mux); err != nil {
  panic(err)
 }
}
The new way of method routing

Much better and smoother. Only ONE method is accepted, you cannot do GET POST /hello. Try the CURL command from before and it should have the same behavior.

Wildcards in Patterns

The new routing rules allow us to accept parameters in the path. Path parameters are not uncommon, the fact that we do not have support for them has been a big reason why many people use Gorilla or other libraries.

A path parameter is a part or section of the URL that expects a value in the request, say that we want to allow the users to add a name to say hello to. The request would then be /hello/$NAME.

The old way to manage this would be like this.

package main

import (
 "net/http"
 "fmt"
 "strings"
)

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

 mux.HandleFunc("/hello/", func(w http.ResponseWriter, r *http.Request) {
  // Grab the Request URL
  path := r.URL.Path
  // Split it on / to grab each section
  parts := strings.Split(path, "/")
  // Only allow requests with a name actually set
  if len(parts) < 3 {
   http.Error(w, "Invalid request", http.StatusBadRequest)
   return
  }
  // Grab the name
  name := parts[2] 

  w.Write([]byte(fmt.Sprintf("Hello %s!", name)))
 })

 if err := http.ListenAndServe(":8000", mux); err != nil {
  panic(err)
 }
}
Old way of grabbing a Path variable

As you can see, that is pretty messy and this is only one variable.

The new ServeMux allows us to specify parameters with a name by wrapping them in {}. So we can add a parameter for the name by having the route be /hello/{name}.

To be able to grab the parameter, the HTTP request contains a function called PathValue. This function accepts the name of the parameter to fetch.

This is the new way of using path variables.

package main

import (
 "net/http"
 "fmt"
)

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

 mux.HandleFunc("GET /hello/{name}", func(w http.ResponseWriter, r *http.Request) {
  name := r.PathValue("name")
  w.Write([]byte(fmt.Sprintf("Hello %s!", name)))
 })

 if err := http.ListenAndServe(":8000", mux); err != nil {
  panic(err)
 }
}
New way of grabbing Path variables

We don’t need to verify that the name is present, if you try to send a request without the parameter you will see that it returns HTTP 404 Not found.

Try curling localhost:8080/hello/percy and it should print a nice message.

You can also try curling localhost:8080/hello/percy/123 and see that it fails. There can be use cases where you need to allow dynamic paths to be set and still use parameters. To solve that you can end {} with .... Three dots will make the pattern match any segments after the parameter.

mux.HandleFunc("GET /hello/{name...}", func(w http.ResponseWriter, r *http.Request) {
  name := r.PathValue("name")
  w.Write([]byte(fmt.Sprintf("Hello %s!", name)))
 })
Example of dynamic paths with path variable

This will allow you to pass a dynamic request with more segments to the parameter.

Matching Exact Patterns with Trailing Slashes

This one I didn’t know, but the HTTP mux matches any route that has the correct prefix. That means if the route is hello/ then any requests to paths that start with hello/ will match. The key here is the / in the end of the path, which has always told ServeMux to match any prefixes afterward.

package main

import (
 "net/http"
)

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

 mux.HandleFunc("GET /hello/", func(w http.ResponseWriter, r *http.Request) {
  w.Write([]byte(`Hello`))
 })

 if err := http.ListenAndServe(":8000", mux); err != nil {
  panic(err)
 }
}
Trailing slash example

Running that will allow us to test it easily. Send a CURL to localhost:8080/hello/percy will return the same thing as localhost:8080/hello.

What if we only want to allow EXACT matches? Well, we can now do that using {$} at the end of the route. This will tell the Servemux to only route exact matches. It has to be at the end of the path.

mux.HandleFunc("GET /hello/{$}", func(w http.ResponseWriter, r *http.Request) {
  w.Write([]byte(`Hello`))
 })
An example of exact match

After updating the route and restarting the program, we can now only send requests to localhost:8080/hello/.

Conflict Resolution and Precedence

With these new rules being allowed, we now have a new issue. A request can match TWO routes. 

This is solved by having the MOST SPECIFIC route always being selected.

http.HandleFunc("/hello", helloHandler)          // Less specific
http.HandleFunc("/hello/{name}", helloHandler)  // More specific
Priority by specification detail

I like this approach because I feel that it makes very much sense.

Honorable Mentions

More things are being changed, but I don’t think they are worth their separate chapters. Instead here is a very short summary of the changes.

  • The first V2 package in Go - math/rand/v2. Rand is getting a V2, with the removal of the Read method, and faster algorithms throughout the package. A new rand.N function that accepts generic parameters to randomly get values, works with durations as well.
  • Slog gets a SetLogLoggerLevel to control the log level more easily.
  • slices gets a new Concat function that merges slices. Any function in the slice package that shrinks sizes such as Delete will now automatically Zero the elements.

Conclusion

These are the changes I think have been worth mentioning in the newest release of Go.

I do hope you are as thrilled as I am regarding the changes to the HTTP routing. I can’t wait to test out building a new HTMX app and using the STD Lib router for this.

And finally, that nasty devil of for behavior that has caused so many bugs will be forever gone.

If you haven’t already make sure you update to the newest version!

What change are you most excited about?

What is your most missed feature in Go?

Feel free to let me know on any of my social media handles.

Appendix

Go Benchmark - https://benchmarksgame-team.pages.debian.net/benchmarksgame/fastest/go.html

Garbage Collector - https://en.wikipedia.org/wiki/Garbage_collection_(computer_science)

Go change log - https://tip.golang.org/doc/go1.22

Lets encrypt revoke - https://community.letsencrypt.org/t/revoking-certain-certificates-on-march-4/114864

Go playground - https://go.dev/play/p/LkgkmFMoqTS

GoTip - https://pkg.go.dev/golang.org/dl/gotip

If you enjoyed my writing, please support future articles by buying me an Coffee

Sign up for my Awesome newsletter