Первоначально опубликовано Bryce Neal на bryce.is 1 ноября 2015 г. и включено в Информационный бюллетень GoLang, выпуск 83 (с правильным выделением синтаксиса).

В последнее время больше занимаюсь языком программирования Go. Сегодня я подумал, что расскажу о трех ловушках, которые застали меня врасплох или иным образом дали результаты, которых я не ожидал от работы с Go.

1. Предложение диапазона

Предложение диапазона очень удобно. Он позволяет вам перебирать фрагмент или карту, давая вам ссылку на индекс и значение каждого элемента. Например:

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])
}

В этом примере мы делаем несколько вещей.

  1. Мы создаем часть структур Foo под названием list.
  2. Мы определяем второй фрагмент указателей на структуры Foo, называемый list2.
  3. Мы перебираем каждую структуру в list, чтобы назначить ее указатель на соответствующий индекс в list2.

Поэтому вы можете ожидать, что вывод приведенного выше кода будет следующим:

{A} {B} {C}
&{A} &{B} &{C}

Однако этого не происходит. Давайте посмотрим на вывод этого кода:

{A} {B} {C}
&{C} &{C} &{C}

Первая строка такая, как ожидалось. Это структуры, которые мы изначально создали в list, но вторая строка оказалась неожиданной. Похоже, мы трижды выводим указатель на последнюю структуру в списке. Но почему это происходит?

Причина - предложение диапазона.

for i, value := range list {
    list2[i] = &value
}

Вот проблема: Go использует копию значения вместо самого значения в предложении диапазона. Итак, когда мы берем указатель на value , мы фактически берем указатель на копию значения. Эта копия повторно используется во всем предложении диапазона, в результате чего наш фрагмент list2 заполнен тремя ссылками на один и тот же указатель (указатель копии).

Согласно справочному руководству Go: «Значения итерации присваиваются соответствующим переменным итерации, как в операторе присваивания». Таким образом, вы можете представить приведенное выше предложение диапазона как то же самое, что и запись:

var value Foo
for var i := 0; i < len(list); i++ {
    value = list[i]
    list2[i] = &value
}

Чтобы получить ожидаемый результат, мы должны использовать индекс для получения указателя на фактическое значение, а не на копию.

for i, _ := range list {
    list2[i] = &list[i]
}

2. Встроенная функция добавления

Срезы - это примитивные типы в Go. На других языках вы можете использовать массив, а в Go - срез. Вот пример того, как мы можем добавить значение в конец целочисленного фрагмента.

list := []int{0,1,2}
list = append(list, 3)
fmt.Println(list) // [0 1 2 3]

На первый взгляд это похоже на метод массива push (), но срезы - это не совсем массивы, и встроенная функция добавления удивила меня своим внутренним поведением. Взгляните на пример ниже:

func main() {
    a := []byte("foo")
    b := append(a, []byte("bar")...)
    c := append(a, []byte("baz")...)
        
    fmt.Println(string(a), string(b), string(c))
}

В этом примере мы определяем срез байтов a с начальным значением [«foo»]. Затем мы добавляем еще один фрагмент [«bar»] к нашему начальному фрагменту a, и снова мы добавляем еще один фрагмент [«baz»] к нашему начальный срез a. Результатом приведенного выше фрагмента является:

foofoobazfoobaz

Что? :) Разве это не должно быть foo foobar foobaz?

Чтобы понять, что здесь происходит, мы должны понять, что на самом деле представляет собой срез. Срез - это дескриптор, состоящий из трех компонентов:

  1. Указатель на базовый массив, то есть массив, выделенный Go, к которому у вас нет прямого доступа.
  2. Емкость указанного базового массива.
  3. Эффективная длина среза.

Так что же происходит на самом деле? Go будет повторно использовать тот же базовый массив в append (), если он может сделать это без изменения размера базового массива.

Таким образом, все три структуры ссылаются на один и тот же массив в памяти. Единственное практическое различие заключается в их длине, которая в случае a равна 3, а в случае b и c равно 6.

Имейте в виду, что Go будет повторно использовать один и тот же базовый массив только в том случае, если длина вновь созданного фрагмента меньше или равна емкости исходного фрагмента.

При использовании некоторых встроенных функций среза важно понимать, что происходит под капотом. Для получения дополнительной информации Роб Пайк написал очень полезную запись в блоге, в которой содержится множество полезных деталей, касающихся срезов.

3. Переменное затенение

Когда люди впервые смотрят на код Go в первый раз, они в первую очередь спрашивают: «Для чего нужен этот оператор: =?». Как вы знаете, это сокращенный оператор объявления переменной. Он используется как для объявления, так и для установки значения переменной. Тип объявлен неявно. Это очень удобно, но может привести к некоторым проблемам, если вы не будете осторожны. В частности, я говорю о переменном затенении. Это в особенности меня укусило, потому что последние два года я программировал почти исключительно на es5 javascript, где нет области видимости на уровне блоков.

Проверь это:

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
}

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

  1. Создаем срез строк, список.
  2. Входим в цикл for.
  3. Цикл for вызывает функцию repeat (), которая возвращает новый фрагмент и ошибку.
  4. Мы выходим из цикла for и печатаем значение списка.

Вы можете ожидать, что вывод здесь будет:

[a b c a b c]
[a b c a b c]

но на самом деле это:

[a b c a b c]
[a b c]

Поскольку мы используем сокращенный оператор объявления переменных, мы фактически повторно объявляем переменную list внутри области действия цикла for. Это неудобно, потому что вторая переменная err - новая переменная, которую мы хотим объявить. Мы можем исправить это, изменив первые несколько строк в блоке for на:

var err error
list, err = duplicate(list)

Один из способов выявить эти проблемы на раннем этапе - воспользоваться инструментом иди к проверке. У него есть опция -shadow, которая может помочь обнаружить проблемы с затенением переменных.

В блоге есть хорошая запись The Golang Beartrap, в которой более подробно рассказывается о переменном затенении.

Резюме

Go - отличный язык, и как только вы поймете его причуды, работать с ним станет настоящим удовольствием. Я действительно ценю фантастический инструментарий и удобочитаемость языка. Удачи и счастливого суслика! :)

Опять же, изначально опубликовано на bryce.is 1 ноября 2015 г.