In the first part of What’s New in Go 1.20, we looked at language changes. For part two, I would like to introduce three changes to the standard library that address problems that the community has been thinking about and debating solutions to for years.
First of all, a whole new package has been added. But you can’t import it by default, and you probably shouldn’t be using it at all. It’s the new experimental arena package.
The arena package was proposed by Dan Scales and has been added to the Go standard library in 1.20. But if you just try to add import "arena"
to a program, you get the following, somewhat cryptic error message:
imports arena: build constraints exclude all Go files in GOROOT/src/arena
To opt into using arenas, you need to set GOEXPERIMENT=arenas
when calling the go tool, like GOEXPERIMENT=arenas go build .
.
So what are arenas and why is the Go team trying so hard to keep you from using them? I asked ChatGPT, and this is what it said (this is the equivalent of quoting Webster’s Dictionary for the 21st century):
Memory arenas are a memory management technique used in some programming languages and libraries to allocate and deallocate large blocks of memory efficiently. They are typically used in situations where the program needs to frequently allocate and deallocate a large number of small objects. By allocating and deallocating memory in large blocks, rather than individually for each object, memory arenas can reduce the overhead associated with memory management and improve performance.
If you want to go more in-depth, Uptrace has a nice guide to the arena package (presumably written by a human, but who knows nowadays), but I’ll try to just give a basic overview here.
As you probably know, Go is a garbage collected language. This means that when you refer to a variable, the compiler and the runtime automatically keep track of the uses of that variable to see when it comes into use and when it is no longer being used. Once a variable is no longer used, it is “garbage” waiting to be collected.
For many kinds of applications, garbage comes in waves. For example, if you have a web server, it may allocate a lot of memory in order to build up a response to some user request, but once it responds, it no longer needs any of the memory that it allocated, so it can all be returned to the system at once. Another example is a game might want to free all of the objects created for a level once the level is over. The arena package lets Gophers opt into this approach to memory management in performance critical code. Instead of having the garbage collector start a root and then travel down to “mark and sweep” the live memory and return the dead objects, the whole arena can be marked as dead all at once. The release notes for Go 1.20 claim that
When used appropriately, [using package arena] has the potential to improve CPU performance by up to 15% in memory-allocation-heavy applications.
This is highly efficient, but also highly dangerous. What if the programmer makes a mistake, and for example, adds some strings to a logger call that outlives the request? The log might be overwritten by a subsequent request and the string become replaced with junk data, leading to crashes or worse—security exploits.
To mitigate the risk of these kinds of bugs, the arena package will deliberately cause a panic if can detect someone reusing memory after it has been freed. Dan Scales explains,
- Each arena A uses a distinct range in the 64-bit virtual address space
- A.Free unmaps the virtual address range for arena A
- The physical pages for the arena can then be reused by the operating system for other arenas.
- If a pointer to an object in arena A still exists and is dereferenced, it will get a memory access fault, which will cause the Go program to terminate. Because the implementation knows the address ranges of arenas, it can give an arena-specific error message during the termination.
There is a similar comment in the Go runtime package that implements memory arenas:
// What makes the arenas here safe is that once they are freed, accessing the
// arena's memory will cause an explicit program fault, and the arena's address
// space will not be reused until no more pointers into it are found. There's one
// exception to this: if an arena allocated memory that isn't exhausted, it's placed
// back into a pool for reuse. This means that a crash is not always guaranteed.
So, it is still possible to write buggy code with arenas, but hopefully, the bugs will translate into simple crashes rather than full blown memory corruption or security exploits.
The arena package has a fairly simple API. Here’s some example code from arenas_test.go
:
a := arena.NewArena()
defer a.Free()
tt := arena.New[T1](a)
tt.n = 1
ts := arena.MakeSlice[T1](a, 99, 100)
// …
There is also an arena.Clone
function for when you want to move an object out of an arena and onto the regular Go memory heap.
With luck, the arena experiment will succeed, and we will see it introduced as a regular package in a future version of Go.
While most Go programmers probably will never need to use the arena package directly, I suspect virtually all Go programmers will have some occasion to use a different new feature in Go 1.20: multierrors.
The concept of multierrors in Go is not new. Hashicorp’s go-multierror package goes all the way back to 2014 and there was at least one proposal to add multierrors to the standard library by 2017.
Multierrors also exist in other languages. Python added exception groups to Python 3.11, for example. In the case of Python, while there was a popular third party MultiError class, it ultimately needed to be added to the language for full operability:
Changes to the language are required in order to extend support for exception groups in the style of existing exception handling mechanisms. At the very least we would like to be able to catch an exception group only if it contains an exception of a type that we choose to handle. Exceptions of other types in the same group need to be automatically reraised, otherwise it is too easy for user code to inadvertently swallow exceptions that it is not handling.
Unlike Python, in Go, errors are just values, so it was easy enough to create your own multierror type and expose it using errors.As. Indeed, I wrote my own multierror package that worked this same way. This was clearly an idea that was being created and recreated by the community, so did it really need to be solved at the level of the standard library?
Suppose I have some code like this:
a := errors.New("a")
b := errors