Go

Материал из Википедии — свободной энциклопедии
Перейти к: навигация, поиск
Go
Golang.png
Класс языка:

многопоточный, императивный, структурированный

Тип исполнения:

компилируемый

Появился в:

2009

Автор:

Роберт Гризмер, Роб Пайк и Кен Томпсон

Расширение файлов:

.go

Выпуск:

1.6 (17 февраля 2016)[1]

Система типов:

строгая, статическая, с выводом типов

Основные реализации:

gc (8g, 6g, 5g), gccgo

Испытал влияние:

Си, Python, Оберон-2, Активный Оберон, Limbo

Лицензия:

лицензия BSD

Сайт:

golang.org[2]

ОС:

кроссплатформенное программное обеспечение

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

Название[править | править вики-текст]

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

Назначение и основные возможности языка[править | править вики-текст]

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

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

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

При этом из языка сознательно исключены:

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

Синтаксис[править | править вики-текст]

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

Go — регистро-зависимый язык с полной поддержкой Юникода в строках и идентификаторах
Идентификатор традиционно может быть любой непустой последовательностью, включающей буквы, цифры и знак подчёркивания, начинающийся с буквы и не совпадающий ни с одним из ключевых слов языка Go. При этом под «буквами» понимаются все символы Юникода, относящиеся к категориям «Lu» (буквы верхнего регистра), «Ll» (буквы нижнего регистра), «Lt» (заглавные буквы), «Lm» (буквы-модификаторы) или «Lo» (прочие буквы), под «цифрами» — все символы из категории «Nd» (числа, десятичные цифры). Таким образом, ничто не мешает использовать в идентификаторах, например, русские буквы.
Идентификаторы, различающиеся только регистром букв, являются различными. В языке существует ряд соглашений об использовании заглавных и строчных букв. В частности, в именах пакетов используются только строчные буквы. Все ключевые слова 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 отформатированы этой утилитой.
Объявление типа
Синтаксис объявления типа, в основном, решён в духе Паскаля.
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;  (но нет арифметики для указателей)
 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
Количество переменных, которым присваивается результат вызова функции, должно точно совпадать с количеством возвращаемых функцией значений. Если какой-то из результатов функции не нужен для дальнейшей работы программы, для него можно указать специальную предопределённую переменную «_». Тогда соответствующее значение не будет присвоено никакой переменной и просто потеряется. Так, в примере выше, если функция incTwo возвращает два значения, а из них нужно только одно:
first := incTwo(1, 2) // НЕВЕРНО
first, _ := incTwo(1, 2) // ВЕРНО, второй результат не используется
Переменная «_» может указываться в списке присваивания любое число раз. Все результаты функции, которым соответствует «_», будут проигнорированы.
Механизм отложенного вызова defer
Отложенный вызов заменяет сразу несколько синтаксических средств, в частности, обработчики исключений и блоки с гарантированным завершением. Вызов функции, которому предшествует ключевое слово defer, параметризуется в той точке программы, где размещён, а выполняется непосредственно перед выходом программы из области видимости, где он был объявлен, независимо от того, как и по какой причине происходит этот выход. Ниже пример использования defer в качестве блока гарантированного завершения[7]
// Функция, копирующая файл
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 {
// используется только элемент массива
}

Особенности архитектуры[править | править вики-текст]

Обработка ошибок и исключительных ситуаций[править | править вики-текст]

Язык 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, err 
    } 
    ... // Дальнейшее исполнение функции, если ошибки не было
    return result, nil  // Возврат результата и пустой ошибки, если выполнение успешно
}
  • Проигнорировать ошибку, возвращаемую из функции (в примере выше — не проверить значение переменной err) невозможно, так как инициализация переменной без последующего использования в языке Go приводит к ошибке компиляции. Конечно, этот эффект можно обойти подстановкой вместо err псевдопеременной «_», но это считается плохой практикой и, во всяком случае, явно бросается в глаза при просмотре кода.

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

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

Многопоточность[править | править вики-текст]

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

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

Объектно-ориентированное программирование[править | править вики-текст]

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

type newInt int

Синтаксис определения метода заимствован из языка Оберон-2 и отличается от обычного определения функции тем, что указывается получатель англ. receiver (похож на указатель this в методе класса C++).

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

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

type myInterface interface {
	get() int
	set(i int)
}

Объявленный выше тип myType реализует интерфейс myInterface, хотя это нигде не указано явно.

Такой подход к наследованию соответствует некоторым практическим тенденциям современного программирования. Так в знаменитой книге «банды четырёх» (Эрих Гамма и др.) о паттернах проектирования, в частности, написано:

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

Динамическая поддержка объектно-ориентированного программирования для Go осуществлена с помощью проекта GOOP.

История версий[править | править вики-текст]

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

С марта 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 AMR, 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:

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

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

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

Примеры[править | править вики-текст]

Ниже представлен пример программы «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)
}

Примечания[править | править вики-текст]

Ссылки[править | править вики-текст]