Встроенные функции в Go 1.21
Go 1.21 собрал множество приятных штук, от оптимизации по профилю (profile-guided optimization) до пакетов стандартной библиотеки для работы со срезами и картами (см. подробности в заметках к релизу). Я же их хладнокровно проигнорирую и сосредоточусь на фичах, которые привлекли мое внимание: новых встроенных функциях (builtins).
Builtins — это функции, которые не требуют импорта пакета, вроде len
или make
. В Go 1.21 добавили три новых: min
, max
and clear
. Давайте разберем их.
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
Возможно, не стоит добавлять в него новые 🤔 По правде говоря, я до сих пор не могу смириться с «принтами».
★ Подписывайтесь на новые заметки.