Go is a programming language that stands out for its simplicity and outstanding concurrency model. Like any other tool, staying on top of its changes aids the ability to be competitive and aware of the opportunities available while using the tool. In this post, we’ll dive into Go’s 1.22 prominent changes for you to reflect on the opportunities that these bring to your Go applications.

What’s in store?

Go 1.22 was launched on February 6th and since this language is near our Core at Wawandco, we read through them and saw a few things that they tweaked. We grouped these changes in multiple sections as follows:

  • Changes in the Language
  • Enhanced Packages
  • Tool Settings
  • New Packages

Without further ado, let’s jump right into it!

Changes in the Language

These refer to the changes that syntactically speaking were adjusted for Go. Starting off we have:

Loops: Iterator Variable

The iterator variable in Go allows you to access a copy of the current value when using for loops with Range. In this update, Go made an adjustment that changes the scope of this variable to allay an issue associated with the scope of such variable. Let’s take for instance the following piece of code:

myValues := []string{"a","b","c"}
for _, v := range myValues {
    go func() {
        fmt.Println(v)
    }()
}

Now, in that case you may have been looking to run an asynchronous operation on each element of the slice, hoping that the outcome of that code would be something like so:

c
a
b

But instead, you got this:

c
c
c

In this other example without asynchronous operations, we will have the same result:

myValues := []string{"a","b","c"}

actions := []func(){}

for _, v := range myValues {
    actions = append(actions, func() {
        fmt.Println(v)
    })
}

for _, fn := range actions {
    fn()
}

So, What happens in those situations? Let’s take a look at the following before and after to have a closer look at what was previously happening and what they changed it to:

closer look diagram

Previously, Go created the iterator variable prior to all iterations. In each iteration, the value for this one was being set. Now instead, they reduced the scope of the iterator variable and now it’s available for each separate iteration. So our problem in this case wasn’t exactly the Goroutine but, this iterator variable which was holding the value of the slice’s last element to then be processed.

Loops: Range with Integers

We now have the ability to iterate using Range specifying a fixed number of times! You may have come across the need of doing this in the past, but it was only available using the following form:

for i := 0; i < 5; i++ {
    fmt.Println(i)
}

Now, you can do the following and achieve the same goal:

for i := range 5 {
    fmt.Println(i)
}

This serves as an alternative, but it reads better than the only one that was available in previous versions of Go. Now that we’ve explored the ones available for the language itself, let’s see what packages were altered in this version.

Enhanced Packages

net/http

When looking to map routes in a Web server, we’ve opted for using specific frameworks/packages to handle this, such as Gorilla Mux, Gin or even Fiber. That’s because what was available in net/http server mux was rigid for us to achieve a few things while using it.

In this version, this package received two significant improvements:

  • You can set HTTP methods when declaring the pattern for a handler.
  • Wildcards are now accepted as part of the pattern for a handler.

Let’s take a closer look at those.

HTTP Methods

Previously, declaring a handleFunc looked somewhat like this:

HandleFunc("/path", handler)

So, there we would be telling the Mux to map the /path route and assign handler to it. Inside that handler, is where we would validate the kind of HTTP method was being used for the incoming request, and then based on the HTTP methods of our interests we would have the handler do different things based on the one given. This felt odd, because it shouldn’t be the handler’s responsibility to validate which method was being used.

Thankfully, we now have the power to do the following:

HandleFunc("GET /path", doGet)

Meaning, that we can now define the request’s method for the pattern that we’re mapping. It’s a lot more explicit in this manner and handlers don’t have to worry about the kind of method being used by the client reaching out the server.

Enhanced Routing Patterns

In addition to the previous change, we can now specify wildcards inside patterns, like so:

HandleFunc("GET /path/{id}", doGet)
HandleFunc("GET /path/{id...}", doSomething)
HandleFunc("GET /path/{$}", doSomethingElse)

We’re now enabled to have routes that are a bit more dynamic. We’re no longer relying on the query parameters that we’re passing at the end of the URL, but rather we can now leverage what we define as our route to gather the values we want.

And, to read the values passed through the path we can do the following:

func handler(w http.ResponseWriter, r *http.Request) {
    valueAsString := r.PathValue("id")
}

Simple and straightforward, right?

Word of Caution ⚠️

Mapping a route to a path that can be repeated will result in a build-time panic. Take the following example for instance:

HandleFunc("/user/{action}", handler)
HandleFunc("/{foo}/create", handler)

Now, let’s take a look at the novelties in regards to the tool and command settings

Tool & Command Settings

go get

The command, go get won’t work for modules outside our GOPATH if we have the GO111MODULE environment variable off.

go mod init

This one helps initialize a module and it will no longer include configurations coming from external modules' configuration files such as Gopkg.lock.

go build (with external linker)

go build will call a linker and if this one is external and the CGO flag is disabled, it will panic. If it’s available the building process will continue. A linker is responsible for taking the code for the different files of the module and brings them together to be further compiled.

go work vendor

We now have the ability to generate a folder containing all the dependencies of our workspace using the go work vendor command. Go workspace mode was added in version 1.18 and it lets you work on multiple modules simultaneously without having to edit the go.mod files for each module.

go test -cover

This one is rather a visual change on the output of this command. Previously, if you ran this command looking for how much coverage you had per package, you got something like this:

$ go test -cover
?    example [no test files]

Now, in that same scenario this is the outcome you will see:

$ go test -cover
    example     coverage: 0.0% of statements

go vet

This is a cool tool that Go has, which examines our source code and reports suspicious constructs in it. Let’s take the following code snippet as an example:

func main() {
    t := time.Now()
    defer fmt.Println(time.Since(t))
}

Now, here what’s odd is that the computation of time.Since is the first thing that’ll happen, and then the very last thing it’ll do it’s printing that computated value, which means that this function isn’t exactly being deferred. So, if we run go vet, we would get the following:

$ go vet
./main.go:10:20: call to time.Since is not deferred

Pretty cool, right? And to address the aforementioned situation we simply can wrap our print statement within an anonymous function in the defer statement like so:

func main() {
    t := time.Now()
    defer func () {
        fmt.Println(time.Since(t))
    }()
}

Last but not least, let’s look at what this update brought for the standard library.

New Packages

math/rand/v2

This is the first v2 version of a Go standard library. This one brings some enhancements and changes.

The rand.Read() method won’t be in version 2 of this package. Go encourages to use the Read function from crypto/rand package or the Uint64 method from the math/rand package.

In addition to that, there were a few integer methods whose names changed by other names that can be better recognized. Functions such as Int31n which return an int32 are now called Int32N.

Furthermore, this version introduced a new generic function N for random value generation. With this function, now we can generate random values for any integer type without the need for explicit type conversions. If we want to generate a random duration ranging from 0 up to 3 minutes, we can just use rand.N(3 * time.Minute) and stop dealing with type conversions.

Also, internally, there was a significant change made. Their pseudorandom number generator LFSR was changed for two modern and efficient ones called Cha Cha 8 and PCG.

go/version

This one should help us validate and compare Go versions. Small, simple, yet effective.

These were only a portion of the changes contained in this version, for more details we encourage you to take a look at Go 1.22 Release notes.

Closing Remarks

Go appeared almost 14 years ago and the team behind it has been listening to the community using the language and providing tools to empower us developers. One of the greatest treats we have received from Go is how simple it is to work with it and how fast it has allowed us to deliver web applications to the world. These changes on version 1.22 are sure great and we’re looking forward to see the things that we get to create with these upgrades.