Начиная изучение 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.
Материалы по теме:
Комментарии