3 наиболее распространённых подводных камня в Go

Начиная изучение 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.

Материалы по теме:

 

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ

Библиотека программиста
23 ноября 2018

Go vs Python: изучение основ языка Go в сравнении с Python

Это не соревнование двух языков, а просто еще один способ обучения. Рассмат...
admin
19 сентября 2018

TOП-3 языка программирования, которые нужно выучить до 2019

Это не просто три лучших языка программирования, а в некотором смысле попыт...