Начиная изучение Go, многие сталкиваются с совершенно не очевидными моментами в этом языке. Рассмотрим три таких подводных камня в Go.
1. Range
Начнем программирование на Go с основ. Функция range
является одной из самых используемых в Go. Вот пример использования (не обращайте внимания, что мы присваиваем всем животным в зоопарке 999 ног):
type Animal struct { name string legs int } func main() { zoo := []Animal{ Animal{ "Dog", 4 }, Animal{ "Chicken", 2 }, Animal{ "Snail", 0 }, } fmt.Printf("-> Before update %v\n", zoo) for _, animal := range zoo { // ? Oppps! `animal` is a copy of an element ? animal.legs = 999 } fmt.Printf("\n-> After update %v\n", zoo) }
Вышеприведённый код выглядит довольно невинно. Однако вы можете удивиться, узнав, что два fmt.Printf()
выражения дают одинаковые результаты.
-> Before update [{Dog 4} {Chicken 2} {Snail 0}] -> After update ??? [{Dog 4} {Chicken 2} {Snail 0}]
Подводный камень
Значения (хранятся как animal
), по которым мы проходимся с помощью range
, являются не указателями на значения, а копиями значений из zoo
.
Решение
Чтобы изменить элемент массива, мы должны изменить этот элемент через указатель:
for idx, _ := range zoo { zoo[idx].legs = 999 }
Возможно, это выглядит довольно тривиально, но вы можете быть удивлены, обнаружив, что это один из самых распространенных источников ошибок.
2. "…" и вариативные функции
Быть может, вы использовали “…” в ЯП С для создания вариативной функции; вариативная функция – это функция, принимающая переменное количество аргументов.
В C вы должны последовательно вызвать макрос va_arg
для доступа к необязательным аргументам. И, если вы попытаетесь использовать вариативный аргумент любым другим способом, компилятор выдаст ошибку.
int add_em_up (int count,...) { ... va_start (ap, count); /* Initialize the argument list */ for (i = 0; i < count; i++) sum += va_arg(ap, int); /* Get the next argument value */ va_end (ap); /* Clean up */ return sum }
Программирование на Go задает несколько иные правила. В Golang это выглядит так же, как и в С, но работает по-другому. Здесь представлена вариативная функция myFprint
. Обратите внимание, как используется вариативный аргумент a
:
func myFprint(format string, a ...interface{}) { if len(a) == 0 { fmt.Printf(format) } else { // ⚠ `a` should be `a...` fmt.Printf(format, a) // ✅ fmt.Printf(format, a...) } } func main() { myFprint("%s : line %d\n", "file.txt", 49) }
[file.txt %!s(int=49)] : line %!d(MISSING) file.txt : line 49
Можно подумать, что компилятор выдал бы ошибку при неправильном использовании вариативных параметров. Но обратите внимание, как fmt.Sprintf
без нареканий использовал первый аргумент в a
.
Подводный камень
В Go вариативные параметры разделяются компилятором.
Это означает, что вариативный аргумент a
– на самом деле отдельная переменная. Поэтому приведённый ниже код действителен:
// `a` is just a slice! for _, elem := range a { fmt.Println(elem) }
3. Слайсинг
Продолжим изучение Go слайсингом. Если вы делали слайсинг в Python, то наверняка помните, что этот приём даёт вам новый список со ссылками на элементы. Это свойство позволяет использовать такой Python код:
a = [1, 2, 3] b = a[:2] # ? a completely new list! b[0] = 999 >>> a [1, 2, 3] >>> b [999, 2]
Если вы попробуете то же самое в Go, получите что-то другое:
func main() { data := []int{1,2,3} slice := data[:2] slice[0] = 999 fmt.Println(data) fmt.Println(slice) }
[999 2 3] [999 2]
Подводный камень
В Go слайс имеет тот же базовый массив и ёмкость, что и оригинал. В Golang если вы измените элемент в слайсе, исходное содержимое тоже будет изменено.
Решение
Если вы хотите получить независимый слайс, у вас есть два варианта:
// Option #1 // appending elements to a nil slice // `...` changes slice to arguments for the variadic function `append` a := append([]int{}, data[:2]...) // Option #1 // Create slice with length of 2 // copy(dest, src) a := make([]int, 2) copy(a, data[:2])
И, согласно StackOverflow, append намного быстрее make.
Материалы по теме:
Комментарии