Coming Soon: Golang 1.21 🚀

GOLANG
8 min read

At DoltHub, we love Go! We're building DoltDB, an open-source SQL database engine with Git-like distributed version control, all implemented in Golang. (Actually... astute blog readers will remember that we have a little bit of C code in there, too, but that's a story for another blog post.) We publish a new article in our Golang blog series every three weeks. In today's post, we're taking a look at what's coming in Go 1.21.

Go 1.21 Release Candidate

It's that time of year again... the Golang release train is coming to town! 🙌🏻 Go 1.21 is scheduled to be released in August and the 1.21 Release Candidate has a lot of improvements that we're excited to start using. We took some time to play with the 1.21 release candidate and explored its new features and changes. This blog post covers what we think are the most important changes to know about.

Go 1.21 is on its way!

The Go team makes trying out release candidates a breeze. At the time of writing this post, the latest release candidate is Go 1.21rc2, so all I had to do to get it installed was:

go install golang.org/dl/go1.21rc2@latest
~/go/bin/go1.21rc2 download
export GOROOT=/Users/jason/sdk/go1.21rc2/

Then just configure your tools and your go.mod files to use 1.21rc2 and you're ready to play!

Fixing Loop Variable Capture

Perhaps the most infamous sharp edge with writing Golang code, has been the reuse of loop variables that are then captured by closures. Most Golang developers I've met have hit this sharp edge at some point and learned about it the hard way. Go isn't the only language to handle loop variables this way, but it isn't the most intuitive behavior and it has surprised a lot of developers over the years. For example, the code below attempts to iterate over a slice of strings and print out each element in a goroutine:

func main() {
	myStuff := []string{"one", "two", "three"}
	wg := sync.WaitGroup{}
	for _, v := range myStuff {
		wg.Add(1)
		go func() {
			fmt.Println(v)
			wg.Done()
		}()
	}

	wg.Wait()
}

On the surface, it sure looks like this should print out each element from our myStuff slice, but when we run it, we get a surprise:

three
three
three

You may see different results depending on how your goroutines get scheduled, but it's incredibly unlikely you'd ever see "one", "two", "three" printed out, like you might think from naively reading the code. The closure for the goroutine in the loop body captures the v variable, but that same variable instance is reused for all passes through the loop, so when our goroutines DO run, they have all captured the same memory location and will all read the same value from that same chunk of memory. In practice, you're most likely to see the last value in the slice printed out since the loop will run quickly and then the goroutines will be scheduled and every one will read from the same memory location.

This sharp edge is painfully well known and in fact, was one of the earliest entries documented in the Golang FAQ! Despite being quite well-known, it's still too easy of a trap to fall into and it can result in some nasty bugs in production. One of the more notable production bugs attributed to this sharp edge was Let's Encrypt's Certificate Authority Authorization (CAA) Rechecking bug, which resulted in the potential to bypass domain authorization checks in some cases when a user shouldn't have been able to issue a certificate.

Go 1.21 doesn't completely remove this sharp edge yet... but, it does take a BIG first step – it introduces a preview of a new behavior that fixes the loop variable capture issue by re-allocating the loop variable at each pass through the loop so that every pass gets a unique memory location that can be safely captured by a closure. You can opt-in to this behavior in 1.21 very easily – just make sure your project's go.mod file and your GOROOT env var are configured to use Go 1.21 and make sure the GOEXPERIMENT env var is set to loopvar in whatever environment runs go build for your project.

After enabling the loopvar experiment, the code above now prints out each element in the slice! Note that you may see a different order, since the order depends on how the goroutines get scheduled, but you're now guaranteed to see each element printed out once.

three
one
two

Major kudos to the Go team for taking this first big step towards tackling a LONG-standing sharp edge in the language! 👏 There are many examples online, going back 10 years even, of people getting bitten by this. Some people might complain that it took too long to get this fixed, but haters gunna hate, and I think "Better Late Than Never" is a much better perspective. I, for one, am super excited to not have to ever explain this sharp edge to new Go developers again!

Better late than never!

Fresh, New APIs!

The Go 1.21 release candidate brings in several small, but helpful, new APIs that are worth learning.

New Built-ins: max, min, and clear

Three new built-in functions have been added as part of the 1.21 release. The new min and max functions make it really easy to find minimum and maximum values for values of ordered types. I actually needed a simple min function just the other day. It was easy to write my own of course, but having these small built-ins available is a welcome change!

Note that min and max only support a fixed number of arguments – meaning, Go needs to know the number of arguments at compile time, so unfortunately you can't expand a slice into multiple arguments using the ellipsis operator and pass it into min or max. (However, as we'll see in the next section... the new slices package provides generically typed utility functions that can help us out here!)

Here's a quick example of using the new min built-in. Note that all arguments must be of the same type, otherwise you'll get a compiler error:

myMin := min(100.0, 3.14159, 42.0, -1.0)
fmt.Printf("myMin: %v \n", myMin)

As you (hopefully) intuited, running that code produces:

myMin: -1 

The third new built-in function is clear, which allows you to easily clear out a map or a slice. For map arguments, you'll have an empty map after calling clear on it. Slices are a little different though... after calling clear on a slice, the slice will be the same length, but every element in the slice will be initialized to the zero value of the slice's element type. Here's a concrete example just to make that crystal clear:

mySlice := []bool{true, true, true}
fmt.Printf("mySlice (before): %v \n", mySlice)
clear(mySlice)
fmt.Printf("mySlice (after): %v \n", mySlice)

