Три частые ошибки, возникающие при разработке на Go

Это перевод статьи. Раньше желания переводить не возникало, но статья очень понравилась, наверное, из-за того, что сам наступал уже на подобные грабли. Перевод достаточно вольный, но суть передает верно. Если хоть одному начинающему разработчику поможет эта статья, труды мои будут не напрасными :).

Оригинал статьи доступен по адресу 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 — отличный язык, но чтобы создавать действительно классно работающие приложения, стоит понимать что происходит у него “под капотом”.