Go — большая энциклопедия. Что такое Go

Go


Материал из свободной энциклопедии
Go
Go Logo Aqua.svg
Класс языка многопоточный, императивный, структурированный
Тип исполнения компилируемый
Появился в 2009
Автор Роберт Гризмер, Роб Пайк и Кен Томпсон
Расширение файлов .go
Выпуск
Система типов строгая, статическая, с выводом типов
Основные реализации: gc (8g, 6g, 5g), gccgo
Испытал влияние Си, Паскаль/Модула/Оберон[2], Limbo
Лицензия BSD[3]
Сайт golang.org

Go (часто также Golang) — компилируемый многопоточный язык программирования, разработанный внутри компании Google[4]. Первоначальная разработка Go началась в сентябре 2007 года, а его непосредственным проектированием занимались Роберт Гризмер, Роб Пайк и Кен Томпсон[5], занимавшиеся до этого проектом разработки операционной системы Inferno. Официально язык был представлен в ноябре 2009 года. На данный момент его поддержка осуществляется для операционных систем FreeBSD, OpenBSD, Linux, macOS, Windows[6], начиная с версии 1.3 в язык Go включена экспериментальная поддержка DragonFly BSD, Plan 9 и Solaris, начиная с версии 1.4 — поддержка платформы Android.

Название

Название языка, выбранное компанией Google, практически совпадает с названием языка программирования Go!, созданного Ф. Джи. МакКейбом и К. Л. Кларком в 2003 году[7]. Обсуждение названия ведётся на странице, посвящённой Go[7].

Назначение

Язык Go разрабатывался как язык программирования для создания высокоэффективных программ, работающих на современных распределённых системах и многоядерных процессорах. Он может рассматриваться как попытка создать замену языкам Си и C++[8]. По словам Роба Пайка[8], «Go был разработан для решения реальных проблем, возникающих при разработке программного обеспечения в Google». В качестве основных таких проблем он называет:

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

В результате получился язык, «который не стал прорывом, но тем не менее явился отличным инструментом для разработки крупных программных проектов»[8].

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

Основные возможности языка

Основные возможности языка Go[5]:

  • Go — язык со строгой статической типизацией. Доступен автоматический вывод типов, для пользовательских типов — «утиная типизация».
  • Полноценная поддержка указателей, но без возможности применять к ним арифметические операции, в отличие от C/C++/D.
  • Строковый тип со встроенной поддержкой юникода.
  • Использование динамических массивов, хэш-таблиц, срезов (слайсов), вариант цикла для обхода коллекции.
  • Средства функционального программирования: неименованные функции, замыкания, передача функций в параметрах и возврат функциональных значений.
  • Автоматическое управление памятью со сборщиком мусора.
  • Средства объектно-ориентированного программирования, но без поддержки наследования реализации (наследуются только интерфейсы). По большому счёту, Go является процедурным языком с поддержкой интерфейсов.
  • Средства параллельного программирования: встроенные в язык потоки (go routines), взаимодействие потоков через каналы и другие средства организации многопоточных программ.
  • Достаточно лаконичный и простой синтаксис, основанный на Си, но существенно доработанный, с большим количеством синтаксического сахара.

При этом в язык сознательно не был включён ряд популярных средств.

  • Структурная запись обработчиков исключений сочтена провоцирующей на пропуск ошибок или неадекватную их обработку. Вместо неё предлагается проверка кодов возврата с использованием многозначных функций и специального интерфейса error, а также применение отложенных (deferred) функций для перехвата исключительных ситуаций.
  • Наследование реализации, как считают авторы, приводит к трудноподдерживаемому коду с неявными зависимостями. Аналогичные возможности, но без свойственных наследованию нежелательных эффектов, обеспечиваются поддержкой вложения типов и свободно определяемыми интерфейсами.
  • Обобщённое программирование. Авторы воздерживались от его реализации, поскольку, по их словам[5], предоставляемые им возможности не окупают требуемого усложнения компилятора и runtime-библиотек, а уже имеющиеся в языке средства (пустые интерфейсы, «утиная типизация» и рефлексия) позволяют создавать обобщённый код без специальных синтаксических механизмов. Тем не менее, обсуждается вопрос о включении таких средств в проектируемую вторую версию языка, выход которой запланирован на 2019 год.
  • Использование утверждений было сочтено ненужным.
  • Переопределение методов и функций было исключено из соображений эффективности компиляции: требование различного именования устраняет необходимость сопоставлять списки параметров при компиляции вызовов функций и методов, при том что сама эта возможность есть не более чем синтаксический сахар.

