Часто задаваемые вопросы (FAQ)

Происхождение

Какова цель проекта?

В момент появления Go в 2007 году программный мир был совсем другим, чем сегодня. Производственные программы обычно писались на C++ или Java, GitHub ещё не существовал, большинство компьютеров ещё не были многоядерными, а кроме Visual Studio и Eclipse было мало IDE или других высокоуровневых инструментов, а тем более бесплатных в интернете.

Тем временем мы стали раздражены чрезмерной сложностью, необходимой для создания крупных программных проектов с использованием языков, которые мы использовали, и их связанных систем сборки. Компьютеры стали значительно быстрее с тех пор, как были разработаны языки, такие как C, C++ и Java, но сам процесс программирования не продвинулся так же далеко. Кроме того, было ясно, что многоядерные процессоры становятся универсальными, но большинство языков не предлагали никакой поддержки для их эффективной и безопасной работы.

Мы решили сделать шаг назад и подумать, какие основные проблемы будут доминировать в инженерии программного обеспечения в ближайшие годы, по мере развития технологий, и как новый язык может помочь решить их. Например, рост многопроцессорных CPU говорит о том, что язык должен обеспечивать первоклассную поддержку какой-либо формы параллелизма или конкурентности. А чтобы управлять ресурсами в большой конкурентной программе, нужна была сборка мусора или, по крайней мере, какой-то вид безопасного автоматического управления памятью.

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

Более подробное описание целей Go и того, как они реализованы или, по крайней мере, подходят к реализации, доступно в статье Go at Google: Language Design in the Service of Software Engineering.

Какова история проекта?

Роберт Гриземер, Роб Пайк и Кен Томпсон начали обсуждать цели нового языка на доске 21 сентября 2007 года. В течение нескольких дней цели сформировались в план действий и довольно точное представление о том, что это будет. Проектирование продолжалось неполный рабочий день параллельно с другими задачами. К январю 2008 года Кен начал работу над компилятором, чтобы исследовать идеи; он генерировал C-код в качестве вывода. К середине года язык стал полноценным проектом и достаточно устоялся, чтобы попытаться создать производственный компилятор. В мае 2008 года Иан Тейлор независимо начал работу над фронтендом для Go на GCC, используя черновой спецификации. Расс Кс joined в конце 2008 года и помог перевести язык и библиотеки из прототипа в реальность.

Go стал общедоступным проектом с открытым исходным кодом 10 ноября 2009 года. Бесчисленное количество людей из сообщества внесли свои идеи, обсуждения и код.

Сейчас в мире насчитывается миллионы программистов на Go — гоферов — и их становится всё больше. Успех Go значительно превзошёл наши ожидания.

Откуда взялся манекен гофера?

Манекен и логотип были разработаны Рене Френч, которая также разработала Glenda, кролика из Plan 9. Блог-пост о гофере объясняет, как он был получен из того, что она использовала для дизайна футболки на WFMU несколько лет назад. Логотип и манекен защищены лицензией Creative Commons Attribution 4.0.

У гофера есть модельный лист, иллюстрирующий его характеристики и способы правильного изображения. Модельный лист впервые был показан в выступлении Рене на Gophercon в 2016 году. У него есть уникальные особенности; он — гофер Go, а не просто любой гофер.

Как называется язык — Go или Golang?

Язык называется Go. Обозначение «golang» возникло потому, что веб-сайт изначально имел адрес golang.org. (В то время не существовало домена .dev.) Многие используют имя golang, хотя оно удобно как метка. Например, тег социальных сетей для языка — «#golang». Название языка — просто Go, независимо от этого.

Отдельное замечание: хотя официальный логотип имеет две заглавные буквы, название языка пишется Go, а не GO.

Почему был создан новый язык?

Go появился из-за разочарования в существующих языках и средах для работы, которую мы выполняли в Google. Программирование стало слишком сложным, и выбор языков был в какой-то степени виноват. Нужно было выбирать между эффективной компиляцией, эффективным выполнением или простотой программирования; все три свойства не были доступны в одном и том же основном языке. Программисты, которые могли, выбирали простоту вместо безопасности и эффективности, переходя на динамически типизированные языки, такие как Python и JavaScript, вместо C++ или, в меньшей степени, Java.

Мы не были одни в своих опасениях. После многих лет относительно спокойной ситуации в области языков программирования, Go стал одним из первых среди нескольких новых языков — Rust, Elixir, Swift и других — которые снова сделали разработку языков программирования активной, почти основной областью.

Go решил эти проблемы, попытавшись объединить простоту программирования интерпретируемого, динамически типизированного языка с эффективностью и безопасностью статически типизированного, скомпилированного языка. Также он стремился быть лучше адаптированным к современным аппаратным средствам, с поддержкой сетевых и многопроцессорных вычислений. Наконец, работа с Go должна быть быстрой: сборка крупного исполняемого файла на одном компьютере должна занимать не более нескольких секунд. Достижение этих целей заставило нас переосмыслить некоторые подходы к программированию из наших текущих языков, что привело к: композиционной, а не иерархической системе типов; поддержке параллелизма и сборки мусора; строгой спецификации зависимостей; и так далее. Эти задачи нельзя эффективно решить с помощью библиотек или инструментов; требовался новый язык.

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

Какие языки являются предшественниками Go?

Go в основном принадлежит к семейству C (базовый синтаксис), с существенным вкладом из семейства Pascal/Modula/Oberon (объявления, пакеты), а также некоторыми идеями из языков, вдохновленных CSP Тони Хоара, таких как Newsqueak и Limbo (параллелизм). Однако, это новый язык в целом. В каждом аспекте язык был спроектирован с мыслью о том, что делают программисты, и о том, как сделать программирование, по крайней мере, того вида программирования, которым мы занимаемся, более эффективным, а значит — более интересным.

Какие принципы лежат в основе проектирования?

Когда Go проектировался, Java и C++ были наиболее часто используемыми языками для написания серверов, по крайней мере, в Google. Мы считали, что эти языки требуют слишком много рутинной работы и повторений. Некоторые программисты реагировали на это переходом к более динамическим, гибким языкам, таким как Python, в ущерб эффективности и безопасности типов. Мы считали, что должно быть возможно объединить эффективность, безопасность и гибкость в одном языке.

Go стремится уменьшить объем вводимых данных как в прямом, так и в переносном смысле. На протяжении всего дизайна мы старались уменьшить количество шума и сложности. Не существует предварительных объявлений и файлов заголовков; всё объявляется ровно один раз. Инициализация выражена, автоматична и проста в использовании. Синтаксис чистый и не перегружен ключевыми словами. Повторения (foo.Foo* myFoo = new(foo.Foo)) уменьшаются с помощью простого вывода типа с использованием конструкции := для объявления и инициализации. И, возможно, наиболее радикально, не существует иерархии типов: типы просто существуют, им не нужно объявлять свои отношения. Эти упрощения позволяют Go быть выразительным и понятным без потери продуктивности.

Еще один важный принцип — сохранять концепции ортогональными. Методы могут быть реализованы для любого типа; структуры представляют данные, а интерфейсы — абстракции; и так далее. Ортогональность облегчает понимание того, что происходит, когда вещи сочетаются.

Использование

Использует ли Google Go внутренне?

Да. Go активно используется в производстве внутри Google. Один из примеров — сервер загрузок Google, dl.google.com, который доставляет бинарные файлы Chrome и другие большие установочные файлы, такие как пакеты apt-get.

Go — не единственный язык, используемый в Google, совсем нет, но это ключевой язык для ряда областей, включая инженерию надежности сайтов (SRE) и обработку данных в масштабах. Он также является ключевой частью программного обеспечения, которое запускает Google Cloud.

