Built-in functions in Go 1.21

Go 1.21 brings many exciting things, from profile-guided optimization to standard library packages for structured logging, slices and maps operations (see the release notes for details). In this post, I'd like to skip all that and focus on the features that caught my eye: the new builtins.

In case you're wondering, builtins are functions that do not require importing a package, like len or make. Go 1.21 brings three more of them: min, max and clear. Let's take a look.

Go trends
There are some serious trends we're seeing here.

min/max

These work exactly as you'd expect: they select the smallest or largest of the values you specify:

n := min(10, 3, 22)
fmt.Println(n)
// 3

m := max(10, 3, 22)
fmt.Println(m)
// 22

Both functions accept values of an ordered type: integers, floats, or strings (or their derived types).

x := min(9.99, 3.14, 5.27)
fmt.Println(x)
// 3.14

s := min("one", "two", "three")
fmt.Println(s)
// "one"

type ID int

id1 := ID(7)
id2 := ID(42)
id := max(id1, id2)
fmt.Println(id)
// 42

Both functions take one or more arguments:

fmt.Println(min(10))
// 10
fmt.Println(min(10, 9))
// 9
fmt.Println(min(10, 9, 8))
// 8
// ...

However, they are not variadic:

nums := []int{10, 9, 8}
n := min(nums...)
fmt.Println(n)
// invalid operation: invalid use of ... with built-in min

If you're wondering whether these new builtins will break your existing code that already uses the names min and max — they won't. Builtins aren't keywords, you can shadow them however you like:

// all are just fine
max := "My name is Max"
min := 4 - 1
make := func() int {
    return 14
}
fmt.Println(max, min, make())
// My name is Max 3 14

Now here is an interesting question:

Why would the Go team add new builtins instead of generic Min and Max functions in the cmp package?

There is an answer, though you may not like it. Russ Cox here:

We have gone back and forth a few times on this proposal about min/max being builtins vs being in package cmp.

There are good arguments on both sides. On the one hand, min and max are fundamental arithmetic operations much like addition, which justifies making them builtins.

On the other hand, we have generics now and it would make sense to use generics to write library code rather than make them builtins.

Even among the active Go language designers, our own personal intuitions differ on this.

full quote

So it is what it is, I guess.

clear

While min and max look obvious, clear is a more interesting beast. It works with maps, slices, and type parameter values (more on the latter in a minute).

When called on a map, clear deletes all entries, resulting in an empty map:

m := map[string]int{"one": 1, "two": 2, "three": 3}
clear(m)

fmt.Printf("%#v\n", m)
// map[string]int{}

But when called on a slice, clear sets all slice elements to their zero values:

s := []string{"one", "two", "three"}
clear(s)

fmt.Printf("%#v\n", s)
// []string{"", "", ""}

And why would that be, you might ask. Wouldn't it be logical for clear to, well, clear a slice? Actually, no.

A slice in Go is a value, and it's length is a part of that value:

// https://github.com/golang/go/blob/master/src/runtime/slice.go
type slice struct {
	array unsafe.Pointer
	len   int
	cap   int
}

So any function that accepts a slice works with a copy of that value. There is no point in modifying the copy, as the caller will not see the changes. That's why the append builtin returns a new slice instead of changing the length of the original one.

clear is no different. It can't change the length. But what it can do is change the underlying array elements by setting them to zero value. And that's what it does.

Map, on the other hand, is a pointer to the following struct:

// https://github.com/golang/go/blob/master/src/runtime/map.go
type hmap struct {
	count int
	// ...
	buckets unsafe.Pointer
	// ...
}

So it's perfectly reasonable for clear to remove all elements from a map.

Now to the "type parameter values" part:

func customClear [T []string | map[string]int] (container T) {
    clear(container)
}

func main() {
    s := []string{"one", "two", "three"}
    customClear(s)
    fmt.Printf("%#v\n", s)
    // []string{"", "", ""}

    m := map[string]int{"one": 1, "two": 2, "three": 3}
    customClear(m)
    fmt.Printf("%#v\n", m)
    // map[string]int{}
}

customClear takes a container argument, which can be either a slice or a map. clear inside the function handles the container accordingly, clearing the maps and zeroing-out the slices.

Oh, and by the way, clear does not work with arrays:

arr := [3]int{1, 2, 3}
clear(arr)
// invalid argument: argument must be (or constrained by) map or slice

Summary

All in all, getting three new builtins in a single release is a bit unexpected. But probably fine.

Here is a complete list of built-in functions in Go as of 1.21:

append     appends zero or more values to a slice
clear      deletes or zeroes out all elements

close      records that no more values will be sent on the channel

complex    assemble and disassemble complex numbers
real
imag

delete     removes the element with key k from a map

len        returns the length of a container
cap        returns the capacity of a container

make       creates a new slice, map, or channel
new        allocates storage for a variable

min        computes the smallest value among its arguments
max        computes the largest value among its arguments

panic      assist in reporting and handling run-time panics
recover

print      print the arguments
println

Maybe it's a good idea not to add any more to this list. I still can't get over prints, to be honest.

──

Interactive examples in this post are powered by codapi — an open source tool I'm building. Use it to embed live code snippets into your product docs, online course or blog.

 Subscribe to keep up with new posts.