Язык продолжает развиваться, и разработчики рассматривают возможность включения в язык средств обобщённого программирования. В «Часто задаваемых вопросах»[5] по языку приводятся аргументы против использования утверждений, а наследование без указания типа, наоборот, отстаивается.

Синтаксис

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

Go — регистрозависимый язык с полной поддержкой Юникода в строках и идентификаторах
Идентификатор традиционно может быть любой непустой последовательностью, включающей буквы, цифры и знак подчёркивания, начинающийся с буквы и не совпадающий ни с одним из ключевых слов языка Go. При этом под «буквами» понимаются все символы Юникода, относящиеся к категориям «Lu» (буквы верхнего регистра), «Ll» (буквы нижнего регистра), «Lt» (заглавные буквы), «Lm» (буквы-модификаторы) или «Lo» (прочие буквы), под «цифрами» — все символы из категории «Nd» (числа, десятичные цифры). Таким образом, ничто не мешает использовать в идентификаторах, например, кириллицу.
Идентификаторы, различающиеся только регистром букв, являются различными. В языке существует ряд соглашений об использовании заглавных и строчных букв. В частности, в именах пакетов используются только строчные буквы. Все ключевые слова Go пишутся в нижнем регистре.
Естественно, в строковых литералах могут использоваться все символы Юникода без ограничений.
Система пакетов
Любая программа на Go включает один или несколько пакетов. Пакет, к которому относится файл исходного кода, задаётся описанием package в начале файла. Имена пакетов имеют те же ограничения, что и идентификаторы, но могут содержать буквы только нижнего регистра. Система пакетов go-среды имеет древовидную структуру, аналогичную дереву каталогов. Любые глобальные объекты (переменные, типы, интерфейсы, функции, методы, элементы структур и интерфейсов) доступны без ограничений в пакете, в котором они объявлены. Глобальные объекты, имена которых начинаются на заглавную букву, являются экспортируемыми. Если в каком-то модуле (файле кода) Go имеется команда импорта данного пакета import, только экспортируемые имена пакета видны (доступны) в данном модуле. Доступ к импортируемым объектам всегда требует квалификации: указывается имя пакета, после которого через точку — имя экспортируемого пакетом объекта.
Исполняемая программа на Go обязательно должна иметь пакет с именем main. В этом пакете обязательно должна быть функция main() без параметров и возвращаемого значения, а также, возможно, функции init(). Функция main.main() является «телом программы» — её код запускается, когда программа стартует. Функции init() могут использоваться для инициализации пакета — они выполняются при загрузке программы перед началом её исполнения. Система гарантирует, что функции init() импортируемого пакета всегда вызываются раньше функций init() импортирующего. Таким образом, инициализация пакета main всегда происходит последней, и все инициализации выполняются до начала исполнения функции main.main().
Комментарии и точки с запятой
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 отформатированы этой утилитой.
Объявление типов
Язык содержит достаточно стандартный набор простых встроенных типов данных (int, float, char, bool), а также тип complex — комплексное число, для которого поддерживаются все математические операции и сравнения на равенство и неравенство, и встроенный строковый тип string. Для этих типов могут объявляться новые типы-аналоги, повторяющие все свойства оригиналов, но несовместимые с ними. Для этих новых типов также могут дополнительно объявляться методы.
Пользовательскими типами данных в Go являются массивы (объявляются без отдельного ключевого слова), отображения (map), структуры (struct), каналы (chan) и функции (func). В описаниях этих типов указываются типы и, возможно, идентификаторы их элементов.
Новые типы объявляются с помощью ключевого слова 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  CoompareFunc ( a, b interface {} ) int   // тип-функция.
Начиная с версии Go 1.9 также доступно объявление алиасов (псевдонимов) типов:
type TitleString=string  // "TitleString" - псевдоним для встроенного типа string

type Integer=int64  // "Integer" - псевдоним для встроенного 64-разрядного целого типа
Алиас может быть объявлен как для системного, так и для любого пользовательского типа. Принципиальным отличием алиасов от обычных объявлений типов является то, что при объявлении создаётся новый тип, который не совместим с оригиналом, даже если в объявлении к оригинальному типу никаких изменений не добавляется. Алиас же — это просто другое имя того же типа, то есть алиас и оригинальный тип полностью взаимозаменимы.
Объявление переменных
Синтаксис объявления переменных, в основном, решён в духе Паскаля: объявление начинается с ключевого слова var, за которым через разделитель следует имя переменной, далее, через разделитель — её тип.
Go C++
 var v1 int                
 var 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);
