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.
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
andMax
functions in thecmp
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.
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 print
s, 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.