Type go что за функция

50 оттенков Go: ловушки, подводные камни и распространённые ошибки новичков

Type go что за функция. 64fbc18a37fa4dd884eeb8200dd5c8dd. Type go что за функция фото. Type go что за функция-64fbc18a37fa4dd884eeb8200dd5c8dd. картинка Type go что за функция. картинка 64fbc18a37fa4dd884eeb8200dd5c8dd

Go — простой и забавный язык. Но в нём, как и в любых других языках, есть свои подводные камни. И во многих из них сам Go не виноват. Одни — это естественное следствие прихода программистов из других языков, другие возникают из-за ложных представлений и нехватки подробностей. Если вы найдёте время и почитаете официальные спецификации, вики, почтовые рассылки, публикации в блогах и исходный код, то многие из подводных камней станут для вас очевидны. Но далеко не каждый так начинает, и это нормально. Если вы новичок в Go, статья поможет сэкономить немало часов, которые вы бы потратили на отладку кода. Мы будем рассматривать версии Go 1.5 и ниже.

Содержание

Уровень: абсолютный новичок

Уровень: более опытный новичок

Уровень: продвинутый новичок

1. Открывающую фигурную скобку нельзя размещать в отдельной строке

В большинстве других языков, использующих фигурные скобки, вам нужно выбирать, где их размещать. Go выбивается из правила. За это вы можете благодарить автоматическую вставку точки с запятой (точка с запятой предполагается в конце каждой строки, без анализа следующей). Да, в Go есть точка с запятой!

2. Неиспользуемые переменные

Если у вас есть неиспользуемые переменные, то код не скомпилируется. Исключение: переменные, которые объявляются внутри функций. Это правило не касается глобальных переменных. Также можно иметь неиспользуемые аргументы функций.

Если вы присвоили неиспользуемой переменной новое значение, то ваш код всё равно не будет компилироваться. Придётся её где-то использовать, чтобы угодить компилятору.

Другое решение: комментировать или удалять неиспользуемые переменные.

3. Неиспользуемые импорты

Если вы импортируете пакет и потом не используете какие-либо из его функций, интерфейсов, структур или переменных, то код не скомпилируется. Если нужно импортировать пакет, идентификатор «_» в качестве его имени поможет избежать ошибок компилирования. Идентификатор «_» чаще всего применяется для использования сайд-эффектов импортированных библиотек.

Другое решение: удалить или закомментировать неиспользуемые импорты. В этом поможет инструмент goimports.

4. Короткие объявления переменных можно использовать только внутри функций

5. Переобъявления переменных с помощью коротких объявлений

В одной области видимости выражения нельзя переобъявлять переменные, но это можно делать в объявлении нескольких переменных (multi-variable declarations), среди которых хотя бы одна — новая. Переобъявляемые переменные должны располагаться в том же блоке, иначе получится скрытая переменная (shadowed variable).
Неправильно:

6. Нельзя использовать короткие объявления переменных для настройки значений полей

Хотя разработчикам Go уже предлагали это исправить, не стоит надеяться на перемены: Робу Пайку нравится всё «как есть». Вам помогут временные переменные. Или предварительно объявляйте все свои переменные и используйте стандартный оператор присваивания.

7. Случайное сокрытие переменных

Синтаксис короткого объявления переменных так удобен (особенно для тех, кто пришёл в Go из динамических языков), что его легко принять за регулярную операцию присваивания. Если вы сделаете эту ошибку в новом блоке кода, то компилятор не выдаст ошибку, но приложение будет работать некорректно.

8. Нельзя использовать nil для инициализации переменной без явного указания типа

Идентификатор nil можно использовать как «нулевое значение» (zero value) для интерфейсов, функций, указателей, хеш-таблиц (map), слайсов (slices) и каналов. Если не задать тип переменной, то компилятор не сможет завершить работу, потому что не сумеет угадать тип.

9. Использование nil-слайсов (slice) и хеш-таблиц (map)

10. Ёмкость хеш-таблиц

11. Строки не могут быть nil

12. Передача массивов в функции

Если вы разрабатываете на С/С++, то массивы для вас — указатели. Когда вы передаёте массивы функциям, функции ссылаются на ту же область памяти и поэтому могут обновлять исходные данные. В Go массивы являются значениями, так что, когда мы передаём их функциям, те получают копию исходного массива. Это может стать проблемой, если вы пытаетесь обновлять данные в массиве.

Если нужно обновить исходные данные в массиве, используйте типы указателей массива.

Другое решение: слайсы. Хотя ваша функция получает копию переменной слайса, та всё ещё является ссылкой на исходные данные.