Какие еще компании используют Go?

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

Это не только облако, как можно увидеть по списку компаний на веб-сайте go.dev вместе с некоторыми историями успеха. Кроме того, Вики Go включает страницу, которая регулярно обновляется и перечисляет множество компаний, использующих Go.

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

Возможно использовать C и Go вместе в одном адресном пространстве, но это не является естественной совместимостью и может потребовать специального программного обеспечения интерфейса. Кроме того, связывание C с кодом на Go приводит к потере свойств безопасности памяти и управления стеком, которые предоставляет Go. Иногда абсолютно необходимо использовать библиотеки C для решения задачи, но это всегда вводит элемент риска, отсутствующий в чистом Go-коде, поэтому следует действовать с осторожностью.

Если вам действительно нужно использовать C вместе с Go, то как это делать зависит от реализации компилятора Go. «Стандартный» компилятор, входящий в состав инструментария Go, поддерживаемого командой Go в Google, называется gc. Кроме того, существуют также компиляторы на основе GCC (gccgo) и на основе LLVM (gollvm), а также растущий список необычных компиляторов, служащих различным целям, иногда реализующих подмножества языка, такие как TinyGo.

Gc использует другую соглашение о вызовах и компоновщик, чем C, поэтому напрямую вызвать Go-программу из C-программы или наоборот нельзя. Программа cgo предоставляет механизм для «иностранных функциональных интерфейсов», позволяющий безопасно вызывать C-библиотеки из Go-кода. SWIG расширяет эту возможность до C++-библиотек.

Вы также можете использовать cgo и SWIG с gccgo и gollvm. Поскольку они используют традиционную ABI, возможно, с большой осторожностью, связать код из этих компиляторов напрямую с программами, скомпилированными с помощью GCC/LLVM-компилятора на C или C++. Однако безопасное выполнение такого связывания требует понимания соглашений о вызовах для всех участвующих языков, а также заботы о пределах стека при вызове C или C++ из Go.

Какие IDE поддерживает Go?

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

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

Список известных IDE и редакторов, предлагающих хорошую поддержку Go, включает Emacs, Vim, VSCode, Atom, Eclipse, Sublime, IntelliJ (через пользовательскую версию под названием GoLand) и многие другие. Скорее всего, ваша любимая среда разработки будет продуктивной для программирования на Go.

Поддерживает ли Go Google Protocol Buffers?

Для обеспечения необходимого компилятора и библиотеки существует отдельный проект с открытым исходным кодом. Он доступен по адресу github.com/golang/protobuf/.

Проектирование

Имеется ли у Go среда выполнения?

Go имеет обширную библиотеку среды выполнения, часто называемую просто runtime, которая является частью каждого Go-программы. Эта библиотека реализует сборку мусора, параллелизм, управление стеком и другие критически важные особенности языка Go. Хотя среда выполнения более центральна для языка, она аналогична libc, стандартной библиотеке языка C.

Однако важно понимать, что среда выполнения Go не включает виртуальную машину, как это делает Java Runtime Environment. Go-программы компилируются заранее в нативный машинный код (или JavaScript или WebAssembly, в случае некоторых вариантов реализации). Таким образом, хотя термин часто используется для описания виртуальной среды, в которой выполняется программа, в Go слово «runtime» просто обозначает библиотеку, предоставляющую критически важные языковые сервисы.

Что такое Unicode-идентификаторы?

При проектировании Go мы хотели убедиться, что язык не будет чрезмерно ориентирован на ASCII, что означало расширение пространства идентификаторов за пределами 7-битного ASCII. Правило Go — символы идентификаторов должны быть буквами или цифрами, определёнными в Unicode — простое для понимания и реализации, но имеющее ограничения. Например, комбинирующиеся символы исключены по дизайну, что исключает некоторые языки, такие как деванагари.

Это правило имеет ещё одно неприятное последствие. Поскольку экспортируемый идентификатор должен начинаться с заглавной буквы, идентификаторы, созданные из символов некоторых языков, по определению не могут быть экспортированы. На данный момент единственным решением является использование чего-то вроде X日本語, что явно неудовлетворительно.

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

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

Почему в Go нет функции X?

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

Если вас беспокоит тот факт, что в Go отсутствует функция X, пожалуйста, простите нас и ознакомьтесь с возможностями, которые предоставляет Go. Возможно, вы обнаружите, что они компенсируют недостающую X интересным образом.

Когда в Go появились дженерик типы?

В релизе Go 1.18 в язык были добавлены параметры типов. Это позволяет использовать форму полиморфного или дженерик программирования. См. спецификацию языка и предложение для получения подробной информации.

Почему Go изначально был выпущен без дженерик типов?

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

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

Почему в Go нет исключений?

Мы полагаем, что связывание исключений с управляющей структурой, как в идиоме try-catch-finally, приводит к запутанному коду. Это также склоняет программистов помечать слишком много обычных ошибок, таких как неудачное открытие файла, как исключительные.

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

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

См. статью Defer, Panic, and Recover для получения подробной информации. Также, в блоге Errors are values описывается один из подходов к чистой обработке ошибок в Go, демонстрируя, что, поскольку ошибки — это просто значения, вся мощь Go может быть использована при обработке ошибок.

Почему в Go нет утверждений?

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

Мы понимаем, что это спорный момент. Во многих аспектах языка и библиотек Go отличается от современных практик, просто потому, что мы считаем, что иногда имеет смысл попробовать другой подход.

Почему в Go реализуется параллелизм на основе идей CSP?

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

Одной из самых успешных моделей предоставления высокоуровневой языковой поддержки для параллелизма является модель Хоара «Коммуникующие последовательные процессы», или CSP. Occam и Erlang — два хорошо известных языка, основанных на CSP. Примитивы параллелизма в Go происходят из другой ветви этого семейства, главным вкладом которого является мощная концепция каналов как объектов первого класса. Опыт с несколькими предыдущими языками показал, что модель CSP хорошо сочетается с процедурным языковым фреймворком.

Почему горутины вместо потоков?

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

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

Почему операции с картами не определены как атомарные?

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

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

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

В помощь правильному использованию карт, некоторые реализации языка содержат специальную проверку, которая автоматически сообщает во время выполнения, когда карта изменяется небезопасным образом при параллельном выполнении. Также в библиотеке sync имеется тип под названием sync.Map, который хорошо работает для определённых паттернов использования, таких как статические кэши, хотя он не подходит в качестве общего заменителя встроенной типа карты.

Вы примете моё изменение языка?

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

Несмотря на то, что Go — это проект с открытым исходным кодом, язык и библиотеки защищены обещанием совместимости, которое предотвращает изменения, нарушающие существующие программы, по крайней мере на уровне исходного кода (программы могут требовать повторной компиляции время от времени, чтобы оставаться актуальными). Если ваше предложение нарушает спецификацию Go 1, мы даже не можем рассмотреть идею, независимо от её достоинств. Будущий крупный релиз Go может быть несовместим с Go 1, но обсуждения по этой теме только начались, и одно точно: таких несовместимостей будет очень мало. Кроме того, обещание совместимости побуждает нас предоставлять автоматический путь для старых программ адаптироваться, если такая ситуация возникнет.

Даже если ваше предложение совместимо со спецификацией Go 1, оно может не соответствовать духу целей проектирования Go. Статья Go в Google: проектирование языка в интересах инженерии программного обеспечения объясняет происхождение Go и мотивацию, стоявшую за его проектированием.

Типы

Является ли Go объектно-ориентированным языком?