При объявлении переменные инициализируются на нулевое значение для данного типа (0 для int, пустая строка для string, nil для указателей).
Объявления можно группировать:
var (
	i int
	m float
)
Автоматический вывод типов
Язык Go поддерживает также автоматический вывод типов. Переменная может быть инициализирована при объявлении, её тип при этом можно не указывать — типом переменной становится тип присваиваемого ей выражения. Для литералов (чисел, символов, строк) стандарт языка определяет конкретные встроенные типы, к которым относится каждое такое значение. Чтобы инициализировать переменную другого типа, к литералу необходимо применить явное преобразование типа.
var v = *p
Присваивания
Внутри функции короткий синтаксис присваивания переменным значения с автоматическим выводом типов напоминает обычное присваивание в Паскале:
v1 := v2 // аналог var v1 = v2
Go допускает множественные присваивания, выполняемые параллельно:
i, j = j, i    // Поменять местами значения i и j.
Аргументы функций и методов
объявляются таким образом:
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
}
Несколько значений, возвращаемых функциями, присваиваются переменным их перечислением через запятую, при этом количество переменных, которым присваивается результат вызова функции, должно точно совпадать с количеством возвращаемых функцией значений:
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 в качестве блока гарантированного завершения[9]:
// Функция, копирующая файл
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()
}
Прочие синтаксические различия
Отсутствуют круглые скобки для условных конструкций for и if:
func print(arr []int) {
    n := len(arr)
    for i := 0; i < n; i++ {
        println(arr[i])
    }
}
Любые циклы в Go описываются с помощью конструкции for, синтаксических аналогов while, do-while и других вариантов циклической конструкции в языке нет. Но конструкция for позволяет организовать цикл любого желаемого вида.
for { // бесконечный цикл
    // Выход из цикла должен быть организован вручную,
    // обычно это делается с помощью конструкций return или break
}

for i < 10 { // цикл выполняется, пока условие истинно (аналог while в Си)
}

for i := 0; i < 10; i++ { // точно то же самое, что цикл for в Си
}

var arr []int
for i, v := range arr { // цикл по элементам массива или среза arr
// i - индекс текущего элемента
// v - копия текущего элемента (аналог arr[i])
}

for i := range arr {
// используется только индекс
}

for _, v := range arr {
// используется только элемент массива
}

for range arr {  //цикл по коллекции без переменных - поддерживается с версии 1.4
    // Может использоваться, когда коллекция используется только в качестве счётчика итераций,
    // а само текущее значение не требуется.
}

Особенности архитектуры

Обработка ошибок и исключительных ситуаций

Язык Go не поддерживает типичный для большинства современных языков синтаксис структурной обработки исключений (блоки try-catch); авторы сочли, что его применение провоцирует программиста на игнорирование ошибок.

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

  • В одном из параметров (обычно последнем) функция возвращает объект-ошибку. В качестве типа ошибки обычно используется библиотечный интерфейс error. Типичной практикой является возврат пустого указателя nil, если функция выполнилась без ошибок.
  • После вызова полученный из функции объект проверяется и ошибка, если она возникла, обрабатывается. Если ошибка в месте вызова не может быть адекватно обработана, она обычно возвращается в качестве результата текущей функции, либо на её основе создаётся новая ошибка, которая и возвращается.
func ReadFile(srcName string)(result string, err error) {
    file, err := os.Open("file.txt")
    if err != nil {
        // Генерация новой ошибки с уточняющим текстом
        return nil, fmt.Errorf("Ошибка при чтении файла %s: %g\n", srcName, err) 
    } 
    ... // Дальнейшее исполнение функции, если ошибки не было
    return result, nil  // Возврат результата и пустой ошибки, если выполнение успешно
}
  • Проигнорировать ошибку, возвращаемую из функции (в примере выше — не проверить значение переменной err) невозможно, так как инициализация переменной без последующего использования в языке Go приводит к ошибке компиляции. Конечно, этот эффект можно обойти подстановкой вместо err псевдопеременной «_», но это считается плохой практикой и, во всяком случае, явно бросается в глаза при просмотре кода.

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

