Go 1.21 does not have anything major but has a lot of niceties.

Maybe not obvious, but (to me) this release has a lot to do with generics and NaNs. Apart from the cool generic maps and slices packages there are also some new built-in functions that seem to be an admission that generics can’t do all that we hoped they could.

PGO (profile guided optimization) was previewed in 1.20 but is now enabled by default.

As usual, there are lots of smaller additions and improvements to runtime, standard library, tools, and performance (see Release Notes)

Background

General

Here’s some background info on NaN (“not a number”) as it helps understand the point of the built-in functions and the new cmp package.

NaN

NaNs cause lots of problems. You may be wondering what the point of them is.

NaNs are part of the IEEE floating point specification (which is what Go uses for float32 and float64). It’s almost a ubiquitous standard for floating point numbers now. I believe all implementations of Go run on hardware with support (ie CPU instructions) for IEEE floating point numbers.

I’m not sure if this is documented but (from what I understand) some clever electrical engineers decided to invent special floating point values (NaNs) as a way to simplify error-handling. Instead of an error (like divide by zero, or square root of -1) being signalled after every operation you can do a series of operations (add, multiply, etc) and you only have to check at the end if the final value is NaN to see if something went wrong.

Like other forms of “in-band signalling” this causes problems.

Comparing a value to NaN obviously should give a false result - right? But what about comparing two NaNs to each other? It would be very confusing if that gave a true result since they are not the same value (or even values at all).

Booleans can only be true or false. There is no NaB (not a Boolean). So comparing NaNs always gives false.

As Gophers, we know that using tricks to simplify error-handling is fraught with danger. There are a lot of good things about IEEE floats but NaNs cause problems. For example, if you use a NaN as a key in map[float64]T then there is no way to delete it, which I think is a big reason for the addition of the new clear() built-in function.

C++

It’s exciting that generic sorting has now made it into the Go Standard Library.

I’ll never forget when I first got hold of the (then new) C++ STL library, 25 years ago now. The first thing I did was benchmark the sorting algorithms to compare against the traditional way - using the C standard library qsort() function.

At the time I did not appreciate how templates (C++ name for parametric polymorphism, ie generics) could provide performance and type safety.

I was hoping that the STL sort algorithm would be faster. I was astounded that STL sorting ran at least 5 times faster than the equivalent use of qsort().

There are similar improvements, in speed and type-safety, using Go’s slices.Sort().


Go


After Go 1.18 added generics, I have been eagerly awaiting each new release for the addition of generic packages to the standard library. (If you didn’t know Go has a new release every 6 months like clockwork.)

Rob Pike vetoed the idea of generic packages being added to 1.18 and said we should wait a bit. Instead, we got some “experimental” packages at https://pkg.go.dev/golang.org/x/exp. As far as I can see the standard library packages are the same as the experimental packages that we saw more than a year ago.

Generic and Sets

One thing we still need is a generic sets package, like the one Russ Cox wrote even before 1.18 (using go2go transpiler).

I always found using maps as sets was particularly ugly - and even error-prone. So a generic set package using an underlying map would be great.

In fact, I started writing a generic set package, but assumed that one would be added. So instead, I put a lot of effort into a similar, but strangely different, rangeset package.

Generics and Channels

One thing, which I have always thought would be a big reason for adding generics to Go is its use with channels. Fan-in and fan-out are the sorts of things that are written repeatedly. We need a generic channels package where they can be implemented just once.

Furthermore, I think there are additional ideas that can be explored with generic channels. I started to experiment with these using go2go a few years ago but have not had time to some back to them. Some of the ideas are used in my above-mentioned rangeset package, such as the Iterator method that returns a channel that provides all the elements of the set.

Channels (especially when combined with go-routines) provide one of the most powerful (yet simple to use) features of Go. Using generics with channels makes for an even bigger selling point for the language.

Clearing maps

When I first used maps in Go I was a bit confused about how to clear a map (remove all elements). Then I realised it’s a simple matter (most of the time) to just create a new map (using make()) and let the GC clean up the old one.