Да и нет. Хотя в Go есть типы и методы, а также поддерживается объектно-ориентированный стиль программирования, иерархии типов в языке нет. Концепция «интерфейса» в Go предлагает другой подход, который, по нашему мнению, прост в использовании и в некоторых аспектах более общий. Существуют также способы встраивания типов в другие типы, чтобы обеспечить что-то аналогичное — но не идентичное — наследованию.

Кроме того, методы в Go более общие, чем в C++ или Java: они могут быть определены для любого вида данных, даже встроенных типов, таких как обычные, «непомещённые» целые числа. Они не ограничены структурами (классами).

Кроме того, отсутствие иерархии типов делает «объекты» в Go намного более лёгкими, чем в языках, таких как C++ или Java.

Как реализовать динамическую диспетчизацию методов?

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

Почему отсутствует наследование типов?

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

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

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

Поскольку между типами и интерфейсами нет явных отношений, не существует иерархии типов для управления или обсуждения.

Возможно использовать эти идеи для создания чего-то аналогичного безопасным по типу конвейерам Unix. Например, обратите внимание, как fmt.Fprintf позволяет форматированный вывод в любое устройство вывода, а не только в файл, или как пакет bufio может быть полностью независим от ввода-вывода файлов, или как пакет image генерирует сжатые файлы изображений. Все эти идеи исходят из одного интерфейса (io.Writer), представляющего один метод (Write). И это лишь поверхностное описание. Интерфейсы Go оказывают глубокое влияние на то, как строятся программы.

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

Почему len является функцией, а не методом?

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

Почему в Go не поддерживается перегрузка методов и операторов?

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

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

Что касается перегрузки операторов, то это больше удобство, чем абсолютная необходимость. Снова же, отсутствие такой возможности делает всё проще.

Почему в Go нет деклараций "implements"?

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

См. вопрос о наследовании типов для получения дополнительных деталей.

Как можно гарантировать, что мой тип реализует интерфейс?

Вы можете попросить компилятор проверить, реализует ли тип T интерфейс I, выполнив присваивание с использованием нулевого значения типа T или указателя на T, в зависимости от ситуации:

<code>type T struct{}
var _ I = T{}       // Проверить, что T реализует I.
var _ I = (*T)(nil) // Проверить, что *T реализует I.
</code>

Если тип T (или *T, соответственно) не реализует интерфейс I, ошибка будет замечена на этапе компиляции.

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

<code>type Fooer interface {
  Foo()
  ImplementsFooer()
}
</code>

Тип должен реализовать метод ImplementsFooer, чтобы быть Fooer, что ясно документирует факт и объявляет его в выводе go doc.

<code>type Bar struct{}
func (b Bar) ImplementsFooer() {}
func (b Bar) Foo() {}
</code>

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

Почему тип T не реализует интерфейс Equal?

Рассмотрим следующий простой интерфейс для представления объекта, способного сравнить себя с другим значением:

<code>type Equaler interface {
  Equal(Equaler) bool
}
</code>

и этого типа, T:

<code>type T int
func (t T) Equal(u T) bool { return t == u } // does not satisfy Equaler
</code>

В отличие от аналогичной ситуации в некоторых полиморфных типизированных системах, T не реализует Equaler. Тип аргумента функции T.EqualT, а не буквально требуемый тип Equaler.

В Go система типов не выполняет автоматическое преобразование аргумента функции Equal; это задача программиста, как показано на примере типа T2, который реализует Equaler:

<code>type T2 int
func (t T2) Equal(u Equaler) bool { return t == u.(T2) }  // satisfies Equaler
</code>

Даже это не похоже на другие типизированные системы, потому что в Go любой тип, реализующий Equaler, может быть передан в качестве аргумента в T2.Equal, и во время выполнения необходимо проверить, что аргумент имеет тип T2. Некоторые языки обеспечивают такую гарантию на этапе компиляции.

Связанный пример идёт в другую сторону:

<code>type Opener interface {
  Open() Reader
}
func (t T3) Open() *os.File
</code>

В Go T3 не реализует Opener, хотя это возможно в других языках.

Хотя в Go система типов делает меньше для программиста в подобных случаях, отсутствие подтипов делает правила реализации интерфейсов очень простыми: совпадают ли имена и сигнатуры функций с интерфейсом? Правило Go также легко реализовать эффективно. Мы считаем, что эти преимущества компенсируют отсутствие автоматического преобразования типов.

Можно ли преобразовать []T в []interface{}?

Напрямую это невозможно. Это запрещено спецификацией языка, поскольку два этих типа имеют разное представление в памяти. Необходимо скопировать элементы по отдельности в целевой срез. Этот пример преобразует срез int в срез interface{}:

<code>t := []int{1, 2, 3, 4}
s := make([]interface{}, len(t))
for i, v := range t {
  s[i] = v
}
</code>

Можно ли преобразовать []T1 в []T2, если T1 и T2 имеют одинаковый базовый тип?

Последняя строка этого примера кода не компилируется.

<code>type T1 int
type T2 int
var t1 T1
var x = T2(t1) // OK
var st1 []T1
var sx = ([]T2)(st1) // NOT OK
</code>

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

Почему значение nil ошибки не равно nil?

Внутри интерфейсы реализуются как два элемента: тип T и значение V. V — это конкретное значение, например int, struct или указатель, но никогда сам интерфейс, и имеет тип T. Например, если мы сохраним значение int 3 в интерфейсе, полученное значение интерфейса будет, условно, (T=int, V=3). Значение V также называется динамическим значением интерфейса, поскольку переменная интерфейса может содержать различные значения V (и соответствующие типы T) во время выполнения программы.

Значение интерфейса равно nil только в том случае, если оба поля V и T не установлены (T=nil, V не установлено). В частности, nil интерфейс всегда будет содержать nil тип. Если мы сохраним nil указатель типа *int внутри значения интерфейса, внутренний тип будет *int, независимо от значения указателя: (T=*int, V=nil). Таким образом, такое значение интерфейса будет ненулевым, даже если значение указателя V внутри равно nil.

Эта ситуация может быть запутанной и возникает, когда nil значение сохраняется внутри значения интерфейса, например, возвращаемого error:

<code>func returnsError() error {
  var p *MyError = nil
  if bad() {
    p = ErrBad
  }
  return p // Will always return a non-nil error.
}
</code>

Если всё проходит хорошо, функция возвращает nil p, поэтому возвращаемое значение будет значением интерфейса error, содержащим (T=*MyError, V=nil). Это означает, что если вызывающая сторона сравнивает возвращённую ошибку с nil, она всегда будет выглядеть как ошибка, даже если ничего плохого не произошло. Чтобы корректно вернуть nil error вызывающей стороне, функция должна явно возвращать nil:

<code>func returnsError() error {
  if bad() {
    return ErrBad
  }
  return nil
}
</code>

Хорошей практикой является использование типа error в сигнатуре функций, возвращающих ошибки (как было показано выше), вместо конкретного типа, такого как *MyError, чтобы гарантировать корректное создание ошибки. Например, os.Open возвращает error, хотя, если он не nil, всегда имеет конкретный тип *os.PathError.

Аналогичные ситуации могут возникать при использовании интерфейсов. Просто помните, что если какое-либо конкретное значение было сохранено в интерфейсе, интерфейс не будет равен nil. За дополнительной информацией обращайтесь к статье The Laws of Reflection.

Почему нулевые типы ведут себя странно?

Go поддерживает нулевые типы, такие как структура без полей (struct{}) или массив без элементов ([0]byte). Ничего нельзя сохранить в нулевом типе, но такие типы иногда полезны, когда значение не требуется, как в случае с map[int]struct{} или типом, у которого есть методы, но нет значения.

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

Кроме того, язык не даёт никаких гарантий относительно того, будут ли указатели на две различные переменные с нулевым типом равны или нет. Такие сравнения могут возвращать true в один момент программы, а затем false в другой, в зависимости от того, как именно программа компилируется и выполняется.

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

<code>func main() {
  type S struct {
    f1 byte
    f2 struct{}
  }
  fmt.Println(unsafe.Sizeof(S{}))
}
</code>

в большинстве реализаций Go напечатает 2, а не 1.

Почему в Go нет необъявленных объединений, как в C?

Необъявленные объединения нарушили бы гарантии безопасности памяти в Go.

Почему в Go нет вариативных типов?

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

Мы рассматривали возможность добавления вариативных типов в Go, но после обсуждения решили исключить их, поскольку они пересекаются с интерфейсами неочевидным образом. Что произойдёт, если элементы вариативного типа сами являются интерфейсами?

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

Почему в Go нет ковариантных типов результата?

Ковариантные типы результата означали бы, что интерфейс, такой как

<code>type Copyable interface {
  Copy() interface{}
}
</code>

удовлетворяется методом

<code>func (v Value) Copy() Value
</code>

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

Значения

Почему в Go не предусмотрены неявные числовые преобразования?

Удобство автоматического преобразования между числовыми типами в C перевешивается вызванным ими недоразумением. Когда выражение беззнаковое? Каков размер значения? Происходит ли переполнение? Является ли результат переносимым, независимо от машины, на которой он выполняется? Это также усложняет компилятор; «обычные арифметические преобразования» в C не просты для реализации и несогласованы между архитектурами. По причинам переносимости мы решили сделать всё ясным и простым за счёт некоторых явных преобразований в коде. Определение констант в Go — произвольной точности, свободной от аннотаций знака и размера — значительно улучшает ситуацию.

Связанный момент заключается в том, что, в отличие от языка C, типы int и int64 являются различными, даже если тип int представляет собой 64-битный тип. Тип int является дженериком; если вы хотите знать, сколько битов занимает целое число, Go поощряет вас быть явным.

Как работают константы в Go?

Хотя Go строг в преобразованиях между переменными различных числовых типов, константы в языке намного более гибкие. Литералы констант, такие как 23, 3.14159 и math.Pi, занимают некую идеальную числовую область с произвольной точностью и без переполнения или недостатка. Например, значение math.Pi задано с точностью до 63 десятичных знаков в исходном коде, и константные выражения, включающие это значение, сохраняют точность, превышающую то, что может удержать float64. Только тогда, когда константа или константное выражение присваивается переменной — месту в памяти в программе — она становится «компьютерным» числом со стандартными свойствами и точностью с плавающей точкой.

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

<code>sqrt2 := math.Sqrt(2)
</code>

без претензий со стороны компилятора, потому что идеальное число 2 может быть безопасно и точно преобразовано в float64 для вызова math.Sqrt.

В блоге опубликована статья под названием Constants, которая подробно рассматривает эту тему.

Почему карты встроены в язык?

Та же самая причина, по которой строки встроены: это очень мощная и важная структура данных, поэтому предоставление одной отличной реализации с синтаксической поддержкой делает программирование более приятным. Мы считаем, что реализация карт в Go достаточно сильна, чтобы она удовлетворяла потребности большинства случаев использования. Если конкретное приложение может получить выгоду от пользовательской реализации, её можно написать, но она не будет такой удобной синтаксически; это кажется разумным компромиссом.

Почему карты не допускают срезы в качестве ключей?

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

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

Почему карты, срезы и каналы являются ссылками, тогда как массивы — значения?

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

Написание кода

Как документируются библиотеки?

Для доступа к документации из командной строки, инструмент go имеет doc подкоманду, которая предоставляет текстовый интерфейс к документации для объявлений, файлов, пакетов и т.д.

Глобальная страница обнаружения пакетов pkg.go.dev/pkg/. запускает сервер, который извлекает документацию пакетов из исходного кода Go в любом месте в интернете и отображает её в виде HTML с ссылками на объявления и связанные элементы. Это самый простой способ узнать о существующих библиотеках на Go.

В ранние дни проекта существовал похожая программа godoc, которую также можно было запустить для извлечения документации для файлов на локальной машине; pkg.go.dev/pkg/ является в основном потомком. Ещё одним потомком является команда pkgsite, которую, как и godoc, можно запустить локально, хотя она пока не интегрирована в результаты, отображаемые командой go doc.

Существует ли руководство по стилю программирования на Go?

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

Go имеет установленные соглашения, которые помогают принимать решения в вопросах именования, расположения и организации файлов. Документ Effective Go содержит некоторые рекомендации по этим темам. Более напрямую, программа gofmt является pretty-printer'ом, целью которого является принудительное применение правил форматирования; она заменяет обычный комpendium правил "не делай этого" и "делай это", позволяющий интерпретировать. Весь код Go в репозитории, и большинство кода в открытом источнике, было обработано с помощью gofmt.

Документ под названием Go Code Review Comments — это коллекция очень коротких эссе о деталях идиоматики Go, которые часто пропускают программисты. Это удобная справка для людей, проводящих код-ревью проектов на Go.

Как отправить патч в библиотеки Go?

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

См. документ Contributing to the Go project для получения дополнительной информации о том, как продолжить.

Почему команда "go get" использует HTTPS при клонировании репозитория?

Компании часто разрешают исходящий трафик только по стандартным TCP-портам 80 (HTTP) и 443 (HTTPS), блокируя исходящий трафик на других портах, включая TCP-порт 9418 (git) и TCP-порт 22 (SSH). Используя HTTPS вместо HTTP, git по умолчанию выполняет проверку сертификатов, обеспечивая защиту от атак типа «человек посередине», прослушивания и подделки. Поэтому команда go get использует HTTPS для безопасности.

Git может быть настроен для аутентификации через HTTPS или для использования SSH вместо HTTPS. Для аутентификации через HTTPS можно добавить строку в файл $HOME/.netrc, который использует git:

<code>machine github.com login *USERNAME* password *APIKEY*
</code>

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

Git также может быть настроен на использование SSH вместо HTTPS для URL-адресов, соответствующих заданному префиксу. Например, чтобы использовать SSH для всего доступа к GitHub, добавьте следующие строки в файл ~/.gitconfig:

<code>[url "ssh://git@github.com/"]
insteadOf = https://github.com/
</code>

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

Как мне управлять версиями пакетов с помощью «go get»?

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

Для создания проекта с использованием модулей выполните команду go mod init. Эта команда создаёт файл go.mod, который отслеживает версии зависимостей.

<code>go mod init example/project
</code>

Чтобы добавить, обновить или понизить версию зависимости, выполните команду go get:

<code>go get golang.org/x/text@v0.3.5
</code>

См. Руководство: Создание модуля для получения дополнительной информации о начале работы.

См. Разработка модулей для руководств по управлению зависимостями с использованием модулей.

Пакеты внутри модулей должны сохранять обратную совместимость по мере развития, следуя правилу совместимости импорта:

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

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

Модули реализуют это с помощью семантического версионирования и семантического версионирования импортов. Если требуется нарушение совместимости, выпустите модуль в новой мажорной версии. Модули с мажорной версией 2 и выше требуют суффикс мажорной версии в своем пути (например, /v2). Это сохраняет правило совместимости импорта: пакеты в разных мажорных версиях модуля имеют разные пути.

Указатели и выделение памяти

Когда параметры функции передаются по значению?

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