При возникновении фатальных ошибок, делающих невозможным дальнейшее исполнение программы, возникает состояние «паники» (panic). Типичный пример возникновения паники — деление на ноль в процессе вычислений либо обращение за границы массива. В отсутствие обработки паника приводит к аварийному завершению программы с выдачей сообщения об ошибке и трассировки стека вызовов. Для обеспечения отказоустойчивости программы паника тоже может быть перехвачена и обработана. Для этого используется механизм отложенного исполнения defer. Инструкция defer, как говорилось выше, получает в качестве параметра вызов функции (то есть фактически создаёт замыкание), который производится тогда, когда исполнение программы покидает текущую область видимости. Это происходит даже в случае паники. Для её перехвата в функции, вызываемой в defer, необходимо вызвать стандартную функцию recover() — она прекращает системную обработку паники и возвращает её причину в виде объекта error. Далее программист может обработать полученную ошибку любым желаемым образом, в том числе и возобновить панику, вызвав стандартную функцию panic(err error).

Многопоточность

Модель многопоточности Go была создана на основе CSP Тони Хоара по типу предыдущих распараллеливаемых языков программирования Occam и Limbo[5], но также присутствуют такие особенности как Пи-исчисления и канальная передача.

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), через которые можно передавать любые значения. Для передачи значения в канал используется <- в качестве бинарного оператора, для получения сообщения из канала — <- в качестве унарного оператора.

Помимо CSP или совместно с механизмом канальной передачи Go позволяет использовать и обычную модель синхронизированного взаимодействия потоков через общую память, с использованием типовых средств синхронизации доступа, таких как мьютексы. Особенностью многопоточности в 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, который при необходимости можно использовать. Также можно обратить внимание на использованный в последнем примере способ вывода результатов. Обычно вывод на консоль в Go производится функциями пакета fmt (Printf, Println и так далее). Однако этот пакет не является потоко-безопасным, и для использования его функций в go-процедурах пришлось бы дополнительно синхронизировать их вызовы. В примере проблема обойдена с помощью использования потоко-безопасного типа log.Logger, который также содержит методы для вывода текстов и может быть использован для вывода на консоль.

Объектно-ориентированное программирование

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

type newInt int

Синтаксис определения метода заимствован из языка Оберон-2 и отличается от обычного определения функции тем, что после ключевого слова func в круглых скобках объявляется так называемый «получатель» (англ. receiver), то есть объект, для которого вызывается метод, и тип, к которому относится метод. Если в традиционных объектных языках получатель обычно имеет стандартное имя (в C++ или Java — «this», в ObjectPascal — «self» и т. п.), то в Go он указывается явно и его имя может быть любым правильным Go-идентификатором.

type myType struct { i int }
func (p *myType) get() int { return p.i }
func (p *myType) set(i int) { p.i = i }

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

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

Полиморфизм классов обеспечивается в 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. В основном критика языка фокусируется на отсутствии в нём тех или иных популярных средств, предоставляемых другими языками. В первую очередь это средства обобщённого программирования (generics) и структурная обработка исключений. Также часто критикуется отсутствие «полноценного ООП» (фактически — наследования реализации), перегрузки функций, переопределения операторов. Типичным ответом на критику подобного рода является утверждение, что средства, за отсутствие которых критикуется язык, не являются действительно необходимыми либо имеют нежелательные побочные эффекты использования, а то, что делается с их помощью в других языках, может быть реализовано в Go его имеющимися средствами.

Распространение и перспективы

Популярность Go в последние годы росла: с 2014 года в рейтинге TIOBE он поднялся с 65-го места на 18-е, текущее значение рейтинга составляет около 1 %. По результатам опроса сайта dou.ua[10] язык Go в 2018 году стал девятым в списке самых используемых и шестым в списке языков, которым отдают личное предпочтение разработчики. При этом с 2012 года, когда вышел первый публичный релиз, использование языка неуклонно растёт. В опубликованном на сайте проекта Go списке компаний, использующих язык в промышленных разработках, насчитывается несколько десятков наименований.

Накоплен большой массив библиотек различного назначения. Поскольку исторически язык ориентировался, в основном, на написание бэк-энда (серверной части веб-проектов), системные библиотеки более развиты в части поддержки сетевых технологий, преобразования и обработки данных, чем в части средств реализации интерфейса пользователя. Практически единственным стандартным способом создать не-консольное клиентское приложение является написание локального сервера с веб-интерфейсом. Существуют созданные сторонними разработчиками библиотеки, обеспечивающие интерфейс с популярными UI-фреймворками, такими как GTK+ и Qt, но они довольно громоздки. Имеется также несколько разработок UI-фреймворков на самом Go, но ни один из этих проектов не достиг уровня промышленной применимости. В 2015 году на конференции GopherCon 2015 в Денвере один из создателей языка, Роберт Грисмер, отвечая на вопросы, согласился, что Go нуждается в пакете UI, но заметил, что такой пакет должен быть универсальным, мощным и мультиплатформенным, что делает его разработку длительным и непростым процессом. Во всяком случае, на конец 2018 года вопрос о реализации клиентского GUI на Go остаётся открытым.

Версии

Нумерация и принципы совместимости версий

Cуществует только одна основная версия самого языка Go — версия 1. Версии среды разработки (компилятора, инструментария и стандартных библиотек) Go нумеруются по двухзначной («<версия языка>.<основной релиз>») либо трёхзначной («<версия языка>.<основной релиз>.<дополнительный релиз>») системе. Выпуск новой «двузначной» версии автоматически означает прекращение поддержки предыдущей «двузначной» версии. «Трёхзначные» версии выпускаются для исправления обнаруженных ошибок и проблем с безопасностью; исправления безопасности в таких версиях могут затрагивать две последние «двузначные» версии[11].

Авторы декларировали[12] стремление к сохранению, насколько это возможно, обратной совместимости в пределах основной версии языка. Это означает, что до выхода релиза 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 2.0

С 2017 года ведётся активная подготовка к выпуску следующей базовой версии языка, имеющей условное обозначение «Go 2.0»[13]. Проводится сбор замечаний к текущей версии и предложений по преобразованиям, аккумулируемых на wiki-сайте проекта[14]. Точных сроков выхода новой версии не называется, было лишь сказано, что процесс подготовки займёт «около двух лет», причём часть новых элементов языка будет включена уже в очередные релизы версии Go 1 (разумеется, только те, которые не нарушают обратной совместимости).[13] В числе возможных принципиальных новшеств назывались явно объявляемые константные значения, новый механизм обработки ошибок и средства обобщённого программирования. В сети доступны проекты нововведений. 28 августа 2018 года в официальном блоге разработчиков был опубликован ролик, ранее представленный на конференции Gophercon 2018, в котором демонстрируются черновые варианты нового дизайна обработки ошибок и механизма обобщённых функций.

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

func CopyFile(src, dst string) error {
    handle err {
        return fmt.Errorf("copy %s to %s error: %v", src, dst, err)
    }
    
    r := check os.Open(src)
    defer r.Close()

    w := check os.Create(dst)
    defer w.Close()

    check io.Copy(w, r)
    check w.Close()

    return nil
}

Как видно из примера (простейшая функция копирования файла), предлагается добавить две конструкции: check и handle. Первая используется в вызове и «перехватывает» последнее возвращаемое функцией значение, предполагая, что это — объект error; если он равен nil, то остальные значения передаются по назначению, если же нет, то управление передаётся в блок handle, размещённый выше по тексту в этой же функции. В данном примере при ошибке в любом из вызовов функция CopyFile вернёт объект-ошибку с текстом, содержащим её параметры и текст ошибки, полученной в неудачной операции. Возможно наличие нескольких блоков handle в одной функции, в этом случае они будут выполняться в обратном порядке, до первого оператора return, который обязательно должен находиться в одном из них. В отсутствие блоков обработки конструкция check приведёт к тому, что произойдёт немедленный возврат из текущей функции, причём в последний по счёту результат будет передана полученная при вызове ошибка, а остальные результаты, если они есть, сохранят свои текущие значения.

Механизм обобщённых функций основывается на двух элементах: «тип-параметр» и «контракт»[15].

contract Equal(t T) {
    t == t
}

func Uniq(type T Equal)(in <-chan T) <-chan T {
    out := make(chan T)
    go func() {
        v := <-in
        out <- v
        for next := range in {
            if v != next {
                v = next
                out <- v
            }
        }
    }()
}

...
src := make(chan string)
...
// Вызов обобщённой функции для конкретного типа string
for s := range Uniq(string)(src) {
    fmt.Println(s)
}