However, sometimes you do have to clear out a map, because there is a copy of it - ie. the map has been assigned or passed as a parameter that is somehow still in use. In this case you need to manually iterate the map elements and delete them all.

Note that, this way of clearing a map will not work if the key of the map is a floating point number and you have added element(s) with a key of NaN. In this case you should use the new builtin clear() function (see below).



Generics

Type inference

There have been several cool changes to type inference. (Type inference is where you don’t need to add the ugly type constraint in square brackets, because the compiler can infer the type for you.)

Apparently it’s greatly improved when using generic interfaces. TODO: example

slices package

The new slices.Sort() function is very nice. It’s now trivial to sort a slice, as long as the slice element type is orderable (satisfies constraints.Ordered). Better yet my benchmarks show it is at least 4 times faster than the old way (eg sort.Ints()).

Benchmarks were performed on amd64 using slices of different sizes, containing random ints.

You can also compare slices even if the elements are not orderable using slices.SortFunc()). This would be useful if you had a slice of structs (eg, People), that you wanted to sort on a particular field (eg Last name)

There is also a stable version - this preserves the order of elements that compare equal.

A binary search is also provided (assumes the slice is sorted, of course).

One thing that can save a lot of time is functions for all the other things that you often do with slices that you normally have to think about for a few minutes, or Google, for the best (simplest) way to do it - copying, comparing, searching, inserting, deleting, etc.

Some of these functions work the same as existing string functions. For example, slices.Compare() works the same way as strings.Compare() - so you can tell if slices are less, equal or greater.

Of course, slices.Compare() requires the element type to be orderable. If you just want to compare for equality then use slices.Equal(), whence the elements only need to be comparable.

maps package

The maps package provides the same sort of functions, but for maps, and not as many. For example, there is a maps.Equal(), but there is no maps.Compare() since maps are not ordered.

The maps.Copy() and maps.Clone() save you a bit of time writing a loop to copy elements of a map. (Remember, maps are “reference” types so whenever you use a map you don’t get a copy of all the elements.)

I am not sure why the maps package still has a maps.Clear() function, as the new clear() builtin function does the same thing and is better (see below).

cmp package

A package I have not seen before is cmp. I think this is just allow you to comparisons that handle NaNs better.

Builtin Functions

It seems to me that the new builtin functions are simply an acknowledgement that generics in Go have limitations.

There are three of them min(), max(), and clear(). These could just as easily been implemented using generics but then they would have either been slower and/or not handled NaNs.

Note that, since generics in Go doesn’t implement “specialisation”, any generic code that tries to handle NaNs would slow down code for all types not just floating point numbers.

min and max

These just return the minimum or maximum of all their parameters. The parameters, must be of the same orderable type (ie numeric or string).

I did some benchmarks which showed they are faster than any generic function to do the same thing. Plus they handle NaNs.

clear (maps)

The new clear() builtin has two quite different uses - one for maps and a quite different one for slices.

For maps, it is similar to the maps.Clear() function in the maps package (see above) but has the advantage that it will clear a map with a floating point key type (eg map[float64]T) even if map element(s) have a key with value NaN.

You can’t delete an element with a NaN key. See the background info on NaNs above for the reason. But it’s probably better to avoid adding them to a map in the first place.

clear (slices)

The built-in clear() function can also be applied to slices but in this case it does not remove any elements but sets them all to their “zero” (default) value.

If you have a large slice or array using clear() would be faster than a loop.

Important: when using clear() remember
* for a map it removes all elements (length becomes zero)
* for a slice it sets all elements to zero (length is unchanged).

Conclusion

The new generic packages are really useful.

It seems that the new built-in functions were mainly added to better handle NaNs, but I think people will use them as they are faster than any generic version you can write yourself.

PGO looks very nice, but I haven’t had a chance to try it on realistic code. I’ll try to do that and post about it soon.

I’ll also try to give the new logging package a try soon, especially looking how it plays with OpenTelemetry.

Comments