Значения карт и срезов ведут себя как указатели: это дескрипторы, содержащие указатели на данные карты или среза. Копирование значения карты или среза не копирует данные, на которые они указывают. Копирование значения интерфейса создаёт копию значения, хранящегося в интерфейсе. Если интерфейс хранит структуру, копирование интерфейса создаёт копию структуры. Если интерфейс хранит указатель, копирование интерфейса создаёт копию указателя, но снова не данные, на которые он указывает.

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

Когда следует использовать указатель на интерфейс?

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

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

Рассмотрим объявление переменной,

<code>var w io.Writer
</code>

Функция печати fmt.Fprintf принимает в качестве первого аргумента значение, удовлетворяющее io.Writer—то есть что-то, что реализует канонический метод Write. Поэтому мы можем написать

<code>fmt.Fprintf(w, "hello, world\n")
</code>

Если же мы передадим адрес w, программа не скомпилируется.

<code>fmt.Fprintf(&w, "hello, world\n") // Ошибка компиляции.
</code>

Единственное исключение — любое значение, даже указатель на интерфейс, может быть присвоено переменной типа пустого интерфейса (interface{}). Даже в этом случае, почти наверняка это ошибка, если значение является указателем на интерфейс; результат может быть запутанным.

Следует ли определять методы на значениях или на указателях?

<code>func (s *MyStruct) pointerMethod() { } // метод на указателе
func (s MyStruct)  valueMethod()   { } // метод на значении
</code>

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

Существует несколько соображений.

Во-первых, и самое главное — нуждается ли метод в изменении получателя? Если да, то получатель должен быть указателем. (Срезы и карты действуют как ссылки, поэтому их история немного сложнее, но, например, чтобы изменить длину среза в методе, получатель всё равно должен быть указателем.) В приведённых выше примерах, если pointerMethod изменяет поля s, то вызывающая сторона увидит эти изменения, но valueMethod вызывается со копией аргумента вызывающей стороны (таково определение передачи значения), поэтому изменения, которые он вносит, будут невидимы для вызывающей стороны.

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

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

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

Для типов, таких как базовые типы, срезы и маленькие struct, значение получателя очень дешёвое, поэтому, если семантика метода не требует указателя, значение получателя будет эффективным и понятным.

В чём разница между new и make?

Коротко: new выделяет память, тогда как make инициализирует типы срезов, карт и каналов.

См. соответствующий раздел Effective Go для получения дополнительных сведений.

Каков размер int на 64-битной машине?

Размеры int и uint зависят от реализации, но одинаковы для каждого конкретного платформенного окружения. Для переносимости кода, который полагается на определённый размер значения, следует использовать явно заданный тип, например int64. На 32-битных машинах компиляторы по умолчанию используют 32-битные целые числа, тогда как на 64-битных машинах целые числа имеют 64 бита. (Исторически это было не всегда так.)

С другой стороны, скалярные типы с плавающей точкой и комплексные типы всегда имеют размер (не существует базовых типов float или complex), потому что программисты должны быть осведомлены о точности при использовании чисел с плавающей точкой. Тип по умолчанию для (безымянного) константного значения с плавающей точкой — float64. Таким образом, foo := 3.0 объявляет переменную foo типа float64. Для переменной float32, инициализируемой (безымянной) константой, тип переменной должен быть указан явно в объявлении переменной:

<code>var foo float32 = 3.0
</code>

В качестве альтернативы константе необходимо указать тип с помощью приведения типа, как в примере: foo := float32(3.0).

Как мне узнать, выделяется ли переменная в куче или на стеке?

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

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

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

Почему мой процесс на Go использует столько виртуальной памяти?

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

Чтобы узнать объем фактически выделенной памяти для процесса Go, используйте команду Unix top и обратитесь к столбцам RES (Linux) или RSIZE (macOS).

Параллелизм

Какие операции являются атомарными? А что с мьютексами?

Описание атомарности операций в Go можно найти в документе Go Memory Model.

Низкоуровневая синхронизация и атомарные примитивы доступны в пакетах sync и sync/atomic. Эти пакеты хороши для простых задач, таких как увеличение счётчиков ссылок или обеспечение маломасштабного взаимного исключения.

Для более высокого уровня операций, таких как координация между параллельными серверами, более высокий уровень техник может привести к более приятным программам, и Go поддерживает этот подход через свои горутины и каналы. Например, можно структурировать программу так, чтобы только одна горутина одновременно отвечала за определённую часть данных. Этот подход подводит итог оригинальному поговорке Go,

Не общайтесь, разделяя память. Вместо этого, разделяйте память, общаясь.

См. Share Memory By Communicating code walk и его связанную статью для подробного обсуждения этого концепта.

Большие параллельные программы, вероятно, будут использовать инструменты из обоих этих наборов.

Почему моя программа не работает быстрее с большим количеством CPU?

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

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

Для получения дополнительной информации по этой теме см. выступление под названием Concurrency is not Parallelism.

Как можно контролировать количество CPU?

Количество CPU, доступных одновременно для выполнения горутин, контролируется переменной окружения оболочки GOMAXPROCS, значение которой по умолчанию равно количеству доступных ядер CPU. Программы с потенциалом параллельного выполнения должны, следовательно, достигать этого по умолчанию на многопроцессорной машине. Чтобы изменить количество параллельных CPU, используемых программой, установите переменную окружения или используйте аналогично названную функцию пакета runtime для настройки поддержки среды выполнения для использования другого количества потоков. Установка значения 1 исключает возможность настоящего параллелизма, принудительно заставляя независимые горутины выполнять по очереди.

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

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

Почему нет идентификатора горутины?

Горутины не имеют имен; они просто анонимные рабочие процессы. Они не предоставляют уникального идентификатора, имени или структуры данных программисту. Некоторые люди удивляются этому факту, ожидая, что оператор go вернёт какой-либо элемент, который можно использовать для доступа и управления горутиной позже.

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

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

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

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

Функции и Методы

Почему типы T и *T имеют разные наборы методов?

Как указано в спецификации Go, набор методов типа T состоит из всех методов с получателем типа T, тогда как для соответствующего указательного типа *T набор методов состоит из всех методов с получателем *T или T. Это означает, что набор методов *T включает набор методов T, но не наоборот.

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

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

В качестве примера, если бы следующий код был допустимым:

<code>var buf bytes.Buffer
io.Copy(buf, os.Stdin)
</code>

он скопировал бы стандартный ввод в копию buf, а не в сам buf. Это почти никогда не является желаемым поведением и поэтому запрещено языком.

Что происходит с замыканиями, выполняющимися как горутины?

Из-за особенностей работы переменных цикла до версии Go 1.22 (см. конец этого раздела для обновления) некоторые недоразумения могут возникнуть при использовании замыканий с параллелизмом. Рассмотрим следующую программу:

```go func main() { done := make(chan bool) values := []string{"a", "b", "c"} for _, v := range values { go func() { fmt.Println(v) done <- true }() } // ожидание завершения всех горутин перед выходом for _ = range values { <-done } } ```

Можно ошибочно ожидать увидеть a, b, c как вывод. Вероятно, вы увидите вместо этого c, c, c. Это происходит потому, что каждая итерация цикла использует один и тот же экземпляр переменной v, поэтому каждая замыкание разделяет эту единственную переменную. Когда замыкание выполняется, оно выводит значение v на момент выполнения fmt.Println, но v могло быть изменено с момента запуска горутины. Чтобы помочь обнаружить эту и другие подобные проблемы до их возникновения, выполните go vet.

Чтобы привязать текущее значение v к каждой замыканию по мере их запуска, необходимо изменить внутренний цикл, чтобы создавать новую переменную на каждой итерации. Один из способов — передать переменную как аргумент в замыкание:

