Go
Go | |
---|---|
Класс языка | многопоточный, императивный, структурированный |
Тип исполнения | компилируемый |
Появился в | 10 ноября 2009[2][3] |
Автор | Роберт Гризмер, Роб Пайк и Кен Томпсон |
Разработчик | Google, Роб Пайк, Кен Томпсон, The Go Authors[вд] и Роберт Гризмер[вд] |
Расширение файлов |
.go |
Выпуск | |
Система типов | строгая, статическая, с выводом типов |
Испытал влияние | Си[4], Оберон-2, Limbo[5], Active Oberon, теория взаимодействия последовательных процессов[5], Паскаль[4], Оберон[4], Smalltalk[6], Newsqueak[вд][7], Модула-2[7], Alef[вд], APL[8], BCPL, Модула и Оккам |
Лицензия | BSD |
Сайт | go.dev (англ.) |
ОС | DragonFly BSD, FreeBSD, Linux, macOS, NetBSD, OpenBSD, Plan 9, Solaris, Windows, iOS, Android, AIX и Illumos |
Медиафайлы на Викискладе |
Go (часто также golang) — компилируемый многопоточный язык программирования, разработанный внутри компании Google[9]. Разработка Go началась в сентябре 2007 года, его непосредственным проектированием занимались Роберт Гризмер, Роб Пайк и Кен Томпсон[10], занимавшиеся до этого проектом разработки операционной системы Inferno. Официально язык был представлен в ноябре 2009 года. На данный момент поддержка официального компилятора, разрабатываемого создателями языка, осуществляется для операционных систем FreeBSD, OpenBSD, Linux, macOS, Windows, DragonFly BSD, Plan 9, Solaris, Android, AIX.[11]. Также Go поддерживается набором компиляторов gcc, существует несколько независимых реализаций. Ведётся разработка второй версии языка.
Название
Название языка, выбранное компанией Google, практически совпадает с названием языка программирования Go!, созданного Ф. Джи. МакКейбом и К. Л. Кларком в 2003 году[12]. Обсуждение названия ведётся на странице, посвящённой Go[12].
На домашней странице языка и вообще в Интернет-публикациях часто используется альтернативное название — «golang».
Назначение, идеология
Язык Go разрабатывался как язык программирования для создания высокоэффективных программ, работающих на современных распределённых системах и многоядерных процессорах. Он может рассматриваться как попытка создать замену языкам Си и C++ с учётом изменившихся компьютерных технологий и накопленного опыта разработки крупных систем[13]. По словам Роба Пайка[13], «Go был разработан для решения реальных проблем, возникающих при разработке программного обеспечения в Google». В качестве основных таких проблем он называет:
- медленную сборку программ;
- неконтролируемые зависимости;
- использование разными программистами разных подмножеств языка;
- затруднения с пониманием программ, вызванные неудобочитаемостью кода, плохим документированием и так далее;
- дублирование разработок;
- высокую стоимость обновлений;
- несинхронные обновления при дублировании кода;
- сложность разработки инструментария;
- проблемы межъязыкового взаимодействия.
Основными требованиями к языку стали[14]:
- Ортогональность. Язык должен предоставлять небольшое число средств, не повторяющих функциональность друг друга.
- Простая и регулярная грамматика. Минимум ключевых слов, простая, легко разбираемая грамматическая структура, легко читаемый код.
- Простая работа с типами. Типизация должна обеспечивать безопасность, но не превращаться в бюрократию, лишь увеличивающую код. Отказ от иерархии типов, но с сохранением объектно-ориентированных возможностей.
- Отсутствие неявных преобразований.
- Сборка мусора.
- Встроенные средства распараллеливания, простые и эффективные.
- Поддержка строк, ассоциативных массивов и коммуникационных каналов.
- Чёткое разделение интерфейса и реализации.
- Эффективная система пакетов с явным указанием зависимостей, обеспечивающая быструю сборку.
Go создавался в расчёте на то, что программы на нём будут транслироваться в объектный код и исполняться непосредственно, не требуя виртуальной машины, поэтому одним из критериев выбора архитектурных решений была возможность обеспечить быструю компиляцию в эффективный объектный код и отсутствие чрезмерных требований к динамической поддержке.
В результате получился язык, «который не стал прорывом, но тем не менее явился отличным инструментом для разработки крупных программных проектов»[13].
Хотя для Go доступен и интерпретатор, практически в нём нет большой потребности, так как скорость компиляции достаточно высока для обеспечения интерактивной разработки.
Основные возможности языка
Основные возможности языка Go[10]:
- Go — язык со строгой статической типизацией. Доступен автоматический вывод типов, для пользовательских типов — «утиная типизация».
- Полноценная поддержка указателей, но без возможности применять к ним арифметические операции, в отличие от C/C++/D.
- Строковый тип со встроенной поддержкой юникода.
- Использование динамических массивов, хеш-таблиц (словарей), срезов (слайсов), вариант цикла для обхода коллекции.
- Средства функционального программирования: неименованные функции, замыкания, передача функций в параметрах и возврат функциональных значений.
- Автоматическое управление памятью со сборщиком мусора.
- Средства объектно-ориентированного программирования ограничиваются интерфейсами. Полиморфное поведение обеспечивается реализацией интерфейсов типами. Наследование реализации отсутствует, но типы-структуры могут включать другие типы-структуры в себя.
- Средства параллельного программирования: встроенные в язык потоки (go routines), взаимодействие потоков через каналы и другие средства организации многопоточных программ.
- Достаточно лаконичный и простой синтаксис, основанный на Си, но существенно доработанный, с большим количеством синтаксического сахара.
Go не содержит целого ряда популярных синтаксических средств, доступных в других современных языках прикладного программирования. Во многих случаях это вызвано сознательным решением разработчиков. Краткие обоснования выбранных проектных решений можно найти в «Часто задаваемых вопросах»[10] по языку, более подробные — в опубликованных на сайте языка статьях и обсуждениях, рассматривающих различные варианты дизайна. В частности:
- Структурная запись обработчиков исключений сочтена провоцирующей на пропуск ошибок или неадекватную их обработку. К тому же поддержка исключений серьёзно усложняется в приложениях с параллельно работающими частями. Вместо неё предлагается проверка кодов возврата с использованием многозначных функций и специального интерфейса error, а также применение отложенных (deferred) функций для перехвата исключительных ситуаций.
- Наследование реализации, как считают авторы, приводит к созданию кода с неявными зависимостями, избыточно сложного в поддержке. Аналогичные возможности, но без свойственных наследованию нежелательных эффектов, обеспечиваются поддержкой вложения типов и свободно определяемыми интерфейсами.
- Обобщённое программирование. Авторы воздержались от его включения в первую версию языка, поскольку, по их словам[10], предоставляемые им возможности не окупают требуемого усложнения компилятора и runtime-библиотек, а уже имеющиеся в языке средства (пустые интерфейсы, «утиная типизация» и рефлексия) позволяют создавать обобщённый код без специальных синтаксических механизмов. Тем не менее, обсуждается вопрос о включении таких средств в проектируемую вторую версию языка .
- Использование утверждений (assertion) было сочтено ненужным.
- Переопределение методов и функций было исключено из соображений надёжности и эффективности компиляции: требование различного именования всех методов на одном уровне видимости устраняет необходимость сопоставлять списки параметров при компиляции вызовов функций и методов и исключает ошибочный вызов другого одноимённого метода; при этом сама возможность переопределения есть не более чем синтаксический сахар.
- Ряд операций над массивами и срезами (например, вставка элемента в середину) не включен в язык, поскольку они достаточно затратны. Возможность их выполнения одной простой командой провоцировала бы программиста на создание неэффективного кода, отсутствие таких команд в языке, напротив, является стимулом для рассмотрения альтернативных решений.
- Поддержка отрицательных индексов,[15] доступная в ряде популярных языков, может стать причиной труднообнаруживаемых ошибок: появление отрицательного индекса из-за ошибки в коде вместо того, чтобы привести к фатальному сбою, вызовет внешне корректное обращение не к тем элементам массива, что проявится только в неверных результатах и может быть обнаружено далеко не сразу.
- Принцип «любое выражение возвращает значение» провоцирует программиста на создание сложных, трудно воспринимаемых и чреватых неочевидными ошибками выражений (вроде копирования строки на Си командой из трёх слов:
while (*ptr1++ = *ptr2++);
). При этом современные технологии оптимизации обеспечат одинаковый объектный код и для экстремально сокращённого выражения, и для аналогичного ему фрагмента, написанного безо всяких ухищрений.
Синтаксис
Синтаксис языка Go схож с синтаксисом языка Си, с отдельными элементами, заимствованными из Оберона и скриптовых языков.
Алфавит
Go — регистрозависимый язык с полной поддержкой Юникода в строках и идентификаторах.
Идентификатор традиционно может быть любой непустой последовательностью, включающей буквы, цифры и знак подчёркивания, начинающийся с буквы и не совпадающий ни с одним из ключевых слов языка Go. При этом под «буквами» понимаются все символы Юникода, относящиеся к категориям «Lu» (буквы верхнего регистра), «Ll» (буквы нижнего регистра), «Lt» (заглавные буквы), «Lm» (буквы-модификаторы) или «Lo» (прочие буквы), под «цифрами» — все символы из категории «Nd» (числа, десятичные цифры). Таким образом, ничто не мешает использовать в идентификаторах, например, кириллицу.
Идентификаторы, различающиеся только регистром букв, являются различными. В языке существует ряд соглашений об использовании заглавных и строчных букв. В частности, в именах пакетов используются только строчные буквы. Все ключевые слова Go пишутся в нижнем регистре. Переменные, начинающиеся с заглавных букв, являются экспортируемыми (public), а начинающиеся со строчных букв — неэкспортируемыми (private).
В строковых литералах могут использоваться все символы Юникода без ограничений. Строки представляются как последовательности символов в кодировке UTF-8.
Пакеты
Любая программа на Go включает один или несколько пакетов. Пакет, к которому относится файл исходного кода, задаётся описанием package в начале файла. Имена пакетов имеют те же ограничения, что и идентификаторы, но могут содержать буквы только нижнего регистра. Система пакетов go-среды имеет древовидную структуру, аналогичную дереву каталогов. Любые глобальные объекты (переменные, типы, интерфейсы, функции, методы, элементы структур и интерфейсов) доступны без ограничений в пакете, в котором они объявлены. Глобальные объекты, имена которых начинаются на заглавную букву, являются экспортируемыми.
Для использования в файле кода Go объектов, экспортированных другим пакетом, пакет должен быть импортирован, для чего применяется конструкция import
.
package main
/* Импорт */
import (
"fmt" // Стандартный пакет для форматированного вывода
"database/sql" // Импорт вложенного пакета
w "os" // Импорт с псевдонимом
. "math" // Импорт без квалификации при использовании
_ "gopkg.in/goracle.v2" // Пакет не имеет явных обращений в коде
)
func main() {
for _, arg := range w.Args { // Обращение к массиву Args, объявленному в пакете "os", через псевдоним
fmt.Println(arg) // Обращение к функции Println(), объявленной в пакете "fmt", с именем пакета
}
var db *sql.DB = sql.Open(driver, dataSource) // Имена из вложенного пакета квалифицируются
// только именем самого пакета (sql)
x := Sin(1.0) // вызов math.Sin() - квалификация именем пакета math не нужна,
// так как он импортирован без имени
// Обращений к пакету "goracle.v2" в коде нет, но он будет импортирован.
}
В ней перечисляются пути к импортируемым пакетам от каталога src в дереве исходных текстов, положение которого задаётся переменной среды GOPATH
, а для стандартных пакетов достаточно указать имя. Перед строкой, идентифицирующей пакет, может быть указан псевдоним, тогда он будет использоваться в коде вместо имени пакета. Импортированные объекты доступны в импортирующем их файле с полной квалификацией вида «пакет.Объект
». Если при импорте пакета вместо псевдонима указывается точка, то все экспортируемые им имена будут доступны без квалификации. Эта возможность используется некоторыми системными утилитами, однако её применение программистом не рекомендуется, так как явная квалификация обеспечивает защиту от коллизий имён и «незаметного» изменения поведения кода. Невозможно импортировать без квалификации два пакета, экспортирующих одно и то же имя.
Импорт пакетов в Go строго контролируется: если пакет импортирован модулем, то в коде данного модуля должно использоваться хотя бы одно экспортируемое этим пакетом имя. Компилятор Go считает импорт неиспользуемого пакета ошибкой; такое решение вынуждает разработчика постоянно поддерживать актуальность списков импорта. Затруднений это не создаёт, так как средства поддержки программирования на Go (редакторы, IDE), как правило, обеспечивают автоматическую проверку и актуализацию списков импорта.
Когда пакет содержит код, используемый только посредством интроспекции, возникает проблема: импорт такого пакета необходим для включения его в состав программы, но не будет разрешён компилятором, так как к нему не обращаются напрямую. Для таких случаев предусмотрен анонимный импорт: в качестве псевдонима указывается «_
» (одиночный знак подчёркивания); пакет, импортированный таким образом, будет откомпилирован и включён в состав программы при отсутствии явных ссылок на него в коде. Такой пакет, однако, не может быть использован явно; это не позволяет обойти контроль импорта, импортируя все пакеты как анонимные.
Исполняемая программа на Go обязательно содержит пакет с именем main, в котором обязательно должна быть функция main()
без параметров и возвращаемого значения. Функция main.main()
является «телом программы» — её код запускается, когда программа стартует. Любой пакет может содержать функцию init()
— она будет запущена при загрузке программы перед началом её исполнения, до вызова любой функции в данном пакете и в любом пакете, импортирующем данный. Инициализация пакета main всегда происходит последней, и все инициализации выполняются до начала исполнения функции main.main()
.
Модули
Система пакетов Go была разработана в предположении, что вся экосистема разработки существует в виде единого файлового дерева, содержащего актуальные версии всех пакетов, а при появлении новых версий она целиком перекомпилируется. Для прикладного программирования с использованием сторонних библиотек это достаточно сильное ограничение. В реальности часто возникают ограничения по версиям пакетов, используемых тем или иным кодом, а также ситуации, когда разные версии (ветви) одного проекта используют разные версии библиотечных пакетов.
Начиная с версии 1.11 в Go поддерживаются так называемые модули. Модуль — это специальным образом описанный пакет, содержащий информацию о своей версии. При импорте модуля фиксируется версия, которая была использована. Это позволяет системе сборки контролировать, удовлетворены ли все зависимости, автоматически обновлять импортированные модули, когда автор вносит в них совместимые изменения, и блокировать обновление до версий, не обеспечивающих обратной совместимости. Предполагается, что модули станут решением (или значительно облегчат решение) проблемы с контролем зависимостей.
Комментарии и точки с запятой
Go использует оба типа комментариев в стиле Си: строчные (начинающиеся с // …) и блочные (/* … */). Строчный комментарий рассматривается компилятором как перевод строки. Блочный, располагающийся на одной строке — как пробел, на нескольких строках — как перевод строки.
Точка с запятой в Go используется в качестве обязательного разделителя в некоторых операциях (if, for, switch). Формально также она должна завершать каждую команду, но практически ставить такую точку с запятой в конце строки нет необходимости, так как компилятор в процессе обработки кода сам добавляет точки с запятой в конец каждой строки, которая, без учёта пустых символов, завершается идентификатором, числом, символьным литералом, строкой, ключевыми словами break, continue, fallthrough, return, командой инкремента или декремента (++ или --) или закрывающей круглой, квадратной или фигурной скобкой (важное исключение — запятая в приведённый список не входит). Из этого следует две особенности:
- Практически точка с запятой нужна только в некоторых форматах операторов if, for, switch и для разделения команд, расположенных на одной строке. Поэтому в коде на Go точек с запятой очень мало.
- Побочным эффектом автоматической расстановки точек с запятой компилятором стало то, что не в любом месте программы, где допустим пробел, можно использовать перенос строки. В частности, в описаниях, командах инициализации и конструкциях if, for, switch нельзя переносить открывающуюся фигурную скобку на следующую строку:
func g() // ! { // НЕВЕРНО } if x { } // ! else { // НЕВЕРНО } func g(){ // ВЕРНО } if x { } else { // ВЕРНО }
- Здесь в двух первых случаях компилятор вставит точку с запятой в строке, помеченной комментарием с восклицательным знаком, так как строка заканчивается (без учёта пробелов и комментария), соответственно, на круглую и фигурную закрывающиеся скобки. В результате будет нарушен синтаксис объявления функции в первом случае и условного оператора — во втором.
- Аналогично нельзя в списке элементов, разделённых запятыми, переносить запятую на следующую строку:
func f(i // ! , k int // ! , s // ! , t string) string { // НЕВЕРНО } func f(i, k int, s, t string) string { // ВЕРНО }
- При переносе запятой на следующую строку текущая строка заканчивается идентификатором и в её конце автоматически ставится точка с запятой, что нарушает синтаксис списка (запятая, как уже говорилось выше — исключение из правила, после неё точка с запятой компилятором не добавляется).
- Таким образом, язык диктует определённый стиль записи кода. В комплект компилятора Go входит утилита gofmt, обеспечивающая правильное и единообразное форматирование исходных текстов. Все тексты стандартной библиотеки Go отформатированы этой утилитой.
Встроенные типы данных
Язык содержит достаточно стандартный набор простых встроенных типов данных: целые числа, числа с плавающей запятой, символы, строки, логические значения, а также несколько специальных типов.
Целые числа
Имеется 11 целочисленных типов:
- Целые числа со знаком фиксированного размера —
int8
,int16
,int32
,int64
. Это целые числа со знаком, представленные в дополнительном коде, размер значений этих типов — 8, 16, 32, 64 бита соответственно. Диапазон значений составляет от −2n−1 до 2n−1−1, где n — размер типа. - Целые числа без знака фиксированного размера —
uint8
,uint16
,uint32
,uint64
. Число в названии типа, как и в предыдущем случае, задаёт размер, но диапазон значений составляет от 0 до 2n−1. int
иuint
— соответственно, знаковое и беззнаковое целое число. Размер этих типов одинаков, и может быть 32 или 64 бита, но не фиксируется спецификацией языка и может выбираться реализацией. Предполагается, что для них будет выбран наиболее эффективный на целевой платформе размер.byte
— синонимuint8
. Предназначается, как правило, для работы с неформатированными бинарными данными.rune
— синонимuint32
, представляет символ в кодировке Unicode.uintptr
— целое беззнаковое значение, размер которого определяется реализацией, но должен быть достаточным для размещения в переменной этого типа полного значения указателя для целевой платформы.
Создатели языка рекомендуют для работы с числами внутри программы использовать по возможности только стандартный тип int
. Типы с фиксированными размерами предназначены для работы с данными, получаемыми из внешних источников или передаваемыми в них, когда для корректности кода важно указать конкретный размер типа. Типы-синонимы byte
и rune
предназначены для работы с бинарными данными и символами, соответственно. Тип uintptr
необходим только для взаимодействия с внешним кодом, например, на Си.
Числа с плавающей запятой
Числа с плавающей точкой представлены двумя типами, float32
и float64
. Их размер, соответственно, 32 и 64 бита, реализация соответствует стандарту IEEE 754. Диапазон значений можно получить из стандартного пакета math
.
Числовые типы с неограниченной точностью
Также стандартная библиотека Go содержит пакет big
, который предоставляет три типа с неограниченной точностью: big.Int
, big.Rat
и big.Float
, представляющие, соответственно, целые числа, рациональные числа и числа с плавающей запятой; размер этих чисел может быть любым и ограничивается только объёмом доступной памяти. Поскольку операторы в Go не перегружаются, вычислительные операции над числами с неограниченной точностью реализованы в виде обычных методов. Производительность вычислений с большими числами, разумеется, значительно уступает встроенным числовым типам, но при решении некоторых типов вычислительных задач использование пакета big
может оказаться предпочтительнее, чем ручная оптимизация математического алгоритма.
Комплексные числа
Язык предоставляет также два встроенных типа для комплексных чисел, complex64
и complex128
. Каждое значение этих типов содержит пару из вещественной и мнимой части, имеющих типы, соответственно, float32
и float64
. Создать в коде значение комплексного типа можно одним из двух способов: либо встроенной функцией complex()
, либо использовав в выражении мнимый литерал. Получить вещественную и мнимую часть комплексного числа можно функциями real()
и imag()
.
var x complex128 = complex(1, 2) // 1 + 2i y := 3 + 4i // 3 + 4i , 4 - число, за которым следует суффикс i, // является мнимым литералом fmt.Println(x * y) // выведет "(-5+10i)" fmt.Println(real(x * y)) // выведет "-5" fmt.Println(imag(x * y)) // выведет "10"
Логические значения
Логический тип bool
вполне обычен — к нему относятся предопределённые значения true
и false
, обозначающие, соответственно, истинность и ложность. В отличие от Си, логические значения в Go не являются числовыми и не могут непосредственно преобразовываться в числа.
Строки
Значения строкового типа string
представляют собой неизменяемые массивы байтов, содержащие текстовые строки в кодировке UTF-8
. Этим обусловлен ряд специфических особенностей строк (например, в общем случае длина строки не равна длине представляющего её массива, т. е. количество содержащихся в ней символов не равно количеству байт в соответствующем ей массиве). Для большинства приложений, которые обрабатывают строки целиком, эта специфика не важна, но в тех случаях, когда программа должна непосредственно обрабатывать конкретные руны (символы Unicode), требуется применение пакета unicode/utf8
, содержащего вспомогательные средства для работы с Unicode-строками.
Объявление типов
Для любых типов данных, включая встроенные, могут объявляться новые типы-аналоги, повторяющие все свойства оригиналов, но несовместимые с ними. Для этих новых типов также могут дополнительно объявляться методы.
Пользовательскими типами данных в Go являются указатели (объявляются при помощи символа *
), массивы (объявляются при помощи квадратных скобок), структуры (struct
), функции (func
), интерфейсы (interface
), отображения (map
) и каналы (chan
). В описаниях этих типов указываются типы и, возможно, идентификаторы их элементов.
Новые типы объявляются с помощью ключевого слова type
:
type PostString string // Тип "строка", аналогичен встроенному type StringArray []string // Тип-массив с элементами строкового типа type Person struct { // Тип-структура name string // поле стандартного типа string post PostString // поле ранее объявленного пользовательского строкового типа bdate time.Time // поле типа Time, импортированного из пакета time edate time.Time chief *Person // поле-указатель infer [](*Person) // поле-массив } type InOutString chan string // тип-канал для передачи строк type CompareFunc func(a, b interface{}) int // тип-функция.
Начиная с версии Go 1.9 также доступно объявление алиасов (псевдонимов) типов:
type TitleString=string // "TitleString" - псевдоним для встроенного типа string type Integer=int64 // "Integer" - псевдоним для встроенного 64-разрядного целого типа
Алиас может быть объявлен как для системного, так и для любого пользовательского типа. Принципиальным отличием алиасов от обычных объявлений типов является то, что при объявлении создаётся новый тип, который не совместим с оригиналом, даже если в объявлении к оригинальному типу никаких изменений не добавляется. Алиас же — это просто другое имя того же типа, то есть алиас и оригинальный тип полностью взаимозаменимы.
Поля структур могут в описании иметь тэги — произвольные последовательности символов, заключённые в обратные кавычки:
// Структура с тэгами полей type XMLInvoices struct { XMLName xml.Name `xml:"INVOICES"` Version int `xml:"version,attr"` Invoice []*XMLInvoice `xml:"INVOICE"` }
Тэги игнорируются компилятором, но информация о них помещается в код и может быть прочитана с помощью функций пакета reflect
, входящего в состав стандартной библиотеки. Обычно тэги используются для обеспечения маршалинга типов для сохранения и восстановления данных на внешних носителях или взаимодействия с внешними системами, получающими или передающими данные в собственных форматах. В примере выше используются тэги, обрабатываемые стандартной библиотекой для чтения и записи данных в формате XML.
Объявление переменных
Синтаксис объявления переменных, в основном, решён в духе Паскаля: объявление начинается с ключевого слова var, за которым через разделитель следует имя переменной, далее, через разделитель — её тип.
Go C++ var v1 int const v2 string var v3 [10]int var v4 []int var v5 struct { f int } var v6 *int /* арифметика указателей не поддерживается */ var v7 map[string]int var v8 func(a int) int
int v1; const std::string v2; /* примерно */ int v3[10]; int* v4; /* примерно */ struct { int f; } v5; int* v6; std::unordered_map v7; /* примерно */ int (*v8)(int a);
Объявление переменной может совмещаться с инициализацией:
var v1 int = 100 var v2 string = "Hello!" var v3 [10]int = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 } var v4 []int = {1000, 2000, 12334} var v5 struct { f int } = { 50 } var v6 *int = &v1 var v7 map[string]int = {"one":1, "two":2, "three":3} var v8 func(a int) int = func(a int) int { return a+1 }
Если при объявлении переменной не производится её явная инициализация, то она автоматически инициализируется «нулевым значением» для данного типа. Нулевым значением для всех числовых типов является 0, для типа string
— пустая строка, для указателей — nil
. Структуры по умолчанию инициализируются наборами из нулевых значений для каждого из входящих в них полей, элементы массивов — нулевыми значениями указанного в определении массива типа.
Объявления можно группировать:
var ( i int m float )
Автоматический вывод типов
Язык Go поддерживает также автоматический вывод типов. Если переменная инициализируется при объявлении, её тип можно не указывать — типом переменной становится тип присваиваемого ей выражения. Для литералов (чисел, символов, строк) стандарт языка определяет конкретные встроенные типы, к которым относится каждое такое значение. Чтобы инициализировать переменную другого типа, к литералу необходимо применить явное преобразование типа.
var p1 = 20 // p1 int - целый литерал 20 имеет тип int. var p2 = uint(20) // p2 uint - значение явно приведено к типу uint. var v1 = &p1 // v1 *int - указатель на p1, для которой выведен тип int. var v2 = &p2 // v2 *uint - указатель на p2, которая явно инициализирована как беззнаковое целое.
Для локальных переменных существует сокращённая форма объявления, совмещённого с инициализацией, с использованием вывода типов:
v1 := 100 v2 := "Hello!" v3 := [10]int{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 } v4 := []int{1000, 2000, 12334} v5 := struct{f int}{50} v6 := &v1
Присваивания
В качестве оператора присваивания в Go используется символ =
:
a = b // Присвоить переменной a значение b
Как уже говорилось выше, существует форма определения переменной с автоматическим выводом типа, совмещённого с инициализацией, внешне напоминающая присваивание в Паскале:
v1 := v2 // аналог var v1 = v2
Компилятор Go строго отслеживает определения и присваивания и отличает одно от другого. Поскольку в одной области видимости запрещено переопределение переменной с тем же именем, в пределах одного блока кода переменная может встретиться слева от знака :=
только один раз:
a := 10 // Объявление и инициализация целой переменной a. b := 20 // Объявление и инициализация целой переменной b. ... a := b // ОШИБКА! Попытка повторного определения a.
Go допускает множественные присваивания, выполняемые параллельно:
i, j = j, i // Поменять местами значения i и j.
При этом количество переменных слева от знака присваивания должно точно соответствовать количеству выражений справа от знака присваивания.
Возможно параллельное присваивание и при использовании оператора :=
. Его особенность в том, что в числе переменных, перечисленных слева от знака :=
, могут быть уже существующие. В этом случае новые переменные будут созданы, уже существующие — использованы повторно. Этот синтаксис часто используется для обработки ошибок:
x, err := SomeFunction() // Функция возвращает два значения (см. ниже), // две переменные объявляются и инициализируются. if (err != nil) { return nil } y, err := SomeOtherFunction() // Здесь объявляется только y, а err просто присваивается значение.
В последней строке примера первое значение, возвращённое функцией, присваивается новой переменной y, второе — уже существующей переменной err
, которая во всём коде используется для размещения последней возвращённой вызываемыми функциями ошибки. Если бы не эта особенность оператора :=
, во втором случае пришлось бы объявлять новую переменную (например, err2
) либо отдельно объявлять y
и далее уже использовать обычное параллельное присваивание.
Go реализует семантику «копирования при присваивании», то есть присваивание приводит к созданию копии значения исходной переменной и размещения этой копии в другой переменной, после чего значения переменных являются различными и при изменении одного из них другое не меняется. Однако это верно только для встроенных скалярных типов, структур и массивов с заданной длиной (то есть для типов, значения которых размещаются в стеке). Массивы с неопределённой длиной и отображения размещаются в куче, переменные этих типов фактически содержат ссылки на объекты, при их присваивании копируется только ссылка, но не сам объект. Иногда это может привести к неожиданным эффектам. Рассмотрим два почти одинаковых примера:
type vector [2]float64 // Длина массива задана явно v1 := vector{10, 15.5} // Инициализация - v1 содержит сам массив v2 := v1 // Массив v1 копируется в массив v2 v2[0] = 25.3 // Изменяется только массив v2 fmt.Println(v1) // Выведет "[10 15.5]" - исходный массив не изменился. fmt.Println(v2) // Выведет "[25.3 15.5]"
Здесь тип vector
определён как массив из двух чисел. Присваивание таких массивов ведёт себя так же, как присваивание чисел и структур.
А в следующем примере код отличается ровно на один символ: тип vector
определён как массив с неопределённым размером. Но ведёт себя этот код совершенно иначе:
type vector []float64 // Массив с неопределённой длиной v1 := vector{10, 15.5} // Инициализация - v1 содержит ссылку на массив v2 := v1 // Ссылка на массив копируется из v1 в v2 v2[0] = 25.3 // Может быть воспринято как изменение только массива v2 fmt.Println(v1) // Выведет "[25.3 15.5]" - исходный массив ИЗМЕНИЛСЯ! fmt.Println(v2) // Выведет "[25.3 15.5]"
Таким же образом, как во втором примере, ведут себя отображения и интерфейсы. Причём если в структуре есть поле ссылочного или интерфейсного типа, или поле — безразмерный массив либо отображение, то при присваивании такой структуры тоже будет скопирована только ссылка, то есть поля разных структур начнут указывать на одни и те же объекты в памяти.
Чтобы избежать такого эффекта, необходимо явно использовать системную функцию copy()
, которая гарантирует создание второго экземпляра объекта.
Аргументы функций и методов
объявляются таким образом:
func f(i, j, k int, s, t string) string { }
Функции могут возвращать несколько значений
Типы таких значений заключаются в скобки:
func f(a, b int) (int, string) { return a+b, "сложение" }
Результаты функций также могут быть именованы:
func incTwo(a, b int) (c, d int) { c = a+1 d = b+1 return }
Именованные результаты считаются описанными сразу после заголовка функции с нулевыми начальными значениями. Оператор return в такой функции может использоваться без параметров, в этом случае после возврата из функции результаты будут иметь те значения, которые были им присвоены в ходе её исполнения. Так, в примере выше функция вернёт пару целых значений, на единицу больших, чем её параметры.
Несколько значений, возвращаемых функциями, присваиваются переменным их перечислением через запятую, при этом количество переменных, которым присваивается результат вызова функции, должно точно совпадать с количеством возвращаемых функцией значений:
first, second := incTwo(1, 2) // first = 2, second = 3 first := incTwo(1, 2) // НЕВЕРНО — нет переменной, которой присваивается второй результат
Псевдопеременная «_»
В отличие от Паскаля и Си, где объявление локальной переменной без её последующего использования или потеря значения локальной переменной (когда присвоенное переменной значение затем нигде не читается) может лишь вызывать предупреждение (warning) компилятора, в Go такая ситуация считается языковой ошибкой и приводит к невозможности компиляции программы. Это означает, в частности, что программист не может проигнорировать значение (или одно из значений), возвращаемое функцией, просто присвоив его какой-нибудь переменной и отказавшись от его дальнейшего использования. Если возникает необходимость игнорировать одно из значений, возвращаемых вызовом функции, используется предопределённая псевдопеременная с именем «_» (один знак подчёркивания). Она может быть указана в любом месте, где должна быть переменная, принимающая значение. Соответствующее значение не будет присвоено никакой переменной и просто потеряется. Смысл такого архитектурного решения — выявление на стадии компиляции возможной потери результатов вычислений: случайный пропуск обработки значения будет обнаружен компилятором, а использование псевдопеременной «_» укажет на то, что программист сознательно проигнорировал результаты. В следующем примере, если из двух возвращаемых функцией incTwo значений нужно только одно, вместо второй переменной нужно указать «_»:
first := incTwo(1, 2) // НЕВЕРНО first, _ := incTwo(1, 2) // ВЕРНО, второй результат не используется
Переменная «_» может указываться в списке присваивания любое число раз. Все результаты функции, которым соответствует «_», будут проигнорированы.
Механизм отложенного вызова defer
Отложенный вызов заменяет сразу несколько синтаксических средств, в частности, обработчики исключений и блоки с гарантированным завершением. Вызов функции, которому предшествует ключевое слово defer, параметризуется в той точке программы, где размещён, а выполняется непосредственно перед выходом программы из области видимости, где он был объявлен, независимо от того, как и по какой причине происходит этот выход. Если в одной функции содержится несколько объявлений defer, соответствующие вызовы выполняются по завершении функции последовательно, в обратном порядке. Ниже пример использования defer в качестве блока гарантированного завершения[16]:
// Функция, копирующая файл func CopyFile(dstName, srcName string) (written int64, err error) { src, err := os.Open(srcName) // Открытие файла-источника if err != nil { // Проверка return // Если неудача, возврат с ошибкой } // Если пришли сюда, то файл-источник был успешно открыт defer src.Close() // Отложенный вызов: src.Close() будет вызван по завершении CopyFile dst, err := os.Create(dstName) // Открытие файла-приёмника if err != nil { // Проверка и возврат при ошибке return } defer dst.Close() // Отложенный вызов: dst.Close() будет вызван по завершении CopyFile return io.Copy(dst, src) // Копирование данных и возврат из функции // После всех операций будут вызваны: сначала dst.Close(), затем src.Close() }
Условия циклов и ветвлений
В отличие от большинства языков с Си-подобным синтаксисом, в Go отсутствуют круглые скобки для условных конструкций for
, if
, switch
:
if i >=0 && i < len(arr) { println(arr[i]) } ... for i := 0; i < 10; i++ { } }
Циклы
В Go для организации всех видов циклов используется циклическая конструкция for
.
for i < 10 { // цикл с предусловием, аналог while в Си } for i := 0; i < 10; i++ { // цикл со счётчиком, аналог for в Си } for { // бесконечный цикл // Выход из цикла должен быть организован вручную, // обычно это делается с помощью конструкций return или break } for { // цикл с постусловием ... // тело цикла if i>=10 { // условие выхода break } } for i, v := range arr { // цикл по коллекции (массиву, срезу, отображению) arr // i - индекс (или ключ) текущего элемента // v - копия значения текущего элемента массива } for i := range arr { // цикл по коллекции, используется только индекс } for _, v := range arr { // цикл по коллекции, используются только значения элементов } for range arr { // Цикл по коллекции без переменных (коллекция используется // только в качестве счётчика итераций). } for v := range c { // цикл по каналу: // в v будут читаться значения из канала c, // пока канал не будет закрыт параллельно исполняющейся // go-процедурой }
Оператор множественного выбора
Синтаксис оператора множественного выбора switch
имеет ряд особенностей. Прежде всего, в отличие от Си, не требуется использование оператора break
: после отработки выбранной ветви исполнение оператора завершается. Если, напротив, необходимо, чтобы после выбранной ветви продолжила обрабатываться следующая, необходимо использовать оператор fallthrough
:
switch value { case 1: fmt.Println("One") fallthrough // Далее будет выполнена ветвь "case 0:" case 0: fmt.Println("Zero") }
Здесь при value==1
будет выведено две строки, «One» и «Zero».
Выражение выбора и, соответственно, альтернативы в операторе switch могут быть любого типа, возможно перечисление нескольких вариантов в одной ветви:
switch chars[code].category { case "Lu", "Ll", "Lt", "Lm", "Lo": ... case "Nd": ... default: ... }
Допускается отсутствие выражения выбора, в этом случае в альтернативах должны быть записаны логические условия. Выполняется первая по счёту ветвь, условие которой истинно:
switch { case '0' <= c && c <= '9': return c - '0' case 'a' <= c && c <= 'f': return c - 'a' + 10 case 'A' <= c && c <= 'F': return c - 'A' + 10 }
Важная деталь: если одна из ветвей с условием заканчивается оператором fallthrough
, то после данной ветви начнёт обрабатываться следующая, независимо от того, выполняется ли её условие. Если нужно, чтобы следующая ветвь обрабатывалась, только если её условие выполняется, нужно использовать последовательные конструкции if
.
Особенности архитектуры
Обработка ошибок и исключительных ситуаций
Язык Go не поддерживает типичного для большинства современных языков синтаксиса структурной обработки исключений, предполагающего генерацию исключений специальной командой (обычно throw
или raise
) и их обработку в блоке try-catch
. Вместо этого рекомендуется использовать возврат ошибки как одного из результатов функции (что достаточно удобно, так как в Go функция может возвращать более одного значения):
- В последнем параметре функция возвращает объект-ошибку либо пустой указатель
nil
, если функция выполнилась без ошибок. В качестве типа ошибки обычно используется библиотечный интерфейсerror
. - Возвращённый функцией объект проверяется и ошибка, если она возникла, обрабатывается. Если ошибка в месте вызова не может быть адекватно обработана, она обычно возвращается в качестве результата текущей функции, либо на её основе создаётся новая ошибка, которая и возвращается.
func ReadFile(srcName string)(result string, err error) {
file, err := os.Open(srcName)
if err != nil {
// Генерация новой ошибки с уточняющим текстом
return nil, fmt.Errorf("Ошибка при чтении файла %s: %g\n", srcName, err)
}
... // Дальнейшее исполнение функции, если ошибки не было
return result, nil // Возврат результата и пустой ошибки, если выполнение успешно
}
- Проигнорировать ошибку, возвращаемую из функции (в примере выше — не проверить значение переменной
err
) невозможно, так как инициализация переменной без последующего использования в языке Go приводит к ошибке компиляции. Это ограничение можно обойти подстановкой вместо err псевдопеременной_
, но это явно бросается в глаза при просмотре кода.
Многие критики языка считают, что подобная идеология хуже, чем обработка исключений, так как многочисленные проверки засоряют код и не позволяют сосредоточить всю обработку ошибок в блоках catch
. Создатели языка не считают это серьёзной проблемой. Описан ряд паттернов обработки ошибок в Go (см., например, статью Роба Пайка в официальном блоге Go, русский перевод), позволяющих сократить объём кода, обрабатывающего ошибки.
При возникновении фатальных ошибок, делающих невозможным дальнейшее исполнение программы (например, деления на ноль либо обращения за границы массива), возникает состояние «паники» (panic), которое по умолчанию приводит к аварийному завершению программы с выдачей сообщения об ошибке и трассировки стека вызовов. Паника может быть перехвачена и обработана с помощью конструкции отложенного исполнения defer
, описанной выше. Вызов функции, указанный в defer
, производится перед выходом из текущей области видимости, в том числе и в случае паники. Внутри функции, вызываемой в defer
, можно вызвать стандартную функцию recover()
— она прекращает системную обработку паники и возвращает её причину в виде объекта error
, который можно обработать как обычную ошибку. Но программист может и возобновить ранее перехваченную панику, вызвав стандартную функцию panic(err error)
.
// Программа выполняет целочисленное деление
// своего первого параметра на второй
// и выводит результат.
func main() {
defer func() {
err := recover()
if v, ok := err.(error); ok { // Обработка паники, соответствующей интерфейсу error
fmt.Fprintf(os.Stderr, "Error %v \"%s\"\n", err, v.Error())
} else if err != nil {
panic(err) // Обработка неожиданных ошибок - повторный вызов паники.
}
}()
a, err := strconv.ParseInt(os.Args[1], 10, 64)
if err != nil {
panic(err)
}
b, err := strconv.ParseInt(os.Args[2], 10, 64)
if err != nil {
panic(err)
}
fmt.Fprintf(os.Stdout, "%d / %d = %d\n", a, b, a/b)
}
В примере выше могут произойти ошибки при преобразовании аргументов программы в целые числа функцией strconv.ParseInt()
. Также возможна паника при обращении к массиву os.Args при недостаточном количестве аргументов, либо при делении на нуль, если второй параметр окажется нулевым. При любой ошибочной ситуации генерируется паника, которая обрабатывается в вызове defer
:
> divide 10 5
10 / 5 = 2
> divide 10 0
Error runtime.errorString "runtime error: integer divide by zero"
> divide 10.5 2
Error *strconv.NumError "strconv.ParseInt: parsing "10.5": invalid syntax"
> divide 10
Error runtime.errorString "runtime error: index out of range"
Паника не может быть вызвана в одной параллельно исполняемой го-процедуре (см. ниже), а обработана в другой. Также не рекомендуется «передавать» панику через границу пакета.
Многопоточность
Модель многопоточности Go была создана на основе CSP Тони Хоара по типу предыдущих распараллеливаемых языков программирования Occam и Limbo[10], но также присутствуют такие особенности как Пи-исчисление и канальная передача.
Go дает возможность создать новый поток выполнения программы с помощью ключевого слова go, которое запускает анонимную или именованную функцию в заново созданной go-процедуре (термин, используемый в Go для обозначения сопрограмм). Все go-процедуры в рамках одного процесса используют общее адресное пространство, выполняясь над ОС-потоками, но без жёсткой привязки к последним, что позволяет выполняющейся go-процедуре покидать поток с заблокированной go-процедурой (ждущей, например, отправки или приема сообщения из канала) и продолжать работу далее. Библиотека времени исполнения включает мультиплексор, обеспечивающий разделение доступного количества системных ядер между go-процедурами. Имеется возможность ограничить максимальное число физических процессорных ядер, на которых будет исполняться программа. Самостоятельная поддержка go-процедур runtime-библиотекой Go позволяет без затруднений использовать в программах огромные количества go-процедур, намного превышающие предельное число поддерживаемых системой потоков.
func server(i int) {
for {
print(i)
time.Sleep(10)
}
}
go server(1)
go server(2)
В выражении go можно использовать замыкания.
var g int
go func(i int) {
s := 0
for j := 0; j < i; j++ { s += j }
g = s
}(1000)
Для связи между go-процедурами используются каналы (встроенный тип chan), через которые можно передавать любые значения. Канал создаётся встроенной функцией make()
, которой передаётся тип и (опционально) объём канала. По умолчанию объём канала равен нулю. Такие каналы являются небуферизованными. Можно задать любой целый положительный объём канала, тогда будет создан буферизованный канал.
Небуферизованный канал жёстко синхронизирует поток-читатель и поток-писатель, использующих его. Когда поток-писатель что-то записывает в канал, он приостанавливается и ожидает, пока значение не будет прочитано. Когда поток-читатель пытается что-то прочитать из канала, куда уже произведена запись, он считывает значение, и оба потока могут продолжать исполняться. Если же в канал ещё не записано значения, поток-читатель приостанавливается и ожидает, пока кто-нибудь не произведёт запись в канал. То есть небуферизованные каналы в Go ведут себя так же, как каналы в Occam'е или механизм рандеву в языке Ада.
Буферизованный канал имеет буфер значений, размер которого равен объёму канала. При записи в такой канал значение помещается в буфер канала, а поток-писатель продолжает работу без приостановки, если только буфер канала на момент записи не полон. Если буфер полон, то поток-писатель приостанавливается до момента, пока из канала не будет прочитано хотя бы одно значение. Поток-читатель также считывает из буферизованного канала значение без приостановки, если в буфере канала есть непрочитанные значения; если буфер канала пуст, то поток приостанавливается и ждёт, пока какой-либо другой поток не запишет в него значение.
По завершении использования канал может быть закрыт встроенной функцией close()
. Попытка записи в закрытый канал приводит к панике, чтение из закрытого канала всегда происходит без приостановки и считывает значение по умолчанию. Если канал буферизованный и в момент закрытия содержит в буфере N ранее записанных значений, то первые N операций чтения выполнятся так, как будто канал ещё открыт, и прочитают значения из буфера, и только после этого чтение из канала будет возвращать значения по умолчанию.
Для передачи значения в канал и из канала используется операция <-
. При записи в канал она применяется в качестве бинарного оператора, при чтении — в качестве унарного оператора:
in := make(chan string, 0) // Создание небуферизованного канала in
out := make(chan int, 10) // Создание буферизованного канала out
...
in <- arg // запись значения в канал in
...
r1 := <- out // чтение из канала out
...
r2, ok := <- out // чтение с проверкой закрытия канала
if ok { // если ok == true - канал открыт
...
} else { // если канал закрыт, делаем что-то ещё
...
}
Операция чтения из канала имеет два варианта: без проверки и с проверкой закрытия канала. Первый вариант (чтение r1 в примере выше) просто выполняет чтение очередного значения в переменную; если канал закрыт, то в r1 прочитается значение по умолчанию. Второй вариант (чтение r2) считывает, помимо значения, логическое значение — флаг состояния канала ok, который будет истинным, если из канала прочитаны данные, помещённые туда каким-либо потоком, и ложным, если канал закрыт и его буфер пуст. С помощью этой операции поток-читатель может определить, когда входной канал закрыт.
Также поддерживается чтение из канала с помощью циклической конструкции for-range:
// Функция запускает параллельное чтение из входного канала in целых чисел и запись
// в выходной канал только тех из них, которые положительны.
// Возвращает выходной канал.
func positives(in <-chan int64) <-chan int64 {
out := make(chan int64)
go func() {
// Цикл далее будет выполняться, пока канал in не закрыт
for next := range in {
if next > 0 {
out <- next
}
}
close(out)
}()
return out
}
Помимо CSP или совместно с механизмом канальной передачи Go позволяет использовать и обычную модель синхронизированного взаимодействия потоков через общую память, с использованием типовых средств синхронизации доступа, таких как мьютексы. При этом, однако, спецификация языка предостерегает от любых попыток несинхронизированного взаимодействия параллельных потоков через общую память, так как в отсутствие явной синхронизации компилятор оптимизирует код доступа к данным без учёта возможности одновременного обращения из разных потоков, что может приводить к неожиданным ошибкам. Так, запись значений в глобальные переменные в одном потоке может быть не видна или видна не в том порядке из параллельного потока.
Для примера рассмотрим программу ниже. Код функции main()
написан в предположении, что запущенная в go-процедуре функция setup()
создаст структуру типа T
, инициализирует её строкой «hello, world», после чего присвоит ссылку на инициализированную структуру глобальной переменной g
. В main()
запускается пустой цикл, ожидающий появления в g
ненулевого значения. Как только оно появится, main()
выводит строку из структуры, на которую указывает g
, считая, что структура уже инициализирована.
type T struct {
msg string
}
var g *T
func setup() {
t: = new (T)
t.msg = "hello, world"
g = t
}
func main () {
go setup()
for g == nil { // НЕ РАБОТАЕТ !!!
}
print(g.msg)
}
В действительности же возможна одна из двух ошибок.
- Главный поток может просто не увидеть изменения переменной
g
, и тогда программа зависнет в бесконечном цикле. Такое может произойти, если настроенный на агрессивную оптимизацию компилятор определит, что созданное вsetup()
значение никуда не передаётся, и просто удалит весь код данной функции как незначимый. - Главный поток увидит, что значение
g
перестало быть нулевым, но при этом значениеg.msg
в момент выполнения функцииprint()
окажется не инициализированным; в этом случае программа выведет пустую строку. Такое может произойти, если компилятор в целях оптимизации удалит локальную переменнуюt
и запишет ссылку на созданный объект непосредственно вg
.
Единственным корректным способом организации передачи данных через общую память является использование библиотечных средств синхронизации, которые гарантируют, что все данные, запись которых произведена одним из синхронизируемых потоков до точки синхронизации, гарантированно доступны в другом синхронизируемом потоке после точки синхронизации.
Особенностью многопоточности в Go является то, что go-процедура никак не идентифицируется и не является языковым объектом, на который можно сослаться при вызове функций или который можно поместить в контейнер. Соответственно, отсутствуют средства, позволяющие непосредственно влиять на исполнение сопрограммы извне её, такие как приостановка и последующий запуск, изменение приоритета, ожидание завершения одной сопрограммы в другой, принудительное прерывание исполнения. Любые воздействия на go-процедуру (кроме завершения главной программы, которое автоматически завершает все go-процедуры) могут выполняться только через каналы или иные механизмы синхронизации. Ниже показан типовой код, запускающий несколько go-процедур и ожидающий их завершения с помощью синхронизирующего объекта WaitGroup из системного пакета sync. Этот объект содержит счётчик, первоначально с нулевым значением, который может увеличиваться и уменьшаться, и метод Wait(), который вызывает приостановку текущего потока и ожидание до тех пор, пока счётчик не обнулится.
func main() {
var wg sync.WaitGroup // Создание waitgroup. Исходное значение счётчика — 0
logger := log.New(os.Stdout, "", 0) // log.Logger — потоково-безопасный тип для вывода
for _, arg := range os.Args { // Цикл по всем аргументам командной строки
wg.Add(1) // Увеличение счётчика waitgroup на единицу
// Запуск go-процедуры для обработки параметра arg
go func(word string) {
// Отложенное уменьшение счётчика waitgroup на единицу.
// Произойдёт по завершении функции.
defer wg.Done()
logger.Println(prepareWord(word)) // Выполнение обработки и вывод результата
}(arg)
}
wg.Wait() // Ожидание, пока счётчик в waitgroup wg не станет равным нулю.
}
Здесь перед созданием каждой новой go-процедуры счётчик объекта wg увеличивается на единицу, а по завершении go-процедуры — уменьшается на единицу. В результате в цикле, запускающем обработку аргументов, к счётчику будет добавлено столько единиц, сколько запущено go-процедур. По завершении цикла вызов wg.Wait() вызовет приостановку главной программы. Когда каждая из go-процедур завершается, она уменьшает счётчик wg на единицу, поэтому ожидание главной программы закончится тогда, когда завершится столько go-процедур, сколько было запущено. Без последней строки главная программа, запустив все go-процедуры, немедленно завершилась бы, прервав исполнение тех из них, которые не успели выполниться.
Несмотря на наличие встроенной в язык многопоточности, не все стандартные языковые объекты являются потокобезопасными. Так, стандартный тип map (отображение) не потокобезопасен. Создатели языка объяснили такое решение соображениями эффективности, так как обеспечение безопасности для всех подобных объектов привело бы к дополнительным накладным расходам, которые далеко не всегда являются обязательными (те же операции с отображениями могут быть частью более крупных операций, которые уже синхронизированы программистом, и тогда дополнительная синхронизация лишь усложнит и замедлит программу). Начиная с версии 1.9 в библиотечный пакет sync, содержащий средства поддержки параллельной обработки, добавлен потокобезопасный тип sync.Map, который при необходимости можно использовать. Также можно обратить внимание, на использованный в примере для вывода результатов потокобезопасный тип log.Logger
; он применён вместо стандартного пакета fmt, функции которого (Printf, Println и так далее) не потокобезопасны и потребовали бы дополнительной синхронизации.
Специальное ключевое слово для объявления класса в Go отсутствует, но для любого именованного типа, включая структуры и базовые типы вроде int, можно определить методы, так что в смысле ООП все такие типы являются классами.
type newInt int
Синтаксис определения метода заимствован из языка Оберон-2 и отличается от обычного определения функции тем, что после ключевого слова func в круглых скобках объявляется так называемый «получатель» (англ. receiver), то есть объект, для которого вызывается метод, и тип, к которому относится метод. Если в традиционных объектных языках получатель подразумевается и имеет стандартное имя (в C++ или Java — «this», в ObjectPascal — «self» и т. п.), то в Go он указывается явно и его имя может быть любым правильным Go-идентификатором.
type myType struct { i int }
// Здесь p - получатель в методах типа myType.
func (p *myType) get() int { return p.i }
func (p *myType) set(i int) { p.i = i }
Наследование классов (структур) в Go формально отсутствует, но имеется технически близкий к нему механизм встраивания (англ. embedding). В описании структуры можно использовать так называемое анонимное поле — поле, для которого не указывается имя, а только тип. В результате такого описания все элементы встраиваемой структуры станут одноимёнными элементами встраивающей.
// Новый тип-структура
type myType2 struct {
myType // Анонимное поле обеспечивает встраивание типа myType.
// Теперь myType2 содержит поле i и методы get() и set(int).
k int
}
В отличие от классического наследования, встраивание не влечёт полиморфное поведение (объект встраивающего класса не может выступать в качестве объекта встраиваемого без явного преобразования типов).
Невозможно явно описать методы для безымянного типа (синтаксис просто не даёт возможности указать тип получателя в методе), но это ограничение можно легко обойти путём встраивания именованного типа с необходимыми методами.
Полиморфизм классов обеспечивается в Go механизмом интерфейсов (похожи на полностью абстрактные классы в C++). Интерфейс описывается с помощью ключевого слова interface, внутри (в отличие от описаний типов-классов) описания объявляются предоставляемые интерфейсом методы.
type myInterface interface {
get() int
set(i int)
}
В Go нет необходимости явно указывать, что некоторый тип реализует определённый интерфейс. Вместо этого действует правило: каждый тип, предоставляющий методы, обозначенные в интерфейсе, может быть использован как реализация этого интерфейса. Объявленный выше тип myType
реализует интерфейс myInterface
, хотя это нигде не указано явно, поскольку он содержит методы get()
и set()
, сигнатуры которых соответствуют описанным в myInterface
.
Аналогично классам, интерфейсы допускают встраивание:
type mySecondInterface interface {
myInterface // то же, что явно описать get() int; set(i int)
change(i int) int
}
Здесь интерфейс mySecondInterface наследует интерфейс myInterface (то есть объявляет, что предоставляет методы, входящие в myInterface) и дополнительно объявляет один собственный метод change()
.
Хотя в принципе возможно построить в программе на Go и иерархию интерфейсов, как это практикуется в других объектных языках, и даже имитировать наследование, это считается плохой практикой. Язык диктует не иерархический, а композиционный подход к системе классов и интерфейсов. Классы-структуры при таком подходе вообще могут оставаться формально независимыми, а интерфейсы не объединяются в единую иерархию, а создаются для конкретных применений, при необходимости встраивая уже имеющиеся. Неявная реализация интерфейсов в Go обеспечивает чрезвычайную гибкость этих механизмов и минимум технических затруднений при их использовании.
Такой подход к наследованию соответствует некоторым практическим тенденциям современного программирования. Так в знаменитой книге «банды четырёх» (Эрих Гамма и др.) о паттернах проектирования, в частности, написано:
Зависимость от реализации может повлечь за собой проблемы при попытке повторного использования подкласса. Если хотя бы один аспект унаследованной реализации непригоден для новой предметной области, то приходится переписывать родительский класс или заменять его чем-то более подходящим. Такая зависимость ограничивает гибкость и возможности повторного использования. С проблемой можно справиться, если наследовать только абстрактным классам, поскольку в них обычно совсем нет реализации или она минимальна.
В Go нет понятия виртуальной функции. Полиморфизм обеспечивается за счёт интерфейсов. Если для вызова метода используется переменная обычного типа, то такой вызов связывается статически, то есть всегда вызывается метод, определённый для данного конкретного типа. Если же метод вызывается для переменной типа «интерфейс», то такой вызов связывается динамически, и в момент исполнения для запуска выбирается тот вариант метода, который определён для типа объекта, фактически присвоенного в момент вызова этой переменной.
Динамическая поддержка объектно-ориентированного программирования для Go осуществлена с помощью проекта GOOP.
Рефлексия
Возможность интроспекции во время выполнения, то есть доступ и обработка значений любых типов и динамическая настройка на типы обрабатываемых данных реализуются в Go с помощью системного пакета reflect
. Средства данного пакета позволяют:
- определить тип любого значения;
- сравнить на эквивалентность два любых значения, в том числе те, которые не сравниваются стандартными средствами языка, например, срезы;
- работать одним и тем же кодом со значениями любых типов (тип
reflect.Value
позволяет представить значение любого языкового типа и преобразовать его в один из стандартных типов, если такое преобразование возможно); - изменять любые значения, если такое изменение в принципе возможно (например, изменить часть строки);
- исследовать типы, в частности, обращаться к полям структур и их дескрипторам, получать списки методов типов, их описания;
- вызывать произвольные функции и методы.
Также пакет reflect
содержит множество вспомогательных инструментов для выполнения операций в зависимости от динамического состояния программы.
Низкоуровневое программирование
Средства низкоуровневого доступа к памяти сосредоточены в системном пакете unsafe
. Его особенность в том, что, будучи внешне обычным Go-пакетом, он фактически реализуется самим компилятором. Пакет unsafe
обеспечивает доступ к внутреннему представлению данных и к «настоящим» указателям на память. Он предоставляет функции:
unsafe.Sizeof()
— аргументом может быть выражение любого типа, функция возвращает реальный размер операнда в байтах, включая неиспользуемую память, которая может появляться в структурах из-за выравнивания;unsafe.Alignof()
— аргументом может быть выражение любого типа, функция возвращает размер в байтах, по которому типы операнда выравниваются в памяти;unsafe.Offsetof()
— аргументом должно быть поле структуры, функция возвращает смещение в байтах, по которому располагается это поле в структуре.
Также пакет предоставляет тип unsafe.Pointer
, в который может быть преобразован любой указатель и который может быть преобразован в указатель любого типа, а также в стандартный тип uintptr
— целое беззнаковое значение, достаточно большое для сохранения полного адреса на текущей платформе. Преобразовав указатель в unsafe.Pointer
и, далее, в uintptr
, можно получить адрес в виде целого числа, к которому можно применять арифметические операции. Преобразовав затем значение обратно в unsafe.Pointer
и в указатель на любой конкретный тип, можно таким способом обратиться практически в любое место адресного пространства.
Описанные преобразования могут быть небезопасны, поэтому их рекомендуют по возможности избегать. Во-первых, возможны очевидные проблемы, связанные с ошибочным обращением не к той области памяти. Более тонким моментом является то, что несмотря на использование пакета unsafe
, объекты Go продолжают находиться под управлением менеджера памяти и сборщика мусора. Преобразование указателя в число выводит этот указатель из-под контроля, и программист не может рассчитывать на то, что такой преобразованный указатель останется актуальным неограниченно долго. Например, попытка сохранить указатель на новый объект типа Т
следующим образом:
pT := uintptr(unsafe.Pointer(new(T))) // НЕВЕРНО!
приведёт к тому, что объект будет создан, указатель на него преобразован в число (которое будет присвоено pT
). Однако pT
имеет целый тип и сборщик мусора не считает его указателем на созданный объект, так что после завершения операции система управления памятью будет считать этот объект неиспользуемым. То есть он может быть удалён сборщиком мусора, после чего преобразованный указатель pT
станет некорректным. Произойти это может в любой момент, как сразу по завершении операции, так и через много часов работы программы, так что ошибка выразится в случайных сбоях программы, причину которых крайне сложно будет выявить. А при использовании перемещающего сборщика мусора[* 1] преобразованный в число указатель может стать неактуальным даже тогда, когда объект ещё не удалён из памяти.
Поскольку спецификация Go не даёт точных указаний на то, в какой мере программист может рассчитывать на сохранение актуальности преобразованного в число указателя, существует рекомендация: сводить подобные преобразования к минимуму и организовывать их так, чтобы преобразование исходного указателя, его модификации и обратное преобразование находились в пределах одной языковой инструкции, а при вызове любых библиотечных функций, возвращающих адрес в виде uintptr
, немедленно преобразовывать их результат в unsafe.Pointer
для сохранения гарантии, что указатель не будет потерян.
Пакет unsafe
редко используется в прикладном программировании непосредственно, но он активно применяется в пакетах reflect
, os
, syscall
, context
, net
и некоторых других.
Интерфейс с кодом на других языках
Существует несколько внешних инструментов, обеспечивающих интерфейсы с иноязычными функциями (англ. foreign-function interfaces, FFI) для Go-программ. Для взаимодействия с внешним кодом на Си (или имеющем совместимый с Си интерфейс) может применяться утилита cgo. Она вызывается автоматически при обработке компилятором соответствующим образом написанного Go-модуля, и обеспечивает создание временного пакета-враппера на Go, содержащего объявления всех необходимых типов и функций. В вызовах Си-функций часто приходится прибегать к средствам пакета unsafe
, главным образом — использовать тип unsafe.Pointer
. Более мощным инструментом является SWIG[17], обеспечивающий более сложные возможности, в частности, интеграцию с классами C++.
Интерфейс пользователя
Стандартная библиотека Go поддерживает создание консольных приложений и серверных приложений с веб-интерфейсом, но нет стандартных средств для создания GUI в клиентских приложениях. Этот пробел компенсируется созданными сторонними разработчиками врапперами к популярным UI-фреймворкам, таким как GTK+ и Qt, под Windows можно использовать графические средства WinAPI, обращаясь к ним посредством пакета syscall
, но все эти способы довольно громоздки. Имеется также несколько разработок UI-фреймворков на самом Go, но ни один из этих проектов не достиг уровня промышленной применимости. В 2015 году на конференции GopherCon в Денвере один из создателей языка, Роберт Грисмер, отвечая на вопросы, согласился, что Go нуждается в пакете UI, но заметил, что такой пакет должен быть универсальным, мощным и мультиплатформенным, что делает его разработку длительным и непростым процессом. Вопрос о реализации клиентского GUI на Go до сих пор остаётся открытым.
Критика
В силу молодости языка его критика сосредоточена, главным образом, в Интернет-статьях, обзорах и на форумах.
Отсутствие возможностей
Значительная часть критики языка фокусируется на отсутствии в нём тех или иных популярных средств, предоставляемых другими языками. В их числе[18][19][20][21]:
- средства обобщённого программирования (generics);
- реализация наследования;
- алгебраические типы данных;
- перегрузка функций;
- переопределение операторов;
- ряд частных возможностей, характерных для языков типа Ruby или Python.
Как уже говорилось выше,
отсутствие целого ряда средств, доступных в других популярных языках, объясняется сознательным выбором разработчиков, считающих, что такие средства либо затрудняют эффективную компиляцию, либо провоцируют программиста на ошибки или на создание неэффективного или «плохого» с точки зрения сопровождения кода, либо имеют другие нежелательные побочные эффекты.Архитектура
- Отсутствие перечислимых типов. Вместо них используются группы констант, но все константы-значения фактически являются целыми числами, группы эти синтаксически не объединены, и компилятор не может контролировать их использование. Невозможно описать тип, связанный с перечислением, например, массив, содержащий по одному элементу на каждый элемент перечисления (который в Паскале описывается конструкцией вида
type EnumArray = array[EnumType] of ElementType
), создать цикл по перечислению, компилятор не может контролировать полноту списка альтернатив в конструкцииswitch
, когда в качестве селектора используется значение перечисления. - Недостаточность встроенных контейнерных типов данных.
Встроенные в язык контейнеры ограничиваются массивами и отображениями. Контейнеры, реализуемые средствами самого языка (в том числе входящие в стандартную библиотеку), нетипобезопасны из-за вынужденного использования в них элементов типаinterface{}
, к тому же их невозможно обойти с помощью конструкцииfor range
. - Отсутствие явного указания на реализацию интерфейса типом затрудняет понимание кода, его модификацию и рефакторинг. Компилятор не может автоматически проверить тип на соответствие реализуемым интерфейсам. Также возможна (хотя и маловероятна) «случайная реализация» интерфейса, когда методы типа совпадают по сигнатурам с методами интерфейса, но по смыслу не являются реализацией представляемого интерфейсом поведения.
- Отказ от структурной обработки исключений в пользу возврата ошибок делает невозможным сосредоточение обработки ошибок в одном месте, проверки ошибок засоряют код и затрудняют его восприятие. К тому же механизм обработки состояния паники по сути ничем не отличается от обработчиков исключений в стиле
try-catch
. Более того, вопреки собственным рекомендациям авторы языка применяют генерацию и обработку паники для обработки логических ошибок внутри стандартной библиотеки. - Тэги полей структур не контролируются компилятором.
Тэги, задающие дополнительные свойства полей структур, являются просто строками, которые обрабатываются динамически, на их формат нет даже самых простых синтаксических ограничений. Это сделано, чтобы не ограничивать разработчика в использовании тэгов, но на практике приводит к тому, что никакая ошибка в записи тэга не может быть обнаружена на стадии компиляции.
«Подводные камни» (неудачная реализация некоторых средств)
Критики отмечают, что некоторые особенности Go выполнены с точки зрения наиболее простой или наиболее эффективной реализации, но не отвечают «принципу наименьшего удивления»: их поведение отличается от того, что программист ожидает, основываясь на интуиции и прошлом опыте. Такие особенности требуют повышенного внимания программиста, затрудняют обучение и переход с других языков.
- В цикле по коллекции переменная-значение является копией, а не ссылкой.
В цикле вида «for index, value := range collection
» переменнаяvalue
является копией текущего элемента. Операция присваивания этой переменной нового значения доступна, но, вопреки ожиданиям, не приводит к изменению текущего элемента коллекции. - Нулевой интерфейс не равен интерфейсу нулевого объекта.
Значение типа «интерфейс» представляет собой структуру из двух ссылок — на таблицу методов и на сам объект. У нулевого интерфейса оба поля равныnil
. У интерфейса, указывающего на нулевой объект, первая ссылка заполнена; он не равен нулевому интерфейсу, хотя с точки зрения логики программы между ними обычно нет разницы[22]. Это приводит к неожиданным эффектам и усложняет проверку корректности значений интерфейсных типов:
type I interface { f() } type T struct {} func (T) f() {...} // Тип T реализует интерфейс I. main() { var t *T = nil // t - нулевой указатель на тип T. var i I = t // Записываем пустой указатель на T в интерфейсную переменную. if i != nil { // ! Неожиданность. Хотя i был присвоен пустой указатель, i != nil i.f() // Этот вызов произойдёт и приведёт к панике. } ... }
- Хотя в переменную
i
был записан нулевой указатель на объект, значение самойi
не является пустым и сравнениеi != nil
даёт положительный результат. Чтобы убедиться, что интерфейсная переменная указывает на действительный объект, необходимо воспользоваться рефлексией, что заметно усложняет код: if i != nil && !reflect.ValueOf(i).IsNil() { ...
- Неоднородная семантика присваивания даже на близких типах.
Встроенные типы и структуры присваиваются по значению, интерфейсы — по ссылке. Массивы с объявленной статически длиной присваиваются по значению, массивы без объявленной длины и отображения — по ссылке. Фактически вариант семантики присваивания для типа определяется тем, как размещаются в памяти значения этого типа, то есть язык определяется реализацией.
- Различное поведение операций над массивами и срезами в разных условиях.
Например, стандартная функцияappend()
, добавляющая элементы к массиву, может создать и вернуть новый массив, а может дописать и вернуть существующий, в зависимости от того, имеется ли в нём достаточно свободного места для добавления элементов. В первом случае последующие изменения результирующего массива не затронут оригинал, во втором — отразятся на нём. Такое поведение вынуждает к постоянному использованию функции копированияcopy()
.
Другие особенности
Часто критике подвергается механизм автоматической расстановки точек с запятой, из-за которого некоторые формы записи операторов, вызовов функций и списков становятся некорректными. Комментируя это решение, авторы языка замечают,[10] что в совокупности с наличием в официальном наборе инструментов средства форматирования кода gofmt
оно привело к фиксации довольно жёсткого стандарта оформления кода на Go. Вряд ли возможно создать стандарт записи кода, который бы устроил всех; внедрение в язык особенности, которая сама по себе задаёт такой стандарт, унифицирует внешний вид программ и устраняет непринципиальные конфликты из-за форматирования, что является положительным фактором для групповой разработки и сопровождения ПО.
Распространение и перспективы
Популярность Go в последние годы росла: с 2014 по 2020 год в рейтинге TIOBE он поднялся с 65-го места на 11-е, значение рейтинга на август 2020 года составляет 1,43 %. По результатам опроса сайта dou.ua[23] язык Go в 2018 году стал девятым в списке самых используемых и шестым в списке языков, которым отдают личное предпочтение разработчики.
С 2012 года, когда вышел первый публичный релиз, использование языка неуклонно растёт. В опубликованном на сайте проекта Go списке компаний, использующих язык в промышленных разработках, насчитывается несколько десятков наименований. Накоплен большой массив библиотек различного назначения. На 2019 год был запланирован выпуск версии 2.0, но работы затянулись и на вторую половину 2020 года ещё продолжаются. Ожидается
появление ряда новых возможностей, в том числе средств обобщённого программирования и специального синтаксиса для упрощения обработки ошибок, отсутствие которых является одними из наиболее распространённых претензий критиков языка .На Golang разработан веб-сервер RoadRunner (Application server), который позволяет веб-приложениям достигать скорости request-response 10-20 мс вместо традиционных 200 мс. Данный веб-сервис планируется включить в состав популярных фреймворков, таких как Yii.
Наряду с C++ Golang применяется для разработки микросервисов, что позволяет «загрузить» работой много-процессорные платформы. Взаимодействовать с микросервисом можно с помощью REST, а язык PHP для этого отлично подходит.
С помощью PHP и Golang разработан Spiral Framework.[24]
Версии
Нумерация и принципы совместимости версий
Cуществует только одна основная версия самого языка Go — версия 1. Версии среды разработки (компилятора, инструментария и стандартных библиотек) Go нумеруются по двухзначной («<версия языка>.<основной релиз>») либо трёхзначной («<версия языка>.<основной релиз>.<дополнительный релиз>») системе. Выпуск новой «двузначной» версии автоматически означает прекращение поддержки предыдущей «двузначной» версии. «Трёхзначные» версии выпускаются для исправления обнаруженных ошибок и проблем с безопасностью; исправления безопасности в таких версиях могут затрагивать две последние «двузначные» версии[25].
Авторы декларировали[26] стремление к сохранению, насколько это возможно, обратной совместимости в пределах основной версии языка. Это означает, что до выхода релиза Go 2 почти любая программа, созданная в среде Go 1, будет корректно компилироваться в любой последующей версии Go 1.x и выполняться без ошибок. Исключения возможны, но они немногочисленны. Однако бинарной совместимости между релизами не гарантируется, так что программа при переходе на более поздний релиз Go должна быть полностью перекомпилирована.
Go 1
С марта 2012 года, когда была представлена версия Go 1, вышли следующие основные версии:
- go 1 — 28 марта 2012 года — Первая официальная версия; зафиксированы библиотеки, внесены изменения в синтаксис.
- go 1.1 — 13 мая 2013 года — целочисленное деление на нуль стало синтаксической ошибкой, введены method values — замыкания метода с заданным значением-источником, в некоторых случаях стало необязательным использование return; в реализации разрешено выбирать между 32- и 64-разрядным представлением стандартного целочисленного типа, изменения в поддержке Unicode.
- go 1.2 — 1 декабря 2013 года — любая попытка обратиться по указателю nil гарантированно вызывает панику, введены трёхиндексные срезы. Доработки Unicode.
- go 1.3 — 18 июня 2014 года — изменена модель распределения памяти; удалена поддержка платформы Windows 2000, добавлены DragonFly BSD, FreeBSD, NetBSD, OpenBSD, Plan 9, Solaris.
- go 1.4 — 10 декабря 2014 года — разрешена конструкция цикла «for range x { … }» (цикл по коллекции без использования переменных), запрещено двойное автоматическое разыменование при вызове метода (если x **T — двойной указатель на тип T, то вызов метода для x в виде x.m() — запрещён); в реализацию добавлена поддержка платформ Android, NaCl on ARM, Plan9 on AMD64.
- go 1.5 — 19 августа 2015 года — в записи map-литералов указание типа каждого элемента сделано факультативным, в реализации среда исполнения и компилятор полностью переписаны на Go и ассемблере, более не используется язык Си.
- go 1.6 — 17 февраля 2016 года — изменений в языке нет, среда портирована на платформы Linux on 64-bit MIPS, Android on 32-bit x86 (android/386), изменения в инструментарии.
- go 1.7 — 16 августа 2016 года — уменьшены время компиляции и размер бинарных файлов, увеличена скорость работы и в стандартную библиотеку добавлен пакет context.
- go 1.8 — 7 апреля 2017 года — ускорена работа встроенного сборщика мусора памяти, модуль «http» получил возможность мягкой остановки, добавлена поддержка процессоров с архитектурой MIPS (32-бит). Внесены исправления в ряд пакетов и утилиты.
- go 1.9 — 24 августа 2017 года — добавлены в язык псевдонимы имён типов, уточнены некоторые моменты использования операций с плавающей точкой, оптимизирован инструментарий, дополнение библиотек, в частности — потоково-безопасный тип map.
- go 1.10 — 16 февраля 2018 года — в язык внесено два уточнения, фактически узаконивших уже существующие реализации, остальные изменения касаются библиотек и инструментария. Выпущено три «трёхзначных» релиза 1.10.1 — 1.10.3, содержащие исправления обнаруженных ошибок.
- go 1.11 — 24 августа 2018 года — добавлены (в качестве экспериментальных) поддержка модулей (нового механизма версионирования пакетов и управления зависимостями), а также возможность компиляции в WebAssembly, улучшена поддержка ARM-процессоров, внесены изменения в инструментарий и библиотеки (в частности, добавлен пакет syscall/js; компилятор теперь правильно контролирует использование переменных, объявленных в конструкциях switch с проверкой типа).
- go 1.12 — 25 февраля 2019 года — исправления в библиотеках и утилитах. Объявлено, что это последний релиз, в котором сохраняется поддержка FreeBSD 10.Х и macOS 10.10. Добавлена поддержка cgo на платформе linux/ppc64. Добавлена поддержка ОС AIX. До августа 2019 в рамках этого релиза вышло девять патч-релизов, исправляющих различные ошибки.
- go 1.13 — 3 сентября 2019 года — добавлены в язык новые числовые литералы: двоичные и восьмеричные целые, шестнадцатеричные с плавающей точкой (последние обязательно должны содержать экспоненту, отделяемую символом p или P); разрешено использование подчёркивания для разделения разрядов в числах; разрешена операция побитового сдвига для целых чисел со знаком; добавлена поддержка Android 10; по ряду платформ прекращена поддержка старых версий.
- go 1.14 — 25 февраля 2020 года — расширено определение включения интерфейсов: теперь разрешено включать несколько интерфейсов, имеющих одноимённые методы с идентичными сигнатурами. Изменения в библиотеках, среде исполнения, инструментарии.
- go 1.15 — 11 августа 2020 года — удалена поддержка 32-битных вариантов ОС на ядре Darwin, улучшена работа линковщика, добавлена опциональная митигация уязвимости Spectre, добавлены новые предупреждения инструмента go vet. Языковых изменений в данном релизе не было. К концу ноября 2020 вышло пять минорных релизов, исправляющих ошибки и устраняющих уязвимости в безопасности.
- go 1.16 — 16 февраля 2021 года — добавлена поддержка 64-bit ARM под macOS и NetBSD, MIPS64 под OpenBSD, улучшена реализация для ряда архитектур, в том числе RISC-V. Поддержка модулей включена по умолчанию, в параметры команд сборки добавлена возможность явно указывать версии. Языковых изменений нет. Внесены изменения в библиотеки, в частности, добавлен пакет
embed
, реализующий возможность доступа к файлам, встроенным в состав исполняемого модуля. На июнь 2021 выпущено пять минорных релизов.
Go 2.0
Эта статья или часть статьи содержит информацию об ожидаемых событиях. |
- Ход разработки
- С 2017 года ведётся подготовка к выпуску следующей базовой версии языка, имеющей условное обозначение «Go 2.0»[27]. Проводится сбор замечаний к текущей версии и предложений по преобразованиям, аккумулируемых на wiki-сайте проекта[28].
- Изначально предполагалось, что процесс подготовки займёт «около двух лет», причём часть новых элементов языка будет включена уже в очередные релизы версии Go 1 (разумеется, только те, которые не нарушают обратной совместимости).[27] На апрель 2021 года версия 2.0 ещё не готова, часть планируемых изменений находится в стадии проектирования и реализации. По планам, изложенным в блоге проекта,[29] работы над реализацией запланированных изменений продлятся ещё как минимум в течение 2021 года.
- Предполагаемые новшества
- В числе принципиальных новшеств заявлены явно объявляемые константные значения, новый механизм обработки ошибок и средства обобщённого программирования. В сети доступны проекты нововведений. 28 августа 2018 года в официальном блоге разработчиков был опубликован ролик, ранее представленный на конференции Gophercon 2018, в котором демонстрируются черновые варианты нового дизайна обработки ошибок и механизма обобщённых функций.
- Также запланировано множество менее заметных, но весьма существенных изменений,[30] таких как расширение правил допустимости символов для идентификаторов на не-латинских алфавитах, разрешение операций сдвига для целых чисел со знаком, использование знака подчёркивания в качестве разделителя групп разрядов в числах, двоичные литералы. Большинство из них на текущий момент уже реализовано и доступно в последних версиях Go 1.
- Обработка ошибок
- Было рассмотрено несколько вариантов модификации механизма обработки ошибок, в частности, дизайн с отдельным обработчиком ошибок («Error Handling — Draft Design»). Последний на июль 2019 вариант описан в статье «Proposal: A built-in Go error check function, try». Данный вариант является наиболее минималистичным и предполагает добавление лишь одной встроенной функции
try()
, обрабатывающей результат вызова функции. Её использование иллюстрируется псевдокодом ниже. func f(…)(r1 type_1, …, rn type_n, err error) { // Проверяемая функция // Возвращает n+1 результатов: r1... rn, err типа error. } func g(…)(…, err error) { // Вызов функции f() с проверкой ошибки: … x1, x2,… xn = try(f(…)) // Используется встроенная конструкция try: // если f() вернула в последнем результате не nil, то g() автоматически завершится, // вернув в СВОЁМ последнем результате это же значение. … } func t(…)(…, err error) { // Аналог g() без использования нового синтаксиса: t1, t2,… tn, te := f(…) // Вызов f() с сохранением результатов во временных переменных. if te != nil { // Проверка кода возврата на равенство nil err = te // Если код возврата - не nil, то он записывается в последний результат t(), return // после чего t() немедленно завершается. } // Если ошибки не было, x1, x2,… xn = t1, t2,… tn // … переменные x1…xn получают значения // и исполнение t() продолжается. … }
- То есть
try()
просто обеспечивает проверку ошибки в вызове проверяемой функции и немедленный возврат из текущей функции с той же самой ошибкой. Для обработки ошибки перед возвратом из текущей функции можно использовать механизмdefer
. Использованиеtry()
требует, чтобы и проверяемая функция, и та функция, в которой происходит её вызов, обязательно имели последнее возвращаемое значение типаerror
. Поэтому, например, вmain()
использоватьtry()
нельзя; на верхнем уровне все ошибки должны быть обработаны явно. - Предполагалось включение описанного механизма обработки ошибок в версии Go 1.14, но это не было сделано. Сроки реализации не уточняются.
- Обобщённый код
- В конце 2018 года был представлен проект реализации в Go обобщённых типов и функций[31]. 9 сентября 2020 года был опубликован переработанный вариант дизайна,[32] в котором функции, типы и параметры функций могут параметризоваться типами-параметрами, которые, в свою очередь, управляются ограничениями.
// Stringer - интерфейс-ограничение, требующее, чтобы тип реализовывал // метод String, возвращающий строковое значение. type Stringer interface { String() string } // Функция получает на вход массив значений любого типа, реализующего метод String, и возвращает // соответствующий массив строк, полученных вызовом метода String для каждого элемента входного массива. func Stringify [T Stringer] (s []T) []string { // тип-параметр T, отвечающий ограничению Stringer, // является типом значения массива-параметра s. ret = make([]string, len(s)) for i, v := range s { ret[i] = v.String() } return ret } ... v := make([]MyType) ... // Для вызова обобщённой функции нужно указать конкретный тип s := Stringify[String](v)
- Здесь функция
Stringify
содержит параметр-типT
, который используется в описании обычного параметраs
. Чтобы вызывать такую функцию, как показано в примере, требуется в вызове указать конкретный тип, для которого она вызывается.Stringer
в данном описании — это ограничение (constraint), которое требует, чтобы тип MyType реализовывал методString
без параметров, возвращающий строковое значение. Это позволяет компилятору правильно обработать выражение «v.String()
». - Реализация обобщённого кода заявлена в версии 1.18, планируемой на август 2021 года.[29]
Реализации
На данный момент существуют два основных компилятора Go:
- gc — общее название для официального набора инструментов разработки, поддерживаемого группой разработчиков языка. Первоначально он включал компиляторы 6g (для amd64), 8g (для x86), 5g (для ARM) и сопутствующие инструменты и был написан на Си с применением yacc/Bison для парсера. В версии 1.5 весь код на Си был переписан на Go и ассемблере, а отдельные компиляторы — заменены на единый go tool compile.
- Поддерживается для FreeBSD, OpenBSD, Linux, macOS, Windows, DragonFly BSD, Plan 9, Solaris, Android, AIX: для актуальных версий FreeBSD, Linux, macOS, Windows имеются бинарные дистрибутивы, для остальных платформ требуется компиляция из исходных текстов. Разработчики поддерживают ограниченный список версий платформ, в новых выпусках компилятора исключая из списка поддерживаемых версии, которые на момент выпуска считаются устаревшими. Так, gc 1.12 поддерживает Windows не старше версий 7 и Server 2008R.
- gccgo — компилятор Go с клиентской частью, написанной на C++, и рекурсивным парсером, совмещённым со стандартным бэк-эндом GCC[33]. Поддержка Go доступна в GCC, начиная с версии 4.6[34]. Большинство расхождений с компилятором gc связаны с библиотекой времени выполнения и не видны для программ на Go.[35] В релизе gcc 8.1 поддерживаются все изменения в языке до версии 1.10.1 и интегрирован параллельно исполняющийся сборщик мусора.[36] Потоки (go-процедуры) реализованы в gccgo через потоки ОС, вследствие чего программы, активно использующие параллельные вычисления, могут приводить к существенно бо́льшим накладным расходам. Поддержка легковесных потоков возможна при использовании компоновщика gold, но он доступен не для всех платформ.
Также существуют проекты:
- llgo — прослойка для компиляции Go в llvm, написанная на самом go (находился в разработке до 2014 года)[37][38].
- gollvm — проект компиляции Go через систему компиляторов LLVM, развиваемый Google. Использует C++ парсер «gofrontend» из GCCGO и преобразователь из представления gofrontend в LLVM IR[39][40]
- SSA interpreter — интерпретатор, позволяющий запускать программы на go[41].
- TinyGo — компилятор Go, нацеленный на создание компактных исполняемых файлов для микроконтроллеров и WebAssembly с помощью LLVM.
Средства разработки
Среда разработки Go содержит несколько инструментов командной строки: утилиту go, обеспечивающий компиляцию, тестирование и управление пакетами, и вспомогательные утилиты godoc и gofmt, предназначенные, соответственно, для документирования программ и для форматирования исходного кода по стандартным правилам. Для вывода полного списка инструментов необходимо вызвать утилиту go без указания аргументов. Для отладки программ может использоваться отладчик gdb. Независимыми разработчиками представлено большое количество инструментов и библиотек, предназначенных для поддержки процесса разработки, главным образом, для облегчения анализа кода, тестирования и отладки.
На текущий момент доступны две IDE, изначально ориентированные на язык Go — это проприетарная GoLand [1] (разрабатывается в JetBrains на платформе IntelliJ) и свободная LiteIDE[2] (ранее проект назывался GoLangIDE). LiteIDE — небольшая по объёму оболочка, написанная на C++ с использованием Qt. Позволяет выполнять компиляцию, отладку, форматирование кода, запуск инструментов. Редактор поддерживает подсветку синтаксиса и автодополнение.
Также Go поддерживается плагинами в универсальных IDE Eclipse, NetBeans, IntelliJ, Komodo, CodeBox IDE, Visual Studio, Zeus и других. Автоподсветка, автодополнение кода на Go и запуск утилит компиляции и обработки кода реализованы в виде плагинов к более чем двум десяткам распространённых текстовых редакторов под различные платформы, в том числе Emacs, Vim, Notepad++, jEdit.
Примеры
Ниже представлен пример программы «Hello, World!» на языке Go.
package main
import "fmt"
func main() {
fmt.Println("Hello, World!")
}
Пример реализации команды Unix echo:
package main
import (
"os"
"flag" // парсер параметров командной строки
)
var omitNewLine = flag.Bool("n", false, "не печатать знак новой строки")
const (
Space = " "
NewLine = "\n"
)
func main() {
flag.Parse() // Сканирование списка аргументов и установка флагов
var s string
for i := 0; i < flag.NArg(); i++ {
if i > 0 {
s += Space
}
s += flag.Arg(i)
}
if !*omitNewLine {
s += NewLine
}
os.Stdout.WriteString(s)
}
Примечания
- Комментарии
- ↑ На 2019 год ни одна из реализаций Go не использует перемещающего сборщика мусора.
- Источники
- ↑ Release History
- ↑ https://go.dev/blog/13years
- ↑ https://go.dev/doc/faq
- ↑ 1 2 3 https://golang.org/doc/faq#ancestors
- ↑ 1 2 https://go.dev/doc/faq#ancestors
- ↑ https://talks.golang.org/2015/gophercon-goevolution.slide#19 — 2015.
- ↑ 1 2 http://golang.org/doc/go_faq.html#ancestors
- ↑ https://talks.golang.org/2014/hellogophers.slide#21
- ↑ Google-go-language . Дата обращения: 28 сентября 2017. Архивировано 18 января 2010 года.
- ↑ 1 2 3 4 5 6 Language Design FAQ . Дата обращения: 11 ноября 2013. Архивировано 7 января 2019 года.
- ↑ Getting Started — The Go Programming Language . Дата обращения: 11 ноября 2009. Архивировано 20 марта 2012 года.
- ↑ 1 2 Сообщение о конфликте имён в системе отслеживания ошибок . Дата обращения: 19 октября 2017. Архивировано 23 февраля 2018 года.
- ↑ 1 2 3 Go at Google: Language Design in the Service of Software Engineering . talks.golang.org. Дата обращения: 19 сентября 2017. Архивировано 25 января 2021 года.
- ↑ Rob Pike. The Go Programming Language. golang.org, 30.10.2009. Дата обращения: 3 ноября 2018. Архивировано 29 августа 2017 года.
- ↑ когда
m[-1]
означает последний элемент массива,m[-2]
— второй с конца и так далее - ↑ Andrew Gerrand. Defer, Panic, and Recover на GoBlog . Дата обращения: 19 марта 2016. Архивировано 20 апреля 2014 года.
- ↑ SWIG . Дата обращения: 27 ноября 2018. Архивировано 28 ноября 2018 года.
- ↑ Yager, Will Why Go is not Good . Дата обращения: 4 ноября 2018. Архивировано 16 июля 2019 года.
- ↑ Elbre, Egon Summary of Go Generics discussions . Дата обращения: 4 ноября 2018. Архивировано 15 июля 2019 года.
- ↑ Dobronszki, Janos Everyday Hassles in Go . Дата обращения: 4 ноября 2018. Архивировано 10 апреля 2019 года.
- ↑ Fitzpatrick, Brad Go: 90% Perfect, 100% of the time . Дата обращения: 28 января 2016. Архивировано 3 февраля 2019 года.
- ↑ Донован, 2016, с. 224—225.
- ↑ "Рейтинг языков программирования 2018: Go и TypeScript вошли в высшую лигу, Kotlin стоит воспринимать серьезно". ДОУ. Архивировано 4 августа 2020. Дата обращения: 29 июля 2018.
- ↑ Spiral Framework . Дата обращения: 23 мая 2020. Архивировано 13 мая 2020 года.
- ↑ https://golang.org/doc/devel/release.html Архивная копия от 17 февраля 2017 на Wayback Machine Версии Go.
- ↑ https://golang.org/doc/go1compat Архивная копия от 2 октября 2017 на Wayback Machine Go 1 и будущие релизы Go.
- ↑ 1 2 Toward Go 2 - The Go Blog . blog.golang.org. Дата обращения: 29 июля 2018. Архивировано 26 июня 2018 года.
- ↑ golang/go (англ.). GitHub. Дата обращения: 29 июля 2018. Архивировано 29 августа 2018 года.
- ↑ 1 2 Russ Cox, «Eleven Years of Go» . Дата обращения: 26 ноября 2020. Архивировано 27 ноября 2020 года.
- ↑ Go2 Here we come! Дата обращения: 6 декабря 2018. Архивировано 1 декабря 2018 года.
- ↑ Contracts — Draft Design (англ.). go.googlesource.com. Дата обращения: 11 октября 2018. Архивировано 11 октября 2018 года.
- ↑ https://go.googlesource.com/proposal/+/refs/heads/master/design/go2draft-type-parameters.md Архивная копия от 23 июня 2020 на Wayback Machine Type Parameters — Draft Design
- ↑ Go FAQ: Implementation . Дата обращения: 11 ноября 2013. Архивировано 7 января 2019 года.
- ↑ https://gcc.gnu.org/gcc-4.6/changes.html Архивная копия от 2 декабря 2013 на Wayback Machine «Support for the Go programming language has been added to GCC.»
- ↑ Setting up and using gccgo - The Go Programming Language . golang.org. Дата обращения: 23 ноября 2018. Архивировано 23 ноября 2018 года.
- ↑ GCC 8 Release Series — Changes, New Features, and Fixes - GNU Project - Free Software Foundation (FSF) (англ.). gcc.gnu.org. Дата обращения: 23 ноября 2018. Архивировано 29 ноября 2018 года.
- ↑ go-llvm Архивная копия от 11 сентября 2014 на Wayback Machine; перемещен в llvm-mirror/llgo Архивная копия от 11 июня 2018 на Wayback Machine
- ↑ Архивированная копия . Дата обращения: 2 ноября 2018. Архивировано 22 марта 2017 года.
- ↑ gollvm — Git at Google . Дата обращения: 2 ноября 2018. Архивировано 8 декабря 2018 года.
- ↑ "Gollvm: Google Working On LLVM-Based Go Compiler" (англ.). Phoronix. 2017-05-29. Архивировано 12 октября 2018. Дата обращения: 2 ноября 2018.
- ↑ interp — GoDoc . Дата обращения: 2 ноября 2018. Архивировано 29 мая 2019 года.
Литература
- Донован, Алан А. А., Керниган, Брайан, У. Язык программирования Go = The Go Programming Language. — М.: ООО «И.Д. Вильямс», 2016. — С. 432. — ISBN 978-5-8459-2051-5.
- Батчер М., Фарина М. Go на практике. — «ДМК Пресс», 2017. — С. 374. — ISBN 978-5-97060-477-9.
- Марк Саммерфильд. Программирование на Go. Разработка приложений XXI века. — «ДМК Пресс», 2013. — С. 580. — ISBN 978-5-94074-854-0.
Ссылки
- Официальный сайт Go (англ.)
- A Tour of Go (англ.)
- Официальный сайт-компаньон golang.org и хаб (англ.)
- Сравнение Go с C++ (из официальной документации) (рус.)
- Вопросы и ответы по языку Go (англ.)
- Go By Example — практическое введение в язык с помощью аннотированных примеров программ (англ.)
- Awesome Go — отборный список фреймворков, библиотек и программного обеспечения на языке Go с открытым исходным кодом (англ.)
- Языки программирования по алфавиту
- Языки программирования, появившиеся в 2009 году
- Ожидаемые события
- Языки программирования семейства Си
- Многопоточные языки программирования
- Процедурные языки программирования
- Языки программирования с автоматическим управлением памятью
- Свободные компиляторы и интерпретаторы
- Кроссплатформенное программное обеспечение