Тип параметра обобщённой функции (здесь это T) описывается в её заголовке с ключевым словом type и указанием ранее описанного контракта (здесь — Equal). Описание контракта содержит код, который обрабатывается компилятором как обычная функция, но без генерации кода. Если некоторый тип будет использован для конкретизации функции Uniq, то для этого типа компилятор обработает описание контракта Equal, и компиляция программы будет успешна только в случае, если эта обработка не приведёт к обнаружению ошибки. В данном случае проверка будет успешной, если к значениям типа может применяться операция проверки на равенство. Используя контракты, разработчики рассчитывают избежать ситуации, когда в конкретизации обобщённого кода используются типы, не обладающие необходимыми качествами, что может приводить к ошибкам, проявляющимся лишь на этапе исполнения.[15]

Реализации

На данный момент существуют два основных компилятора Go:

  • 6g (и сопутствующие ему инструменты, вместе известные под названием gc) написан на Си с применением yacc/Bison для парсера
  • Gccgo — ещё один компилятор Go с клиентской частью, написанной на C++, и рекурсивным парсером, совмещённым со стандартным бэк-эндом GCC[16]. Поддержка Go доступна в GCC, начиная с версии 4.6[17].

А также перспективные разработки:

  • llgo — прослойка для компиляции в llvm, написанная на самом go (находится в разработке)[18].
  • SSA interpreter — интерпретатор, позволяющий запускать программы на go.

Средства разработки

Среда разработки Go содержит несколько инструментов командной строки: утилиту go, обеспечивающий компиляцию, тестирование и управление пакетами, и вспомогательные утилиты godoc и gofmt, предназначенные, соответственно, для документирования программ и для форматирования исходного кода по стандартным правилам. Для вывода полного списка инструментов необходимо вызвать утилиту go без указания аргументов. Для отладки программ может использоваться отладчик gdb. Независимыми разработчиками представлено большое количество инструментов и библиотек, предназначенных для поддержки процесса разработки, главным образом, для облегчения анализа кода, тестирования и отладки.

На текущий момент доступны две IDE, изначально ориентированные на язык Go — это проприетарная GoLand [1] (разрабатывается в JetBrains на платформе IntelliJ) и свободная LiteIDE[2] (ранее проект назывался GoLangIDE). LiteIDE — небольшая по объёму оболочка, написанная на С++ с использованием qt4, с помощью которой можно выполнять весь базовый набор действий по разработке ПО на Go: создание кода (редактор поддерживает подсветку синтаксиса и автодополнение), компиляцию, отладку, форматирование кода, запуск инструментов.

Также 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)
}

Примечания

  1. Release History
  2. The Go Project - The Go Programming Language - FAQ. golang.org. Проверено 26 августа 2018.
  3. LICENSE-file
  4. Google-go-language
  5. 1 2 3 4 5 Language Design FAQ
  6. Getting Started — The Go Programming Language
  7. 1 2 Сообщение о конфликте имён в системе отслеживания ошибок
  8. 1 2 3 Go at Google: Language Design in the Service of Software Engineering. talks.golang.org. Проверено 19 сентября 2017.
  9. Andrew Gerrand. Defer, Panic, and Recover на GoBlog
  10. Рейтинг языков программирования 2018: Go и TypeScript вошли в высшую лигу, Kotlin стоит воспринимать серьезно (рус.), ДОУ. Проверено 29 июля 2018.
  11. https://golang.org/doc/devel/release.html Версии Go.
  12. https://golang.org/doc/go1compat Go 1 и будущие релизы Go.
  13. 1 2 Toward Go 2 - The Go Blog. blog.golang.org. Проверено 29 июля 2018.
  14. golang/go (англ.). GitHub. Проверено 29 июля 2018.
  15. 1 2 Contracts — Draft Design (англ.). go.googlesource.com. Проверено 11 октября 2018.
  16. Go FAQ: Implementation
  17. https://gcc.gnu.org/gcc-4.6/changes.html «Support for the Go programming language has been added to GCC. It is not enabled by default when you build GCC»
  18. go-llvm/llgo · GitHub

Ссылки



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

Основа этой страницы находится в Вики. Текст доступен по официальной лицензии CC BY-SA 3.0 Unported License.

Wikipedia® — зарегистрированный товарный знак организации Wikimedia Foundation, Inc. infor24.ru является независимой компанией и не аффилирована с Фондом Викимедиа (Wikimedia Foundation). Сайт infor24.ru является неофициальным сайтом.

E-mail: admin@infor24.ru