for _, v := range values {
  go func(<b>u</b> string) {
    fmt.Println(<b>u</b>)
    done <- true
  }(<b>v</b>)
}

В этом примере значение v передается как аргумент в анонимную функцию. Это значение затем доступно внутри функции как переменная u.

Еще проще — просто создать новую переменную, используя стиль объявления, который может показаться странным, но отлично работает в Go:

for _, v := range values {
  <b>v := v</b> // создаем новую 'v'.
  go func() {
    fmt.Println(<b>v</b>)
    done <- true
  }()
}

Такое поведение языка, не создающее новую переменную для каждой итерации, считалось ошибкой в hindsight, и было исправлено в Go 1.22, который действительно создает новую переменную для каждой итерации, устраняя эту проблему.

Управление потоком выполнения

Почему в Go нет оператора ?:?

В Go нет тернарной операции. Вы можете использовать следующее для достижения того же результата:

<code>if expr {
  n = trueVal
} else {
  n = falseVal
}
</code>

Причиной отсутствия ?: в Go является то, что дизайнеры языка видели, как эта операция используется слишком часто для создания непрозрачно сложных выражений. Форма if-else, хотя и длиннее, не вызывает сомнений в ясности. Языку нужно только одно условное средство управления потоком выполнения.

Параметры типа

Зачем Go имеет параметры типа?

Параметры типа позволяют использовать так называемое обобщенное программирование, при котором функции и структуры данных определяются в терминах типов, которые указываются позже, когда эти функции и структуры данных используются. Например, они позволяют написать функцию, возвращающую минимум двух значений любого упорядоченного типа, не создавая отдельную версию для каждого возможного типа. Более подробное объяснение с примерами см. в блог-посте Why Generics?.