13. Неожиданные значения в выражениях range в слайсах и массивах

Это может произойти, если вы привыкли к выражениям for-in или foreach в других языках. Но в Go выражение range отличается тем, что оно генерирует два значения: первое — это индекс элемента (item index), а второе — данные элемента (item data).

14. Одномерность слайсов и массивов

Кажется, что Go поддерживает многомерные массивы и слайсы? Нет, это не так. Хотя можно создавать массивы из массивов и слайсы из слайсов. С точки зрения производительности и сложности — далеко не идеальное решение для приложений, которые выполняют числовые вычисления и основаны на динамических многомерных массивах.

Можно создавать динамические многомерные массивы с помощью обычных одномерных массивов, слайсов из «независимых» слайсов, а также слайсов из слайсов «с совместно используемыми данными».

Если вы используете обычные одномерные массивы, то при их росте вы отвечаете за индексирование, проверку границ и перераспределение памяти.

Процесс создания динамического многомерного массива с помощью слайсов из «независимых» слайсов состоит из двух шагов. Сначала нужно создать внешний слайс, а затем разместить в памяти все внутренние слайсы. Внутренние слайсы не зависят друг от друга. Их можно увеличивать и уменьшать, не затрагивая другие.

Создание динамического многомерного массива с помощью слайсов из слайсов «с совместно используемыми данными» состоит из трёх шагов. Сначала нужно создать слайс, выполняющий роль «контейнера» данных, он содержит исходные данные (raw data). Затем — внешний слайс. В конце мы инициализируем каждый из внутренних слайсов, перенарезая слайс с исходными данными.

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

15. Обращение к несуществующим ключам в map

16. Неизменяемость строк

Если вы попытаетесь обновить отдельные символы строковой переменной с помощь оператора индекса, то это не сработает. Строки — это байт-слайсы (byte slices), доступные только для чтения. Если вам все-таки нужно обновить строку, то стоит использовать байт-слайс и преобразовывать его в строку по необходимости.

Стоит заметить, что это неправильный способ обновления символов в текстовой строке, потому что символ может состоять из нескольких байтов. В этом случае лучше конвертировать строку в слайс из «рун» (rune). Но даже внутри слайсов из «рун» одиночный символ может быть разбит на несколько рун, например если есть символ апострофа (grave accent). Такая непростая и запутанная природа «символов» является причиной того, что в Go строковые значения представляют собой последовательностей байтов.

17. Преобразование строк в байт-слайсы (Byte Slices), и наоборот

Когда вы преобразуете строку в байт-слайс (и наоборот), вы получаете полную копию исходных данных. Это не операция приведения (cast operation), как в других языках, и не перенарезка (reslicing), когда переменная нового слайса указывает на один и тот же массив, занятый исходным байт-слайсом.

18. Строки и оператор индекса

Оператор индекса, применяемый к строке, возвращает байтовое значение (byte value), а не символ (как в других языках).

19. Строки — не всегда текст в кодировке UTF-8

Строковые значения необязательно должны быть представлены в виде текста в кодировке UTF-8. Здесь возможен произвольный набор байтов. Единственный случай, когда строки должны быть в кодировке UTF-8, — когда они используются как строковые литералы. Но даже они могут включать в себя данные с экранированными последовательностями.

Чтобы узнать кодировку строки, используйте функцию ValidString() из пакета unicode/utf8.

20. Длина строк

Допустим, вы разрабатываете на Python и у вас есть такой код:

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

Встроенная функция len() возвращает не символ, а количество байт, как это происходит с Unicode-строками в Python.

Чтобы получить такой же результат в Go, используйте функцию RuneCountInString() из пакета unicode/utf8.

Технически функция RuneCountInString() не возвращает количество символов, потому что один символ может занимать несколько рун.

21. Отсутствующая запятая в многострочных литералах slice/array/map

Вы не получите ошибку компилирования, если оставите замыкающую запятую при объявлении в одну строчку.

22. log.Fatal и log.Panic не только журналируют

23. Несинхронизированные операции встроенных структур данных

Некоторые возможности Go нативно поддерживают многозадачность (concurrency), но в их число не входят потокобезопасные коллекции (concurrency safe). Вы сами отвечаете за атомарность обновления коллекций. Для реализации атомарных операций рекомендуется использовать горутины и каналы, но можно задействовать и пакет sync, если это целесообразно для вашего приложения.

24. Итерационные значения для строк в выражениях range