myMap := map[string]bool{"one": true, "two": true, "three": true}
fmt.Printf("myMap (before): %v \n", myMap)
clear(myMap)
fmt.Printf("myMap (after): %v \n", myMap)

Running the code above produces:

mySlice (before): [true true true] 
mySlice (after): [false false false] 
myMap (before): map[one:true three:true two:true] 
myMap (after): map[] 

Standard Library: Slices, Maps, and Cmp, Oh My!

I'm very stoked about the new additions to Go's Standard Library, particularly the new slices and maps packages that utilize generic parameters to provide type-safety. Some of these APIs have been kicking around since generics were first introduced in Go 1.18, but they were only available in Go's golang.org/x/exp package. We've been excited to use some of these handy utilities, but we weren't willing to pull in the experimental package to our product yet. They are now officially part of Go's standard library and ready for production use! You're definitely going to want to learn these new utility functions so you can start using them in your projects.

The slices package in particular has a LOT of useful functions for working with slices in a type-safe manner: BinarySearch, Clip, Clone, Compact, Compare, Contains, Delete, Equal, Max, Min, Reverse, and Sort. Many of these functions also provide *Func variants that allow you to pass in a custom comparison function. The Go 1.21rc2 release notes also mentions that these are "generally faster than the sort package", although I haven't tested that out myself yet.

Let's take a look at how we can use the new slices.Contains function:

supportedAlgorithms := []string{"AES", "Blowfish", "DES", "DSA", "DiffieHellman", "OAEP", "DESede"}
if !slices.Contains(supportedAlgorithms, requestedAlgorithm) {
    return fmt.Errorf("unsupported algorithm: %s", requestedAlgorithm)
}

The code above is simple and short. Compare that to what we have to do without the slices.Contains function:

supportedAlgorithms := []string{"AES", "Blowfish", "DES", "DSA", "DiffieHellman", "OAEP", "DESede"}
found := false
for _, algo := range supportedAlgorithms {
    if algo == requestedAlgorithm {
        found = true
        break
    }
}
if !found {
    return fmt.Errorf("unsupported algorithm: %s", requestedAlgorithm)
}

The code above is still pretty easy to read, but at eleven lines compared to four, the slices.Contains version is clearly the winner.

The maps package provides fewer functions, but still some really useful ones: Clone, Copy, Equal, Keys, and Values. maps.Keys is probably the one I'm most excited to use. It's often handy to get a slice of all the keys in a map, and while rangeing over the map and collecting the keys is easy enough, it's nice to finally have a utility function in the standard library that does that for us.

Let's take a look at using the maps.Copy function. This is another case where using this new function can reduce a lot of tedious boilerplate code:

sourceMap := map[string]bool{"Fry": true, "Leela": true, "Zoidberg": false}
myNewMap := map[string]bool{"Amy": true, "Fry": false}
fmt.Printf("myNewMap (before): %v \n", myNewMap)
maps.Copy(myNewMap, sourceMap)
fmt.Printf("myNewMap (after): %v \n", myNewMap)

That code prints out the results below. Note that any keys that already exist in the destination map are overwritten with the values from the source map.

myNewMap (before): map[Amy:true Fry:false] 
myNewMap (after): map[Amy:true Fry:true Leela:true Zoidberg:false] 

Finally, the cmp package provides new Compare and Less functions that let you easily compare values of any cmp.Ordered type.

Standard Library: Structured Logging with log/slog

In addition to logging a human-readable message to your logs, structured logging enables you to log key value pairs that can be easily parsed by programs reading your logs. Popular third-party logging libraries like Logrus, Zap, Zerolog, and hclog have supported structured logging for a long time, but the Go Standard Library's logging library has noticeably lacked support for structured logging... until now!

The new log/slog package provides a logging API that allows you to pass in a set of key value pairs in addition to a human-readable log message. For example, after importing log/slog, you can log structured data like this:

slog.Info("finished processing orders", "orderCount", orderCount, "processingTime", totalTime, "skippedOrders", skipOrderCount)

This is a nice upgrade for the standard library, and over time more developers may start choosing to use Go's built-in logging APIs for new projects, but I don't think anyone is expecting a significant number of developers to migrate away from the extremely popular third-party logging libraries that are already so ingrained in so many Go applications.

Conclusion

Go 1.21 doesn't bring anything as nearly as big and exciting as the major generics feature we got in 1.18 last year, but it does bring a solid collection of small enhancements and improvements that are going to improve the Go developer experience. I'm particularly excited about not having to explain the tribal knowledge about loop variable capture, as well as some of the handy generic utility functions for working with slices and maps. 1.21 also includes some other nice changes that we didn't cover here, such as a defined, deterministic order for executing module init() functions, and improvements to type inference for generics. Dig into the Go team's official release candidate announcement post if you want to go deeper.

This article is part of our technical Golang blog series. We publish a new article in the series every three weeks, so if you liked this content, subscribe to the DoltHub weekly newsletter to keep up with what we're up to and get notified about Golang blog posts. You can also swing by the DoltHub Discord and let us know what you think. Our whole team hangs out there every day and we love discussing Golang, databases, and data versioning! 🤓 Come by and join us to chat or let us know if we missed something you're excited about in Go 1.21!

SHARE

JOIN THE DATA EVOLUTION

Get started with Dolt

Or join our mailing list to get product updates.