Шефер, Шарль Анри Огюст
- 1 year ago
- 0
- 0
Go (часто также golang ) — компилируемый многопоточный язык программирования , разработанный внутри компании Google . Разработка Go началась в сентябре 2007 года, его непосредственным проектированием занимались , Роб Пайк и Кен Томпсон , занимавшиеся до этого проектом разработки операционной системы Inferno . Официально язык был представлен в ноябре 2009 года . На данный момент поддержка официального компилятора, разрабатываемого создателями языка, осуществляется для операционных систем FreeBSD , OpenBSD , Linux , macOS , Windows , DragonFly BSD , Plan 9 , Solaris , Android , AIX . . Также Go поддерживается набором компиляторов gcc , существует несколько независимых реализаций.
Название языка, выбранное компанией Google, практически совпадает с названием языка программирования Go! , созданного Ф. Джи. Маккейбом и К. Л. Кларком в 2003 году . Обсуждение названия ведётся на странице, посвящённой Go .
На домашней странице языка и вообще в Интернет-публикациях часто используется альтернативное название — «golang».
Язык Go разрабатывался как язык программирования для создания высокоэффективных программ, работающих на современных распределённых системах и многоядерных процессорах. Он может рассматриваться как попытка создать замену языкам Си и C++ с учётом изменившихся компьютерных технологий и накопленного опыта разработки крупных систем . По словам Роба Пайка , «Go был разработан для решения реальных проблем, возникающих при разработке программного обеспечения в Google». В качестве основных таких проблем он называет:
Основными требованиями к языку стали :
Go создавался в расчёте на то, что программы на нём будут транслироваться в объектный код и исполняться непосредственно, не требуя виртуальной машины , поэтому одним из критериев выбора архитектурных решений была возможность обеспечить быструю компиляцию в эффективный объектный код и отсутствие чрезмерных требований к динамической поддержке.
В результате получился язык, «который не стал прорывом, но тем не менее явился отличным инструментом для разработки крупных программных проектов» .
Хотя для Go доступен и интерпретатор , практически в нём нет большой потребности, так как скорость компиляции достаточно высока для обеспечения интерактивной разработки.
Основные возможности языка Go :
Go не содержит целого ряда популярных синтаксических средств, доступных в других современных языках прикладного программирования. Во многих случаях это вызвано сознательным решением разработчиков. Краткие обоснования выбранных проектных решений можно найти в «Часто задаваемых вопросах» по языку, более подробные — в опубликованных на сайте языка статьях и обсуждениях, рассматривающих различные варианты дизайна. В частности:
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, командой инкремента или декремента (++ или --) или закрывающей круглой, квадратной или фигурной скобкой (важное исключение — запятая в приведённый список не входит). Из этого следует две особенности:
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 { // ВЕРНО
}
Язык содержит достаточно стандартный набор простых встроенных типов данных: целые числа, числа с плавающей запятой, символы, строки, логические значения, а также несколько специальных типов.
Имеется 11 целочисленных типов:
int8
,
int16
,
int32
,
int64
. Это целые числа со знаком, представленные в
дополнительном коде
, размер значений этих типов — 8, 16, 32, 64 бита соответственно. Диапазон значений составляет от −2
n−1
до 2
n−1
−1, где n — размер типа.
uint8
,
uint16
,
uint32
,
uint64
. Число в названии типа, как и в предыдущем случае, задаёт размер, но диапазон значений составляет от 0 до 2
n
−1.
int
и
uint
— соответственно, знаковое и беззнаковое целое число. Размер этих типов одинаков, и может быть 32 или 64 бита, но не фиксируется спецификацией языка и может выбираться реализацией. Предполагается, что для них будет выбран наиболее эффективный на целевой платформе размер.
byte
— синоним
uint8
. Предназначается, как правило, для работы с неформатированными бинарными данными.
rune
— синоним
int32
, представляет символ в кодировке 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 в качестве блока гарантированного завершения :
// Функция, копирующая файл
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
,
:
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("чтение файла %q: %w", srcName, err)
}
... // Дальнейшее исполнение функции, если ошибки не было
return result, nil // Возврат результата и пустой ошибки, если выполнение успешно
}
err
), невозможно, так как инициализация переменной без последующего использования в языке Go приводит к ошибке компиляции. Это ограничение можно обойти подстановкой вместо err псевдопеременной
_
, но это явно бросается в глаза при просмотре кода.
Многие критики языка считают, что подобная идеология хуже, чем обработка исключений, так как многочисленные проверки засоряют код и не позволяют сосредоточить всю обработку ошибок в блоках
catch
. Создатели языка не считают это серьёзной проблемой. Описан ряд паттернов обработки ошибок в 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 была унаследована из языка Active Oberon на основе CSP Тони Хоара с использование идей из языков Occam и Limbo , но также присутствуют такие особенности как Пи-исчисление и канальная передача.
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 осуществлена с помощью проекта .
Возможность интроспекции во время выполнения, то есть доступ и обработка значений любых типов и динамическая настройка на типы обрабатываемых данных реализуются в 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
станет некорректным. Произойти это может в любой момент, как сразу по завершении операции, так и через много часов работы программы, так что ошибка выразится в случайных сбоях программы, причину которых крайне сложно будет выявить. А при использовании перемещающего сборщика мусора
преобразованный в число указатель может стать неактуальным даже тогда, когда объект ещё не удалён из памяти.
Поскольку спецификация Go не даёт точных указаний на то, в какой мере программист может рассчитывать на сохранение актуальности преобразованного в число указателя, существует рекомендация: сводить подобные преобразования к минимуму и организовывать их так, чтобы преобразование исходного указателя, его модификации и обратное преобразование находились в пределах одной языковой инструкции, а при вызове любых библиотечных функций, возвращающих адрес в виде
uintptr
, немедленно преобразовывать их результат в
unsafe.Pointer
для сохранения гарантии, что указатель не будет потерян.
Пакет
unsafe
редко используется в прикладном программировании непосредственно, но он активно применяется в пакетах
reflect
,
os
,
syscall
,
context
,
net
и некоторых других.
Существует несколько внешних инструментов, обеспечивающих
интерфейс внешних функций
(FFI) для Go-программ. Для взаимодействия с внешним кодом на
Си
(или имеющем совместимый с Си интерфейс) может применяться утилита
. Она вызывается автоматически при обработке компилятором соответствующим образом написанного Go-модуля, и обеспечивает создание временного пакета-враппера на Go, содержащего объявления всех необходимых типов и функций. В вызовах Си-функций часто приходится прибегать к средствам пакета
unsafe
, главным образом — использовать тип
unsafe.Pointer
. Более мощным инструментом является
SWIG
, обеспечивающий более сложные возможности, в частности, интеграцию с классами
C++
.
Стандартная библиотека Go поддерживает создание
консольных приложений
и серверных приложений с
веб-интерфейсом
, но нет стандартных средств для создания
GUI
в клиентских приложениях. Этот пробел компенсируется созданными сторонними разработчиками врапперами к популярным UI-
фреймворкам
, таким как
GTK+
и
Qt
, под Windows можно использовать графические средства
WinAPI
, обращаясь к ним посредством пакета
syscall
, но все эти способы довольно громоздки. Имеется также несколько разработок UI-фреймворков на самом Go, но ни один из этих проектов не достиг уровня промышленной применимости. В 2015 году на конференции GopherCon в
Денвере
один из создателей языка, Роберт Грисмер, отвечая на вопросы, согласился, что Go нуждается в пакете UI, но заметил, что такой пакет должен быть универсальным, мощным и мультиплатформенным, что делает его разработку длительным и непростым процессом. Вопрос о реализации клиентского GUI на Go до сих пор остаётся открытым.
В силу молодости языка его критика сосредоточена, главным образом, в Интернет-статьях, обзорах и на форумах.
Значительная часть критики языка фокусируется на отсутствии в нём тех или иных популярных средств, предоставляемых другими языками. В их числе :
Как уже говорилось выше,
отсутствие целого ряда средств, доступных в других популярных языках, объясняется сознательным выбором разработчиков, считающих, что такие средства либо затрудняют эффективную компиляцию, либо провоцируют программиста на ошибки или на создание неэффективного или «плохого» с точки зрения сопровождения кода, либо имеют другие нежелательные побочные эффекты.
type EnumArray = array[EnumType] of ElementType
), создать цикл по перечислению, компилятор не может контролировать полноту списка альтернатив в конструкции
switch
, когда в качестве селектора используется значение перечисления.
interface{}
, к тому же их невозможно обойти с помощью конструкции
for range
.
try-catch
. Более того, вопреки собственным рекомендациям авторы языка применяют генерацию и обработку паники для обработки логических ошибок внутри стандартной библиотеки.
Критики отмечают, что некоторые особенности Go выполнены с точки зрения наиболее простой или наиболее эффективной реализации, но не отвечают « принципу наименьшего удивления »: их поведение отличается от того, что программист ожидает, основываясь на интуиции и прошлом опыте. Такие особенности требуют повышенного внимания программиста, затрудняют обучение и переход с других языков.
for index, value := range collection
» переменная
value
является копией текущего элемента. Операция присваивания этой переменной нового значения доступна, но, вопреки ожиданиям, не приводит к изменению текущего элемента коллекции.
nil
. У интерфейса, указывающего на нулевой объект, первая ссылка заполнена; он не равен нулевому интерфейсу, хотя с точки зрения логики программы между ними обычно нет разницы
. Это приводит к неожиданным эффектам и усложняет проверку корректности значений интерфейсных типов:
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()
.
Часто критике подвергается механизм автоматической расстановки точек с запятой, из-за которого некоторые формы записи операторов, вызовов функций и списков становятся некорректными. Комментируя это решение, авторы языка замечают,
что в совокупности с наличием в официальном наборе инструментов средства форматирования кода
gofmt
оно привело к фиксации довольно жёсткого стандарта оформления кода на Go. Вряд ли возможно создать стандарт записи кода, который бы устроил всех; внедрение в язык особенности, которая сама по себе задаёт такой стандарт, унифицирует внешний вид программ и устраняет непринципиальные конфликты из-за форматирования, что является положительным фактором для групповой разработки и сопровождения ПО.
Популярность Go в последние годы росла: с 2014 по 2020 год в рейтинге TIOBE он поднялся с 65-го места на 11-е, значение рейтинга на август 2020 года составляет 1,43 %. По результатам опроса сайта dou.ua язык Go в 2018 году стал девятым в списке самых используемых и шестым в списке языков, которым отдают личное предпочтение разработчики.
С 2012 года, когда вышел первый публичный релиз, использование языка неуклонно растёт. В опубликованном на сайте проекта Go списке компаний, использующих язык в промышленных разработках, насчитывается несколько десятков наименований. Накоплен большой массив библиотек различного назначения. На 2019 год был запланирован выпуск версии 2.0, но работы затянулись и на вторую половину 2022 года ещё продолжаются. Ожидается
появление ряда новых возможностей, в том числе средств обобщённого программирования и специального синтаксиса для упрощения обработки ошибок, отсутствие которых является одними из наиболее распространённых претензий критиков языка .На Golang разработан веб-сервер , который позволяет веб-приложениям достигать скорости request-response 10-20 мс вместо традиционных 200 мс. Данный веб-сервис планируется включить в состав популярных фреймворков, таких как Yii .
Наряду с C++ Golang применяется для разработки микросервисов, что позволяет «загрузить» работой много-процессорные платформы. Взаимодействовать с микросервисом можно с помощью REST , а язык PHP для этого отлично подходит.
С помощью PHP и Golang разработан Spiral Framework.
Существует только одна основная версия самого языка Go — версия 1. Версии среды разработки (компилятора, инструментария и стандартных библиотек) Go нумеруются по двухзначной («<версия языка>.<основной релиз>») либо трёхзначной («<версия языка>.<основной релиз>.<дополнительный релиз>») системе. Выпуск новой «двузначной» версии автоматически означает прекращение поддержки предыдущей «двузначной» версии. «Трёхзначные» версии выпускаются для исправления обнаруженных ошибок и проблем с безопасностью; исправления безопасности в таких версиях могут затрагивать две последние «двузначные» версии .
Авторы декларировали стремление к сохранению, насколько это возможно, обратной совместимости в пределах основной версии языка. Это означает, что до выхода релиза Go 2 почти любая программа, созданная в среде Go 1, будет корректно компилироваться в любой последующей версии Go 1.x и выполняться без ошибок. Исключения возможны, но они немногочисленны. Однако бинарной совместимости между релизами не гарантируется, так что программа при переходе на более поздний релиз Go должна быть полностью перекомпилирована.
С марта 2012 года, когда была представлена версия Go 1, вышли следующие основные версии:
embed
, реализующий возможность доступа к файлам, встроенным в состав исполняемого модуля. На июнь 2021 выпущено пять минорных релизов.
Несмотря на наличие обсуждения, создатели языка приняли решение отказаться от увеличения цифры старшей версии языка. Взамен, разработчики собирают и планируют реализовать замечания и предложения из списка нововведений в версии go 1.X до тех пор, пока это возможно. При этом, отказ от увеличения старшей версии не является окончательным, а разработчики языка не гарантируют, что go 2.0 никогда не выйдет, но это обновление не будет напрямую связано с нынешним документом.
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()
нельзя; на верхнем уровне все ошибки должны быть обработаны явно.
// 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()
».
На данный момент существуют два основных компилятора Go:
Также существуют проекты:
Среда разработки Go содержит несколько инструментов командной строки: утилиту go, обеспечивающий компиляцию, тестирование и управление пакетами, и вспомогательные утилиты godoc и gofmt, предназначенные, соответственно, для документирования программ и для форматирования исходного кода по стандартным правилам. Для вывода полного списка инструментов необходимо вызвать утилиту go без указания аргументов. Для отладки программ может использоваться отладчик gdb. Независимыми разработчиками представлено большое количество инструментов и библиотек, предназначенных для поддержки процесса разработки, главным образом, для облегчения анализа кода, тестирования и отладки.
На текущий момент доступны две IDE, изначально ориентированные на язык Go — это проприетарная GoLand (разрабатывается в JetBrains на платформе IntelliJ) и свободная LiteIDE (ранее проект назывался 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)
}
m[-1]
означает последний элемент массива,
m[-2]
— второй с конца и так далее