Это перевод статьи. Раньше желания переводить не возникало, но статья очень понравилась, наверное, из-за того, что сам наступал уже на подобные грабли. Перевод достаточно вольный, но суть передает верно. Если хоть одному начинающему разработчику поможет эта статья, труды мои будут не напрасными :).
Оригинал статьи доступен по адресу http://bryce.is/writing/code/jekyll/update/2015/11/01/3-go-gotchas.html.
1. Итерация через range
Использовать range
очень удобно. Перебирая срез или отображение, для каждого элемента мы получаем две переменные, в первой индекс элемента, во второй копия значения.
Например:
for index, value := range mySlice {
fmt.Println("index: " + index)
fmt.Println("value: " + value)
}
Но что же здесь происходит на самом деле? Рассмотрим более подробный пример:
type Foo struct {
bar string
}
func main() {
list := []Foo{
{"A"},
{"B"},
{"C"},
}
list2 := make([]*Foo, len(list))
for i, value := range list {
list2[i] = &value
}
fmt.Println(list[0], list[1], list[2])
fmt.Println(list2[0], list2[1], list2[2])
}
В данном примере происходит следующее:
- Создается срез структур
Foo
под названиемlist
; - Определяется срез указателей на структуры
Foo
, под названиемlist2
с длиной равной длине массиваlist
; - Перебираем все структуры в срезе
list
и присваиваем указатель на структуру соответствующему элементу срезаlist2
(имеющему тот же индекс);
Вероятнее всего, мы ожидаем следующий результат работы вышеприведенного кода:
{A} {B} {C}
&{A} &{B} &{C}
А на самом деле, получаем:
{A} {B} {C}
&{C} &{C} &{C}
Первая строка именно такая как ожидалась, а вот со второй что-то не так. Как будто бы распечатался указатель на третий элемент среза list2
три раза. Почему же такое произошло?
Всему виной этот самый удобный range
.
for i, value := range list {
list2[i] = &value
}
Go передает копий значений элементов вместо самих значений, когда производится итерация с помощью range
. И когда мы создаем указатель на value
, на самом деле, мы создаем указатель на копию значения. При каждой итерации, копия нового значения помещается в ту же ячейку памяти, что и предыдущее, таким образом, все указатели в срезе list2
приводят к одной области памяти.
Правильно решить поставленную задачу можно так:
var value Foo
for var i := 0; i < len(list); i++ {
value = list[i]
list2[i] = &value
}
Или, все-таки, с помощью range
, но взяв из него только индекс элемента:
for i := range list {
list2[i] = &list[i]
}
2. Использование встроенной функций append
Срезы — один из примитивных типов в Go. То что в других языках делается с массивами, в гоу можно сделать только со срезом.
Например, нам нужно добавить значение в срез элементов типа int
.
list := []int{0,1,2}
list = append(list, 3)
fmt.Println(list) // [0 1 2 3]
На первый взгляд, все похоже на метод push()
для массивов в других языках, но срезы — это не совсем массивы и функция append
иногда может удивить.
Рассмотрим другой пример:
func main() {
a := []byte("foo")
b := append(a, []byte("bar")...)
c := append(a, []byte("baz")...)
fmt.Println(string(a), string(b), string(c))
}
Здесь мы определили срез байт и сразу же инициировали его, преобразовав в срез строку “foo”. Далее мы добавляем срез [“bar”] к первому срезу, присваиваем полученное переменной b
, после чего добавляем другой срез [“baz”] также к первому срезу и присваиваем полученное переменной c
. Результатом работы данной программы будет следующее:
foo foobaz foobaz
Какого? Мы-то ждали foo foobar foobaz
!
Чтобы понять что произошло, следует разобраться с тем, а что же за звери такие эти срезы? Срез в Go — это дексриптор, который содержит три компонента:
- Указатель на базовый массив элементов. К этому массиву мы не получим прямого доступа;
- Емкость базового массива;
- Длину среза.
Так что же произошло на самом деле? Go переиспользует базовый массив в функции append
если он может сделать это не изменяя его емкости. Все три переменные в примере указывают на одну и ту же области памяти. Единственная разница, что срез a
имеет длину 3, а срезы b
и c
длину равную 6.
Следует всегда помнить, что Go будет использовать тот же базовый массив, если длина нового среза будет меньшей или равной емкости изначального среза.
3. Затенение переменных
Не уверен в переводе термина (Variable shadowing), но примерно как-то так.
В Go есть один интересный оператор, который выглядит вот так: :=
. Из-за него некоторые говорят, что Go очень похож на Pascal, хотя в Go оператор выполняет несколько другую работу. :=
— это короткая форма объявления переменной с одновременным присвоением значения и типа, соответствующего переданному значению. Очень удобная форма записи, но, к сожалению, именно она и таит в себе возможность совершить ошибку.
Рассмотрим следующий код:
func main() {
list := []string{"a", "b", "c"}
for {
list, err := repeat(list)
if err != nil {
panic(err)
}
fmt.Println(list)
break
}
fmt.Println(list)
}
func repeat(list []string) ([]string, error) {
if len(list) == 0 {
return nil, errors.New("Nothing to repeat!")
}
list = append(list, list...)
return list, nil
}
Автор считает, что это очень показательный пример. Ну что же, поверим, к тому же, он вполне себе хорошо иллюстрирует проблему. Происходит следующее:
- Создается срез строк с именем
list
; - Запускается бесконечный цикл;
- Вызываем функцию
repeat()
, которая возвращает новый срез и ошибку; - Прерываем цикл;
- Печатаем
list
.
Глядя на код, ожидается следующий вывод:
[a b c a b c]
[a b c a b c]
Но на деле получаем:
[a b c a b c]
[a b c]
А все потому, что когда использовали оператор :=
, мы на самом деле определили новую переменную list
в области видимости цикла. В Go фигурные скобки создают новую область видимости. Мы это сделали для того, чтобы быстренько объявить переменную err
типа error
.
Чтобы исправить ситуацию, нужно объявить err
заранее и далее использовать обычный оператор присваивания =
.
var err error
list, err = duplicate(list)
В поставке Go есть отличная утилита go vet
, запустив которую с опцией -shadow
, можно проверить код на наличие потенциальных ошибок связанных с затенением переменных.
Go — отличный язык, но чтобы создавать действительно классно работающие приложения, стоит понимать что происходит у него “под капотом”.