Значение индекса (первое значение, возвращаемое операцией range ) — это индекс первого байта текущего «символа» (кодовая точка/руна Unicode), возвращённый во втором значении. Это не индекс текущего «символа», как в других языках. Обратите внимание, что настоящий символ может быть представлен несколькими рунами. Если вам нужно работать именно с символами, то стоит использовать пакет norm (golang.org/x/text/unicode/norm).

Выражения for range со строковыми переменными пытаются интерпретировать данные как текст в кодировке UTF-8. Если они не распознают какую-либо последовательность байтов, то возвращают руны 0xfffd (символы замены Unicode), а не реальные данные. Если в вашей строке хранятся произвольные данные (не UTF-8), то для сохранения преобразуйте их в байт-слайсы.

25. Итерирование хеш-таблиц (map) с помощью выражения for range

На этот подводный камень натыкаются те, кто ожидают, что элементы будут располагаться в определённом порядке (например, отсортированные по значению ключа). Каждая итерация хеш-таблицы приводит к разным результатам. Среда исполнения (runtime) Go пытается сделать всё возможное, рандомизируя порядок итерирования, но ей это не всегда удаётся, поэтому вы можете получить несколько одинаковых итераций (например, пять) подряд.

А если вы используете Go Playground (https://play.golang.org/), то всегда будете получать одинаковые результаты, потому что код не перекомпилируется, пока вы его не измените.

26. Сбойное поведение в выражениях switch

27. Инкременты и декременты

Во многих языках есть операторы инкрементирования и декрементирования. Но в Go не поддерживаются их префиксные версии. Также нельзя в одном выражении использовать оба этих выражения.

28. Побитовый NOT-оператор

используется в качестве унарной NOT-операции (aka побитовое дополнение, bitwise complement), однако в Go для этого применяется XOR-оператор (^).

Кого-то может запутать, что ^ в Go — это XOR-оператор. Если хотите, выражайте унарную NOT-операцию (например, NOT 0x02 ) с помощью бинарной XOR-операции (например, 0x02 XOR 0xff ). Это объясняет, почему ^ используется для выражения унарной NOT-операции.

Также в Go есть специальный побитовый оператор AND NOT (&^), который легко принять за оператор NOT. AND NOT выглядит как специальная функция/хак ради поддержки A AND (NOT B) без обязательного использования фигурных скобок.

29. Различия приоритетов операторов

Помимо «довольно понятных» (bit clear) операторов (&^), в Go есть набор стандартных операторов, используемых многими другими языками. Но их приоритеты в данном случае не всегда такие же.

30. Неэкспортированные поля структур не кодируются

Поля структур (struct fields), начинающиеся со строчных букв, не будут кодироваться (JSON, XML, GON и т. д.), так что при декодировании структуры вы получите в этих неэкспортированных полях нулевые значения.

31. Выход из приложений с помощью активных горутин

Приложение не будет ждать завершения ваших горутин. Новички часто об этом забывают. Все когда-то начинают — в таких ошибках нет ничего стыдного.

Если запустить это приложение, вы увидите:

Похоже, все горутины закончили работать до выхода главной горутины. Замечательно! Однако вы увидите и это:

Теперь всё работает правильно.

32. При отправке в небуферизованный канал данные возвращаются по мере готовности получателя

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

33. Отправка в закрытый канал приводит к panic

Отправка данных в закрытый канал приводит к panic. Это задокументированное поведение, но оно не всегда интуитивно ожидаемо разработчиками, которые могут считать, что поведение при отправке будет аналогично поведению при приёме.

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

Пример с багом можно исправить, сигнализируя через специальный канал отмены (special cancellation channel) остальным рабочим горутинам, что их результаты больше не нужны.

34. Использование «nil»-каналов

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

35. Методы, принимающие параметры по значению, не меняют исходных значений

Параметры методов — это как обычные аргументы функций. Если они объявляются значением, то функция/метод получает копию вашего аргумента (receiver argument). Изменения в принятом значении не повлияют на исходное значение, если значение — переменная хеш-таблицы (map) или слайса и вы обновляете элементы коллекции или если обновляемые поля в значении — это указатели.

36. Закрытие тела HTTP-ответа

Делая запрос с помощью стандартной HTTP-библиотеки, вы получаете переменную HTTP-ответа. Даже если вы не читаете тело ответа, всё равно нужно его закрыть. Обратите внимание: это относится и к пустым ответам. О них очень легко забыть, особенно новичкам.

Некоторые новички пытаются закрывать тело ответа, но в неправильном месте.

Самый распространённый способ закрыть тело ответа — с помощью вызова defer после проверки ошибочности HTTP-ответа.

Её можно предотвратить, добавив вызов для закрытия тел ответов non-nil в блоке обработки ошибок HTTP-запросов. Другое решение: использовать один вызов defer для закрытия тел ответов для всех сбойных и успешных запросов.

Если для вашего приложения важно повторно использовать HTTP-соединения, то в конце логики обработки ответа может понадобиться добавить что-то вроде этого:

Это будет необходимо, если вы не считываете всё тело ответа немедленно, например при обработке ответов JSON API с помощью подобного кода:

37. Закрытие HTTP-соединений

Некоторые HTTP-серверы какое-то время держат сетевые соединения открытыми (согласно спецификации HTTP 1.1 и серверной конфигурации keep alive ). По умолчанию стандартная HTTP-библиотека закрывает соединения, только когда об этом просит целевой HTTP-сервер. Тогда при определённых условиях в вашем приложении могут закончиться сокеты / файловые дескрипторы.

Можно попросить библиотеку закрывать соединение после завершения вашего запроса, задав значение true в поле Close переменной запроса.

Можно ещё глобально отключить повторное использование HTTP-соединений. Для этого создайте кастомную конфигурацию HTTP-транспорта.

Если вы отправляете на один сервер много запросов, то этого достаточно для сохранения соединения открытым. Но если приложение за короткое время шлёт один-два запроса на много разных серверов, то лучше закрывать соединения сразу после получения ответов. Также можно увеличить лимит на количество открытых файлов. Что лучше — зависит от вашего приложения.

38. Десериализация (unmarshalling) JSON-чисел в интерфейсные значения

panic: interface conversion: interface is float64, not int

Если JSON-значение, которое вы пытаетесь декодировать, целочисленное, есть несколько вариантов.

Это полезно, если вы должны выполнить декодирование условного JSON-поля (conditional field) в условиях возможности изменения структуры или типа поля.

39. Сравнение struct, array, slice и map

Можно использовать оператор эквивалентности == для сравнения переменных структур, если каждое поле структуры можно сравнить с помощью этого оператора.

Если хоть одно из полей несравниваемо, то применение оператора эквивалентности приведёт к ошибке компилирования. Обратите внимание, что сравнивать массивы можно только тогда, когда сравниваемы их данные.

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

Самое популярное решение: использовать функцию DeepEqual() из пакета reflect.

Помимо невысокой скорости (что может быть критично для вашего приложения), DeepEqual() имеет свои собственные подводные камни.

DeepEqual() не всегда идеальна при сравнении слайсов.

40. Восстановление после panic

Функцию recover() можно использовать для поимки/перехвата panic. Это получится, только если вызвать её в блоке defer.

Вызов recover() сработает, только если будет выполнен в блоке defer.

41. Обновление и привязка значений полей в slice, array и map в выражениях for range

Сгенерированные в выражениях range значения данных — это копии реальных элементов коллекций, а не ссылки на исходные элементы. Стало быть, обновление значений не изменит исходные данные. Кроме того, если взять адрес значения, то вы не получите указатель на исходные данные.

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

42. «Скрытые данные» в слайсах

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

Чтобы избежать этой ошибки, удостоверьтесь, что копируете нужные данные из временного слайса (вместо перенарезки).

43. «Повреждение» данных в слайсах

Допустим, вам нужно переписать путь (хранящийся в слайсе). Чтобы ссылаться на каждую папку, вы его перенарезаете, изменяя имя первой папки, а затем комбинируете имена в новый путь.

Так не сработает. Вместо AAAAsuffix/BBBBBBBBB вы получите AAAAsuffix/uffixBBBB. Причина в том, что слайсы обеих папок ссылаются на один и тот же массив данных из исходного слайса пути. То есть исходный путь тоже изменился. Это может быть проблемой для вашего приложения.

Ее можно решить, разместив в памяти новые слайсы и скопировав туда нужные данные. Другой выход: использовать полное выражение слайса (full slice expression).

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

44. «Устаревшие» слайсы

На одни и те же данные могут ссылаться несколько слайсов. Например, когда вы создаёте новый слайс на основе имеющегося. Если такое поведение важно для вашего приложения, позаботьтесь об «устаревших» слайсах.

В какой-то момент добавление данных в один из слайсов приведёт к размещению в памяти нового массива, потому что в старом не хватит места для новых данных. Теперь на старый массив (со старыми данными) ссылаются несколько слайсов.

45. Методы и объявления типов

Когда вы определяете новый тип на основе существующего (не интерфейсного), тем самым вы создаёте объявление типа и не наследуете методы, объявленные в существующем типе.

Если вам нужны методы из исходного типа, вы можете задать новый тип структуры, встроив исходный в качестве анонимного поля.

Объявления интерфейсных типов также сохраняют свои наборы методов.

46. Как выбраться из кодовых блоков for switch и for select

То же самое и с выражением goto …

47. Итерационные переменные и замыкания в выражениях for

Другое решение: передать текущую итерационную переменную анонимной горутине в виде параметра.

Здесь чуть более сложная версия ловушки.

Как вы думаете, что вы увидите (и почему), запустив этот код?

48. Вычисление аргумента блока defer (Deferred Function Call Argument Evaluation)

Аргументы для вызовов отложенных функций вычисляются тогда же, когда и выражение defer (а не когда на самом деле выполняется функция).

49. Вызов блока defer

Один из способов решения проблемы — обернуть кодовый блок в функцию.

Другое решение: избавиться от выражения defer 🙂

50. Ошибки при приведении типов

Сбойные утверждения типов возвращают «нулевое значение» для целевых типов, использованных в операторе утверждения. Наложившись на сокрытие переменных, это может привести к непредсказуемому поведению.

51. Блокированные горутины и утечки ресурсов

В выступлении «Go Concurrency Patterns» на конференции Google I/O в 2012-м Роб Пайк рассказал о нескольких фундаментальных concurrency-шаблонах. Один из них — извлечение первого результата.

Для каждой копии (replica) поиска функция запускает отдельную горутину. Каждая из горутин отправляет свои поисковые результаты в канал результатов. Возвращается первое значение из канала.

А что с результатами от других горутин? И что насчёт них самих?

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

Чтобы этого избежать, все горутины должны завершиться (exit). Одно из возможных решений: использовать достаточно большой буферизованный канал результатов, способный вместить все результаты.

Другое решение: использовать выражение select со сценарием (case) default и буферизованный канал на одно значение. Сценарий default позволяет быть уверенным, что горутина не застряла, даже если канал результатов не может принимать сообщения.

Также можно использовать специальный канал отмены (special cancellation channel) для прерывания рабочих горутин.

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

52. Применение методов, принимающих значение по ссылке (pointer receiver), к экземплярам значений

Пока значение адресуемо (addressable), к нему можно применять метод, принимающий значение по ссылке. Иными словами, в некоторых случаях вам не нужно иметь версию метода, принимающего параметр по значению.

Но не каждая переменная адресуема. Элементы хеш-таблицы (map) неадресуемы. Переменные, на которые ссылаются через интерфейсы, тоже неадресуемы.

53. Обновление полей значений в хеш-таблице

Если у вас есть таблица, состоящая из структур, то вы не можете обновлять отдельные структурные поля.

Это не работает, потому что элементы таблицы не адресуемы.

Новичков может дополнительно путать то, что элементы слайсов — адресуемы.

Обратите внимание, что когда-то в одном из компиляторов (gccgo) можно было обновлять поля элементов таблицы. Но это быстро пофиксили 🙂 Также считалось, что такая возможность появится в Go 1.3. Но в то время это было не так важно, так что фича всё ещё висит в списке todo.

Первое обходное решение: использовать временную переменную.

Второе обходное решение: использовать хеш-таблицу с указателями.

Кстати, что будет, если выполнить этот код?

54. nil-интерфейсы и nil-интерфейсные значения

Остерегайтесь этой ловушки, когда у вас есть функция, возвращающая интерфейсы.

55. Переменные стека и кучи

56. GOMAXPROCS, согласованность (concurrency) и параллелизм

Существует распространённое заблуждение, что GOMAXPROCS представляет собой количество процессоров, которые Go будет использовать для запуска горутин. Документация к функции runtime.GOMAXPROCS() только добавляет неразберихи. Но в описании к переменной GOMAXPROCS (https://golang.org/pkg/runtime/) говорится именно о тредах ОС.

Значение GOMAXPROCS может превышать количество ваших процессоров, верхний предел — 256.

57. Изменение порядка операций чтения и записи

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

Если запустить этот код несколько раз, то можно увидеть такие комбинации переменных a и b :

Если нужно сохранить порядок операций чтения и записи среди нескольких горутин, то используйте каналы или соответствующие конструкции из пакета sync.

58. Диспетчеризация по приоритетам (Preemptive Scheduling)

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

Другое решение: явно вызвать диспетчер. Это можно сделать с помощью функции Gosched() из пакета runtime.

Если вы дочитали до конца и у вас есть комментарии или идеи, добро пожаловать в дискуссию на Reddit (и в комментарии здесь, на Хабре. — Примеч. пер.).

Источник

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *