Встроенные функции в Go 1.21

Go 1.21 собрал множество приятных штук, от оптимизации по профилю (profile-guided optimization) до пакетов стандартной библиотеки для работы со срезами и картами (см. подробности в заметках к релизу). Я же их хладнокровно проигнорирую и сосредоточусь на фичах, которые привлекли мое внимание: новых встроенных функциях (builtins).

Builtins — это функции, которые не требуют импорта пакета, вроде len или make. В Go 1.21 добавили три новых: min, max and clear. Давайте разберем их.

Тренды Go
У нас тут тренд намечается.

min/max

Делают ровно то, что вы от них ожидаете — выбирают наименьшее или наибольшее значение из переданных:

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

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

Обе функции принимают значения упорядоченных типов (ordered type): целые числа, вещественные числа или строки (а также производные от них):

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

Обе функции принимают один или более аргументов:

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

Но при этом не являются вариационными (variadic):

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

Если беспокоитесь, что новые встроенные функции сломают существующий код, в котором встречаются названия min и max — не стоит. Встроенные функции — не ключевые слова, вы спокойно можете их перекрыть:

// так можно
max := "My name is Max"
min := 4 - 1
make := func() int {
    return 14
}
fmt.Println(max, min, make())
// My name is Max 3 14

А вот любопытный вопрос:

Зачем «замусоривать» общее пространство имен и делать встроенные min и max вместо одноименных дженерик-функций в пакете cmp?

Ответ есть, но не факт, что вам он понравится. Говорит Расс Кокс (Russ Cox):

Мы несколько раз обсуждали, должны ли функции min/max быть встроенными или находиться в пакете cmp.

Есть веские аргументы в пользу обоих позиций. С одной стороны, min и max — базовые арифметические операции, как сложение. Это оправдывает их существование в виде встроенных функций.

С другой стороны, у нас теперь есть дженерики, и стоило бы использовать именно их, раз с их помощью можно решить задачу.

Мнения разошлись даже среди авторов языка.

полная цитата

Так что уж как смогли, так и сделали.

clear

min и max кажутся достаточно очевидными. С clear интереснее. Функция работает со срезами, картами и значениями параметрических типов (type parameter values — о них чуть позже).

Из карты clear удаляет все элементы, оставляя карту пустой:

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

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

А вот у среза только зануляет отдельные элементы, не меняя длину:

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

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

С чего бы это, спросите вы. Разве не логично для функции с названием clear было бы очищать срез? Вообще-то нет.

Срез в Go — это значение, и длина среза — часть этого значения:

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

Так что функция, которая принимает срез, работает с копией этого значения. Изменять копию нет смысла — вызывающая сторона не увидит изменений. Именно поэтому append возвращает новый срез вместо того, чтобы менять длину исходного.

clear не исключение: она не может изменить длину среза. Но может изменить значения элементов массива, который находится под срезом. Именно это она и делает.

Карта же, с другой стороны — это указатель на структуру вида:

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

Поэтому вполне логично, что clear удаляет элементы из карты.

Теперь насчет «значений параметрических типов»:

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 принимает аргумент container, который может быть либо срезом, либо картой. clear внутри функции обрабатывает container в соответствии с фактическим типом: карты очищает, у срезов зануляет элементы.

Да, и еще. clear не работает с массивами:

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

Итого

Получить аж три новых встроенных функции в одном релизе несколько неожиданно. Но может и неплохо.

Вот полный список встроенных функций начиная с Go 1.21:

append     добавляет значения в срез
clear      удаляет или зануляет элементы контейнера

close      закрывает канал

complex    создают и разбирают комплексные числа
real
imag

delete     удаляет элемент карты по ключу

len        возвращает длину контейнера
cap        возвращает вместимость контейнера

make       создает новый срез, карту или канал
new        выделяет память под переменную

min        выбирает минимальный из переданных аргументов
max        выбирает максимальный из переданных аргументов

panic      создают и обрабатывают панику
recover

print      печатают аргументы
println

Возможно, не стоит добавлять в него новые 🤔 По правде говоря, я до сих пор не могу смириться с «принтами».

Подписывайтесь на канал, чтобы не пропустить новые заметки 🚀