```

Как реализованы дженерики в Go?

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

Как дженерики в Go соотносятся с дженериками в других языках?

Базовая функциональность во всех языках схожа: возможно написать типы и функции, используя типы, которые будут указаны позже. Тем не менее, существуют некоторые различия.

  • Java

    В Java компилятор проверяет дженерик типы во время компиляции, но удаляет типы во время выполнения. Это известно как стирание типов. Например, тип Java, известный как List<Integer> во время компиляции, становится не дженерик типом List во время выполнения. Это означает, например, что при использовании формы отражения типов в Java невозможно отличить значение типа List<Integer> от значения типа List<Float>. В Go информация об отражении для дженерик типа включает полную информацию о типе на этапе компиляции.

    Java использует типовые подстановочные знаки, такие как List<? extends Number> или List<? super Number> для реализации дженеричной ковариантности и контравариантности. Go не имеет этих понятий, что делает дженерик типы в Go намного проще.

  • C++

    Традиционно шаблоны C++ не накладывают никаких ограничений на аргументы типов, хотя C++20 поддерживает необязательные ограничения через концепции. В Go ограничения обязательны для всех параметров типа. Концепции C++20 выражаются как небольшие фрагменты кода, которые должны компилироваться с аргументами типов. Ограничения в Go — это типы интерфейсов, определяющие множество всех допустимых аргументов типов.

    C++ поддерживает метапрограммирование шаблонов; Go — нет. На практике все компиляторы C++ компилируют каждый шаблон в точке его использования; как указано выше, Go может и делает использовать разные подходы для разных реализаций.

  • Rust

    Версия ограничений в Rust известна как trait bounds. В Rust ассоциация между ограничением типа и типом должна быть определена явно, либо в крейте, который определяет ограничение типа, либо в крейте, который определяет тип. В Go аргументы типов неявно удовлетворяют ограничениям, так же как типы Go неявно реализуют типы интерфейсов. Стандартная библиотека Rust определяет стандартные трейты для операций, таких как сравнение или сложение; стандартная библиотека Go — нет, поскольку такие операции могут быть выражены в пользовательском коде через типы интерфейсов. Единственное исключение — предопределенный интерфейс comparable в Go, который отражает свойство, не выразимое в системе типов.

  • Python

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

Почему в Go используются квадратные скобки для списков параметров типов?

Java и C++ используют угловые скобки для списков параметров типов, как в Java List<Integer> и C++ std::vector<int>. Однако этот вариант не был доступен для Go, поскольку он приводит к синтаксической проблеме: при разборе кода внутри функции, например v := F<T>, в момент, когда встречается символ <, возникает неоднозначность — видимо ли это инстанцирование или выражение, использующее оператор <. Это очень сложно разрешить без информации о типах.

Например, рассмотрим инструкцию вида

a, b = w < x, y > (z)

Без информации о типах невозможно определить, представляет ли правая часть присваивания пару выражений (w < x и y > z), или же это инстанцирование и вызов обобщённой функции, возвращающей два результата ((w<x, y>)(z)).

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

Go не является уникальным или оригинальным в использовании квадратных скобок; другие языки, такие как Scala, также используют квадратные скобки для обобщённого кода.

Почему Go не поддерживает методы с параметрами типов?

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

Проблема заключается в том, как их реализовать. В частности, рассмотрим проверку того, реализует ли значение в интерфейсе другой интерфейс с дополнительными методами. Например, рассмотрим такой тип — пустая структура с дженерик-методом Nop, который возвращает свой аргумент для любого возможного типа:

<code>type Empty struct{}
func (Empty) Nop[T any](x T) T {
  return x
}
</code>

Теперь предположим, что значение Empty хранится в any и передаётся в другой код, который проверяет, что он может делать:

<code>func TryNops(x any) {
  if x, ok := x.(interface{ Nop(string) string }); ok {
    fmt.Printf("string %s\n", x.Nop("hello"))
  }
  if x, ok := x.(interface{ Nop(int) int }); ok {
    fmt.Printf("int %d\n", x.Nop(42))
  }
  if x, ok := x.(interface{ Nop(io.Reader) io.Reader }); ok {
    data, err := io.ReadAll(x.Nop(strings.NewReader("hello world")))
    fmt.Printf("reader %q %v\n", data, err)
  }
}
</code>

Как работает этот код, если x — это Empty? Кажется, что x должен удовлетворять всем трём тестам, а также любым другим формам с любыми другими типами.

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

Существует четыре варианта:

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

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

  2. Реализовать какой-либо вид JIT, компилируя необходимый код метода во время выполнения.

    Go сильно выигрывает от простоты и предсказуемой производительности, обеспечиваемой полностью ahead-of-time компиляцией. Мы не хотим добавлять сложность JIT-компилятора только ради реализации одной языковой особенности.

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

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

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

    Интерфейсы являются неотъемлемой частью программирования на Go. Запрет дженерик-методов на удовлетворение интерфейсов неприемлем с точки зрения дизайна.

Ни один из этих вариантов не является хорошим, поэтому мы выбрали «ни один из вышеуказанных».

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

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

Почему нельзя использовать более конкретный тип для получателя параметризованного типа?

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

<code>type S[T any] struct { f T }
func (s S[string]) Add(t string) string {
  return s.f + t
}
</code>

Это не работает, потому что слово string интерпретируется компилятором как имя параметра типа в методе. Сообщение об ошибке компилятора будет примерно таким: «operator + not defined on s.f (variable of type string)». Это может быть запутывающим, потому что оператор + отлично работает с предварительно объявленным типом string, но объявление переопределяет определение string только для этого метода, и оператор не работает с этим несвязанным вариантом string. Переопределение предварительно объявленного имени, как это, допустимо, но является странным и часто ошибочным действием.

Почему компилятор не может вывести аргумент типа в моей программе?

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

Пакеты и тестирование

Как создать многофайловый пакет?

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

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

Как написать модульный тест?

Создайте новый файл, оканчивающийся на _test.go, в том же каталоге, что и исходные файлы пакета. Внутри этого файла выполните import "testing" и напишите функции следующего вида

<code>func TestFoo(t *testing.T) {
  ...
}
</code>

Запустите go test в этом каталоге. Этот скрипт находит функции Test, создаёт исполняемый файл теста и запускает его.

См. документ How to Write Go Code, пакет testing и подкоманду go test для получения дополнительных сведений.

Где моей любимой вспомогательной функции для тестирования?

Стандартный пакет testing в Go позволяет легко писать модульные тесты, но он не предоставляет функций, доступных в других языках тестирования, таких как функции утверждения. Предыдущий раздел этого документа объяснял, почему в Go нет утверждений, и те же аргументы применимы к использованию assert в тестах. Правильная обработка ошибок подразумевает, что другие тесты продолжат выполнение после неудачного, чтобы человек, отлаживающий ошибку, получил полную картину того, что пошло не так. Более полезно, чтобы тест сообщал, что isPrime даёт неверный результат для 2, 3, 5 и 7 (или для 2, 4, 8 и 16), чем сообщать, что isPrime даёт неверный результат для 2 и поэтому больше тестов не выполнялись. Программист, вызвавший сбой теста, может быть не знаком с кодом, который не прошёл проверку. Время, затраченное на написание хорошего сообщения об ошибке, окупается позже, когда тест перестаёт работать.

Связанная мысль заключается в том, что тестовые фреймворки часто становятся мини-языками своими собственными, с условными операторами, управляющими конструкциями и механизмами печати, но в Go уже есть все эти возможности; зачем их повторять? Мы предпочитаем писать тесты на Go; это один язык меньше для изучения, и такой подход делает тесты простыми и понятными.

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

Почему X отсутствует в стандартной библиотеке?

Целью стандартной библиотеки является поддержка runtime-библиотеки, взаимодействие с операционной системой и предоставление ключевой функциональности, необходимой многим программам на Go, такой как форматированный ввод-вывод и сетевые функции. Она также содержит элементы, важные для веб-программирования, включая криптографию и поддержку стандартов, таких как HTTP, JSON и XML.

Не существует чёткого критерия, который определяет, что включено в стандартную библиотеку, потому что долгое время это была единственная библиотека Go. Однако существуют критерии, определяющие, что добавляется в стандартную библиотеку сегодня.

Новые дополнения в стандартную библиотеку редки, и уровень требований к включению высок. Код, включённый в стандартную библиотеку, несёт значительную стоимость постоянного сопровождения (часто несётся теми, кто не является первоначальным автором), подлежит обещанию совместимости Go 1 (блокировка исправлений любых недостатков в API), а также подлежит графику выпусков Go, что препятствует быстрому предоставлению исправлений пользователям.

Большинство нового кода должны находиться за пределами стандартной библиотеки и быть доступным через команду go get инструмента go. Такой код может иметь своих собственных сопровождающих, цикл выпусков и гарантии совместимости. Пользователи могут находить пакеты и читать их документацию на pkg.go.dev.

Хотя в стандартной библиотеке есть элементы, которые на самом деле не должны туда входить, например log/syslog, мы продолжаем поддерживать всё содержимое библиотеки из-за обещания совместимости Go 1. Однако мы рекомендуем большинство новых проектов размещать вне стандартной библиотеки.

Реализация

Какая технология компилятора используется для создания компиляторов?

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

Стандартный компилятор, gc, включён в дистрибутив Go как часть поддержки команды go. Gc изначально был написан на C из-за сложностей с bootstrapping (необходимость компилятора Go для настройки среды Go). Но со временем ситуация изменилась, и начиная с версии Go 1.5 компилятор стал программой на Go. Компилятор был преобразован из C в Go с использованием автоматических инструментов перевода, как описано в этом документе проектирования и презентации. Таким образом, компилятор теперь является «самовоспроизводящимся», что означает, что мы столкнулись с проблемой bootstrapping. Решение заключается в том, чтобы уже существующая рабочая среда Go была установлена, как это обычно бывает с рабочей установкой C. История того, как создать новую среду Go из исходного кода, описана здесь и здесь.

Gc написан на Go с использованием рекурсивного спускающего анализатора и использует собственный загрузчик, также написанный на Go, но основанный на загрузчике Plan 9, для генерации ELF/Mach-O/PE бинарных файлов.

Компилятор Gccgo представляет собой фронтенд, написанный на C++, с рекурсивным спускающим анализатором, совместимым со стандартным бэкендом GCC. Экспериментальный LLVM бэкенд использует тот же фронтенд.

В начале проекта рассматривалось использование LLVM для gc, но было решено, что это слишком велико и медленно для достижения наших целей по производительности. Более важным оказалось то, что начало с LLVM сделало бы сложнее внедрение некоторых изменений ABI и связанных с ними, таких как управление стеком, которые требует Go, но не являются частью стандартной настройки C.

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

Хотя у gc есть собственная реализация, нативный лексер и парсер доступны в пакете go/parser, а также есть нативный проверщик типов. Компилятор gc использует варианты этих библиотек.

Как реализована поддержка среды выполнения?

Снова из-за проблем бутстраппинга код среды выполнения изначально был написан в основном на C (с небольшим количеством ассемблерных вставок), но затем был переведен на Go (кроме некоторых ассемблерных частей). Поддержка среды выполнения gccgo использует glibc. Компилятор gccgo реализует горутины с использованием техники, называемой сегментированные стеки, с поддержкой недавних изменений в линкере gold. Gollvm построен аналогично на соответствующей инфраструктуре LLVM.

Почему мой тривиальный программный файл такой большой бинарный файл?

Линкер в инструментарии gc по умолчанию создаёт статически связанные бинарные файлы. Поэтому все бинарные файлы Go включают среду выполнения Go, а также информацию о типах среды выполнения, необходимую для поддержки динамических проверок типов, рефлексии и даже трассировок стека во время паники.

Простая C-программа «hello, world», скомпилированная и статически связываемая с использованием gcc в Linux, составляет около 750 кБ, включая реализацию printf. Эквивалентная программа на Go, использующая fmt.Printf, весит пару мегабайт, но она включает более мощную поддержку среды выполнения, информацию о типах и отладку.

Программа на Go, скомпилированная с помощью gc, может быть связана с флагом -ldflags=-w, чтобы отключить генерацию DWARF, удаляя отладочную информацию из бинарного файла, но без потери функциональности. Это может значительно уменьшить размер бинарного файла.

Можно ли избавиться от этих предупреждений о неиспользуемых переменных/импортах?

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

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

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

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

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

<code>import "unused"
// Эта декларация помечает импорт как используемый, ссылаясь на элемент из пакета.
var _ = unused.Item  // TODO: Удалить перед коммитом!
func main() {
  debugData := debug.Profile()
  _ = debugData // Используется только во время отладки.
  ....
}
</code>

Сегодня большинство программистов на Go используют инструмент, goimports, который автоматически переписывает исходный файл Go, чтобы иметь корректные импорты, устраняя проблему неиспользуемых импортов на практике. Эта программа легко подключается ко многим редакторам и IDE, чтобы запускаться автоматически при записи исходного файла Go. Эта функциональность также встроена в gopls, как обсуждалось выше.

Почему программа сканирования на вирусы считает мою дистрибутивную сборку Go или скомпилированный бинарный файл заражённым?

Это часто происходит, особенно на машинах под управлением Windows, и почти всегда является ложным срабатыванием. Коммерческие программы сканирования на вирусы часто путаются из-за структуры бинарных файлов Go, которые они не видят так часто, как бинарные файлы, скомпилированные из других языков.

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

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

Производительность

Почему Go работает плохо на тесте производительности X?

Одной из целей проектирования Go является приближение к производительности C для аналогичных программ, однако на некоторых тестах она работает довольно плохо, включая несколько в golang.org/x/exp/shootout. Самые медленные из-за библиотек, для которых версии с аналогичной производительностью недоступны в Go. Например, pidigits.go зависит от пакета многократной точности, а версии на C, в отличие от Go, используют GMP (написанный на оптимизированном ассемблере). Тесты, зависящие от регулярных выражений (regex-dna.go, например), в основном сравнивают нативный regexp пакет Go с зрелыми, высокооптимизированными библиотеками регулярных выражений, такими как PCRE.

В тестах производительности выигрывают тщательная настройка, и версии большинства тестов на Go нуждаются в доработке. Если вы проведёте сравнительное измерение действительно сопоставимых программ на C и Go (reverse-complement.go — один из примеров), вы увидите, что два языка отличаются по производительности намного меньше, чем показывает эта сборка тестов.

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

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

Изменения по сравнению с C

Почему синтаксис сильно отличается от C?

За исключением синтаксиса объявления, различия не являются значительными и происходят из двух желаний. Во-первых, синтаксис должен быть лёгким, без лишнего количества обязательных ключевых слов, повторений или архаизмов. Во-вторых, язык был спроектирован так, чтобы быть простым для анализа и его можно было разбирать без таблицы символов. Это делает намного проще создавать такие инструменты, как отладчики, анализаторы зависимостей, автоматические извлекатели документации, плагины IDE и так далее. C и его потомки известны своей сложностью в этом плане.

Почему объявления выглядят задом наперёд?

Они кажутся задом наперёд, только если вы привыкли к C. В C концепция заключается в том, что переменная объявляется как выражение, обозначающее её тип, что является хорошей идеей, но грамматики выражений и типов плохо сочетаются, и результат может быть запутанным; рассмотрим указатели на функции. Go в основном разделяет синтаксис выражений и типов, что упрощает всё (использование префикса * для указателей — исключение, которое подтверждает правило). В C объявление

<code>    int* a, b;
</code>

объявляет a как указатель, но не b; в Go

<code>    var a, b *int
</code>

объявляет оба как указатели. Это более ясно и регулярно. Также, короткая форма объявления := подразумевает, что полное объявление переменной должно следовать тому же порядку, что и :=, поэтому

<code>    var a uint64 = 1
</code>

имеет тот же эффект, что и

<code>    a := uint64(1)
</code>

Синтаксический анализ также упрощается благодаря наличию отдельной грамматики для типов, отличной от грамматики выражений; ключевые слова, такие как func и chan, помогают сохранить ясность.

См. статью о синтаксисе объявления в Go для получения дополнительных сведений.

Почему в Go нет арифметики указателей?

Безопасность. Без арифметики указателей возможно создать язык, который никогда не сможет вывести недопустимый адрес, который будет успешно выполнен некорректно. Технологии компилятора и аппаратного обеспечения достигли такого уровня, что цикл с использованием индексов массива может быть такой же эффективной, как и цикл с использованием арифметики указателей. Кроме того, отсутствие арифметики указателей может упростить реализацию сборщика мусора.

Почему операторы ++ и -- являются инструкциями, а не выражениями? И почему постфиксная форма, а не префиксная?

Без арифметики указателей удобство использования префиксных и постфиксных операторов инкремента снижается. Убрав их из иерархии выражений полностью, мы упрощаем синтаксис выражений, а также избавляемся от сложных вопросов, связанных с порядком вычисления значений ++ и -- (рассмотрите f(i++) и p[i] = q[++i]). Упрощение имеет значительное значение. Что касается постфиксной формы против префиксной, то любая форма будет работать нормально, но постфиксная форма более традиционна; настойчивость в использовании префиксной формы возникла с STL — библиотекой для языка, имя которого, к слову, содержит постфиксный инкремент.

Почему в Go используются фигурные скобки, но нет точек с запятой? И почему нельзя разместить открывающуюся скобку на следующей строке?

Go использует фигурные скобки для группировки инструкций — синтаксис знаком программистам, работавшим с языками семейства C. Однако точки с запятой предназначены для парсеров, а не для людей, и мы стремились максимально их избавиться. Для достижения этой цели Go заимствует трюк из BCPL: точки с запятой, разделяющие инструкции, находятся в формальной грамматике, но автоматически вставляются лексером в конце любой строки, которая может быть концом инструкции, без предварительного просмотра. Это работает очень хорошо на практике, но имеет эффект принудительного стиля использования скобок. Например, открывающаяся скобка функции не может находиться на отдельной строке.

Некоторые утверждали, что лексер должен выполнять предварительный просмотр, чтобы разрешить размещение скобки на следующей строке. Мы не согласны. Поскольку код на Go должен быть автоматически отформатирован с помощью gofmt, некоторый стиль должен быть выбран. Этот стиль может отличаться от того, который вы использовали в C или Java, но Go — это другой язык, и стиль gofmt такой же хороший, как и любой другой. Более важно — гораздо более важно — преимущества единого программно заданного формата для всех программ на Go значительно перевешивают любые воспринимаемые недостатки конкретного стиля. Обратите внимание также на то, что стиль Go означает, что интерактивная реализация Go может использовать стандартный синтаксис построчно без специальных правил.

Зачем использовать сборку мусора? Не будет ли это слишком затратно?

Одним из самых больших источников бюрократии в системных программах является управление временем жизни выделенных объектов. В языках, таких как C, где это делается вручную, это может потреблять значительное количество времени программиста и часто является причиной серьезных ошибок. Даже в языках, таких как C++ или Rust, которые предоставляют механизмы помощи, эти механизмы могут оказывать значительное влияние на дизайн программного обеспечения, часто добавляя собственную программную нагрузку. Мы считали необходимым устранить такие программные издержки, и достижения в области технологии сборки мусора за последние несколько лет дали нам уверенность, что она может быть реализована достаточно дешево и с низкой задержкой, что позволит использовать такой подход в сетевых системах.

Многие трудности параллельного программирования имеют корни в проблеме управления временем жизни объектов: по мере того как объекты передаются между потоками, становится неудобно гарантировать их безопасное освобождение. Автоматическая сборка мусора делает параллельный код гораздо проще для написания. Конечно, реализация сборки мусора в параллельной среде — это сама по себе задача, но решение её один раз, а не в каждом программном обеспечении, помогает всем.

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

Это не означает, что недавние работы в языках, таких как Rust, которые вносят новые идеи в проблему управления ресурсами, являются ошибочными; мы поощряем такие работы и рады видеть, как они развиваются. Но Go использует более традиционный подход, решая проблемы времени жизни объектов через сборку мусора и только через неё.

Текущая реализация представляет собой маркирующий и очищающий сборщик. Если машина является многоядерной, сборщик работает на отдельном CPU-ядре параллельно с основной программой. Значительная работа над сборщиком в последние годы позволила сократить время пауз до подмиллисекундного уровня, даже для больших куч, почти устраняя один из основных возражений против сборки мусора в сетевых серверах. Продолжаются работы по совершенствованию алгоритма, снижению нагрузки и задержек, а также исследованию новых подходов. Доклад на конференции ISMM от Рика Хадсона из команды Go описывает достигнутые результаты и предлагает некоторые перспективные направления развития.

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

GoRu.dev Golang на русском

На сайте представлена адаптированная под русский язык документация языка программирования Golang