Спецификация языка программирования Go

Спецификация языка программирования Go на русском языке

“The Go Programming Language Specification”

Introduction (Введение)

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

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

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

Notation (Нотация)

Синтаксис задается с помощью варианта расширенной формы Бэкуса-Наура (EBNF):

Production  = production_name "=" [ Expression ] "." .
Expression  = Alternative { "|" Alternative } .
Alternative = Term { Term } .
Term        = production_name | token [ "…" token ] | Group | Option | Repetition .
Group       = "(" Expression ")" .
Option      = "[" Expression "]" .
Repetition  = "{" Expression "}" .

“Productions” (продукции) — это правила в формальной грамматике, которые описывают синтаксис языка Go.

Productions - это выражения, построенные из терминов и следующих операторов в порядке возрастания старшинства:

|   alternation
()  grouping
[]  option (0 or 1 times)
{}  repetition (0 to n times)

Для обозначения лексических (терминальных) лексем используются строчные имена, написанные строчными буквами. Нетерминалы обозначаются в CamelCase. Лексические лексемы заключаются в двойные кавычки "" или обратные кавычки ``.

Форма a ... b представляет собой набор символов от a до b в качестве альтернатив. Горизонтальное многоточие ... также используется в других местах спецификации для неформального обозначения различных перечислений или фрагментов кода, которые не уточняются далее. Символ ... (в отличие от трех символов …) не является лексемой языка Go.

Ссылка вида [Go 1.xx] указывает на то, что описанная функция языка (или какой-то ее аспект) была изменена или добавлена в версии языка 1.xx и поэтому для сборки требуется как минимум эта версия языка. Подробнее см. раздел со ссылками в приложении.

Source code representation (Представление исходного кода)

Исходный код - это текст Юникода, закодированный в UTF-8. Текст не канонизирован, поэтому одна точка кода с ударением отличается от того же символа, построенного из сочетания ударения и буквы; они рассматриваются как две точки кода. Для простоты в этом документе будет использоваться неквалифицированный термин “символ” для обозначения кодовой точки Юникода в исходном тексте.

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

Ограничение на реализацию: Для совместимости с другими инструментами компилятор может запретить использование символа NUL (U+0000) в исходном тексте.

Ограничение на реализацию: Для совместимости с другими инструментами компилятор может игнорировать метку порядка байтов в кодировке UTF-8 (U+FEFF), если она является первой точкой кода Юникода в исходном тексте. В любом другом месте исходного текста знак порядка байтов может быть запрещен.

Символы

Следующие термины используются для обозначения определенных категорий символов Unicode:

newline        = /* кодовая позиция Юникода U+000A */ .
unicode_char   = /* произвольная кодовая позиция  Юникода, кроме newline */ .
unicode_letter = /* кодовая позиция Юникода, отнесенная к категории "Буква" */ .
unicode_digit  = /* кодовая позиция Юникода, отнесенная к категории "Число, десятичная цифра" */ .```

В стандарте Юникод 8.0 в разделе 4.5 “Общая категория” определен набор категорий символов. Go рассматривает все символы в любой из буквенных категорий Lu, Ll, Lt, Lm или Lo как буквы Юникода, а символы в числовой категории Nd - как цифры Юникода.

Символы и числа

Символ подчеркивания _ (U+005F) считается строчной буквой.

letter        = unicode_letter | "_" .
decimal_digit = "0"  "9" .
binary_digit  = "0" | "1" .
octal_digit   = "0"  "7" .
hex_digit     = "0"  "9" | "A"  "F" | "a"  "f" .

Лексические элементы

Комментарии

Комментарии служат в качестве программной документации. Существует две формы:

  1. Строчные комментарии начинаются с последовательности символов // и останавливаются в конце строки.
  2. Общие комментарии начинаются с последовательности символов /* и заканчиваются первой последующей последовательностью символов */.

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

Токены

Токены составляют словарный запас языка Go. Существует четыре класса:

  • идентификаторы (Identifiers),
  • ключевые слова, (Keywords)
  • операторы и пунктуация,
  • литералы.

Пространство пробелов, образованное символами (U+0020), горизонтальных табуляций (U+0009), возвратов каретки (U+000D) и новых строк (U+000A), игнорируется, за исключением случаев, когда оно разделяет лексемы, которые в противном случае были бы объединены в одну лексему. Кроме того, новая строка или конец файла могут вызвать вставку точки с запятой.

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

Точки с запятой

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

  1. Когда ввод разбивается на токены, точка с запятой автоматически вставляется в поток токенов сразу после последнего токена строки, если этот токен
  • идентификатор
  • целое число, плавающая точка, мнимое число, руна или строковый литерал
  • одно из ключевых слов break, continue, fallthrough или return
  • один из операторов и пунктуации ++, –, ), ], или }.
  1. Чтобы сложные операторы занимали одну строку, перед закрывающим оператором “)” или “}” можно опустить точку с запятой.

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

(Идентификаторы) Identifiers

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

identifier = letter { letter | unicode_digit } .

a
_x9
ThisVariableIsExported
αβ

Ключевые слова (Keywords)

Следующие ключевые слова зарезервированы и не могут быть использованы в качестве идентификаторов.

break        default      func         interface    select
case         defer        go           map          struct
chan         else         goto         package      switch
const        fallthrough  if           range        type
continue     for          import       return       var

Операторы и пунктуация

Следующие последовательности символов представляют операторы (включая операторы присваивания) и пунктуацию:

+    &     +=    &=     &&    ==    !=    (    )
-    |     -=    |=     ||    <     <=    [    ]
*    ^     *=    ^=     <-    >     >=    {    }
/    <<    /=    <<=    ++    =     :=    ,    ;
%    >>    %=    >>=    --    !     ...   .    :
     &^          &^=

Целочисленные литералы

Целочисленный литерал - это последовательность цифр, представляющая целочисленную константу. Необязательный префикс задает недесятичное основание: 0b или 0B для двоичной системы, 0, 0o или 0O для восьмеричной и 0x или 0X для шестнадцатеричной [Go 1.13]. Одиночный 0 считается десятичным нулем. В шестнадцатеричных литералах буквы от a до f и от A до F обозначают значения от 10 до 15.

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

int_lit        = decimal_lit | binary_lit | octal_lit | hex_lit .
decimal_lit    = "0" | ( "1"  "9" ) [ [ "_" ] decimal_digits ] .
binary_lit     = "0" ( "b" | "B" ) [ "_" ] binary_digits .
octal_lit      = "0" [ "o" | "O" ] [ "_" ] octal_digits .
hex_lit        = "0" ( "x" | "X" ) [ "_" ] hex_digits .

decimal_digits = decimal_digit { [ "_" ] decimal_digit } .
binary_digits  = binary_digit { [ "_" ] binary_digit } .
octal_digits   = octal_digit { [ "_" ] octal_digit } .
hex_digits     = hex_digit { [ "_" ] hex_digit } .
42
4_2
0600
0_600
0o600
0O600       // second character is capital letter 'O'
0xBadFace
0xBad_Face
0x_67_7a_2f_cc_40_c6
170141183460469231731687303715884105727
170_141183_460469_231731_687303_715884_105727

_42         // an identifier, not an integer literal
42_         // invalid: _ must separate successive digits
4__2        // invalid: only one _ at a time
0_xBadFace  // invalid: _ must separate successive digits

Литералы с плавающей точкой

Литерал с плавающей точкой - это десятичное или шестнадцатеричное представление константы с плавающей точкой.

Десятичный литерал с плавающей точкой состоит из целой части (десятичных цифр), десятичной точки, дробной части (десятичных цифр) и экспоненты (e или E, за которой следует необязательный знак и десятичные цифры). Одна из частей целого числа или дробная часть могут быть опущены; одна из десятичных точек или часть экспоненты могут быть опущены. Значение экспоненты exp увеличивает мантиссу (целую и дробную части) на (10^{exp}).

Шестнадцатеричный литерал с плавающей точкой состоит из префикса 0x или 0X, целой части (шестнадцатеричные цифры), точки радикса, дробной части (шестнадцатеричные цифры) и части экспоненты (p или P, за которой следует необязательный знак и десятичные цифры). Одна из частей целого числа или дробная часть могут быть опущены; точка радикса также может быть опущена, но часть экспоненты обязательна. (Этот синтаксис соответствует синтаксису, приведенному в IEEE 754-2008 §5.12.3.) Значение экспоненты exp увеличивает мантиссу (целую и дробную части) на (2^{exp}) [Go 1.13].

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

float_lit         = decimal_float_lit | hex_float_lit .

decimal_float_lit = decimal_digits "." [ decimal_digits ] [ decimal_exponent ] |
                    decimal_digits decimal_exponent |
                    "." decimal_digits [ decimal_exponent ] .
decimal_exponent  = ( "e" | "E" ) [ "+" | "-" ] decimal_digits .

hex_float_lit     = "0" ( "x" | "X" ) hex_mantissa hex_exponent .
hex_mantissa      = [ "_" ] hex_digits "." [ hex_digits ] |
                    [ "_" ] hex_digits |
                    "." hex_digits .
hex_exponent      = ( "p" | "P" ) [ "+" | "-" ] decimal_digits .
0.
72.40
072.40       // == 72.40
2.71828
1.e+0
6.67428e-11
1E6
.25
.12345E+5
1_5.         // == 15.0
0.15e+0_2    // == 15.0

0x1p-2       // == 0.25
0x2.p10      // == 2048.0
0x1.Fp+0     // == 1.9375
0X.8p-0      // == 0.5
0X_1FFFP-16  // == 0.1249847412109375
0x15e-2      // == 0x15e - 2 (integer subtraction)

0x.p1        // invalid: mantissa has no digits
1p-2         // invalid: p exponent requires hexadecimal mantissa
0x1.5e-2     // invalid: hexadecimal mantissa requires p exponent
1_.5         // invalid: _ must separate successive digits
1._5         // invalid: _ must separate successive digits
1.5_e1       // invalid: _ must separate successive digits
1.5e_1       // invalid: _ must separate successive digits
1.5e1_       // invalid: _ must separate successive digits

Мнимые литералы (imaging)

Мнимый литерал представляет собой мнимую часть комплексной константы. Он состоит из целого литерала или литерала с плавающей точкой, за которым следует строчная буква i. Значение мнимого литерала - это значение соответствующего целого литерала или литерала с плавающей точкой, умноженное на мнимую единицу i [Go 1.13].

imaginary_lit = (decimal_digits | int_lit | float_lit) "i" .

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

0i
0123i         // == 123i for backward-compatibility
0o123i        // == 0o123 * 1i == 83i
0xabci        // == 0xabc * 1i == 2748i
0.i
2.71828i
1.e+0i
6.67428e-11i
1E6i
.25i
.12345E+5i
0x1p-2i       // == 0x1p-2 * 1i == 0.25i

Литералы рун

Литерал руны представляет собой константу руны, целочисленное значение, идентифицирующее код Юникода. Рунический литерал выражается в виде одного или нескольких символов, заключенных в одинарные кавычки, как, например, 'x' или '\n'. Внутри кавычек может находиться любой символ, кроме новой строки и неэкранированной одинарной кавычки. Одиночный символ в кавычках представляет собой значение Unicode самого символа, а многосимвольные последовательности, начинающиеся с обратной косой черты, кодируют значения в различных форматах.

Простейшая форма представляет собой одиночный символ внутри кавычек; поскольку исходный текст Go - это символы Юникода, закодированные в UTF-8, несколько байтов в кодировке UTF-8 могут представлять одно целое значение.

Например, литерал 'a' содержит один байт, представляющий литерал a, Unicode U+0061, значение 0x61, а литерал 'ä' содержит два байта (0xc3 0xa4), представляющих литерал a-dieresis, U+00E4, значение 0xe4.

Несколько обратных косых черточек позволяют кодировать произвольные значения как ASCII-текст. Существует четыре способа представления целочисленного значения в виде числовой константы:

  • \x с последующими ровно двумя шестнадцатеричными цифрами;
  • \u с последующими ровно четырьмя шестнадцатеричными цифрами;
  • \U с последующими ровно восемью шестнадцатеричными цифрами,
  • и обычный обратный слеш \ с последующими ровно тремя восьмеричными цифрами.

В каждом случае значение литерала - это значение, представленное цифрами в соответствующем базисе.

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

Октальные эскейпы должны представлять значение от 0 до 255 включительно. Шестнадцатеричные эскейпы удовлетворяют этому условию по своей конструкции. Эскапады \u и \U представляют кодовые точки Юникода, поэтому в них некоторые значения являются недопустимыми, в частности, значения выше 0x10FFFF и суррогатные половинки.

После обратной косой черты некоторые односимвольные эскейпы представляют специальные значения:

\a   U+0007 alert or bell
\b   U+0008 backspace
\f   U+000C form feed
\n   U+000A line feed or newline
\r   U+000D carriage return
\t   U+0009 horizontal tab
\v   U+000B vertical tab
\\   U+005C backslash
\'   U+0027 single quote  (valid escape only within rune literals)
\"   U+0022 double quote  (valid escape only within string literals)

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

rune_lit         = "'" ( unicode_value | byte_value ) "'" .
unicode_value    = unicode_char | little_u_value | big_u_value | escaped_char .
byte_value       = octal_byte_value | hex_byte_value .
octal_byte_value = `\` octal_digit octal_digit octal_digit .
hex_byte_value   = `\` "x" hex_digit hex_digit .
little_u_value   = `\` "u" hex_digit hex_digit hex_digit hex_digit .
big_u_value      = `\` "U" hex_digit hex_digit hex_digit hex_digit
                           hex_digit hex_digit hex_digit hex_digit .
escaped_char     = `\` ( "a" | "b" | "f" | "n" | "r" | "t" | "v" | `\` | "'" | `"` ) .
'a'
'ä'
'本'
'\t'
'\000'
'\007'
'\377'
'\x07'
'\xff'
'\u12e4'
'\U00101234'
'\''         // rune literal containing single quote character
'aa'         // illegal: too many characters
'\k'         // illegal: k is not recognized after a backslash
'\xa'        // illegal: too few hexadecimal digits
'\0'         // illegal: too few octal digits
'\400'       // illegal: octal value over 255
'\uDFFF'     // illegal: surrogate half
'\U00110000' // illegal: invalid Unicode code point

Строковые литералы

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

Необработанные строковые литералы - это последовательности символов между обратными кавычками, как в `foo`.

Внутри кавычек может находиться любой символ, кроме обратной кавычки. Значением сырого строкового литерала является строка, состоящая из неинтерпретированных (неявно UTF-8-кодированных) символов между кавычками; в частности, обратные косые черты не имеют специального значения, и строка может содержать новые строки. Символы возврата каретки ('\r') внутри необработанных строковых литералов отбрасываются из значения необработанной строки.

Интерпретируемые строковые литералы - это последовательности символов между двойными кавычками, как в слове “bar”. Внутри кавычек может появиться любой символ, кроме новой строки и двойной кавычки без сопровождения. Текст между кавычками образует значение литерала, при этом обратные косые черты интерпретируются так же, как и в рунных литералах (за исключением того, что \' является незаконным, а \" - законным), с теми же ограничениями. Трехзначные восьмеричные (\nnn) и двузначные шестнадцатеричные (\xnn) эскапады представляют отдельные байты результирующей строки; все остальные эскапады представляют (возможно, многобайтовую) кодировку UTF-8 отдельных символов. Таким образом, внутри строки литералы \377 и \xFF представляют один байт значения 0xFF=255, а ÿ, \u00FF, \U000000FF и \xc3\xbf представляют два байта 0xc3 0xbf кодировки UTF-8 символа U+00FF.

string_lit             = raw_string_lit | interpreted_string_lit .
raw_string_lit         = "`" { unicode_char | newline } "`" .
interpreted_string_lit = `"` { unicode_value | byte_value } `"` .
`abc`                // same as "abc"
`\n
\n`                  // same as "\\n\n\\n"
"\n"
"\""                 // same as `"`
"Hello, world!\n"
"日本語"
"\u65e5本\U00008a9e"
"\xff\u00FF"
"\uD800"             // illegal: surrogate half
"\U00110000"         // illegal: invalid Unicode code point

Все эти примеры представляют одну и ту же строку:

"日本語"                                 // UTF-8 input text
`日本語`                                 // UTF-8 input text as a raw literal
"\u65e5\u672c\u8a9e"                    // the explicit Unicode code points
"\U000065e5\U0000672c\U00008a9e"        // the explicit Unicode code points
"\xe6\x97\xa5\xe6\x9c\xac\xe8\xaa\x9e"  // the explicit UTF-8 bytes

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

Константы

Существуют булевы константы, рунные константы, целочисленные константы, константы с плавающей точкой, комплексные константы и строковые константы.

Руны, целые числа, константы с плавающей точкой и комплексные константы в совокупности называются числовыми константами.

Постоянное значение представлено руной, целым числом, плавающей точкой, мнимым или строковым литералом, идентификатором, обозначающим константу, постоянным выражением, преобразованием с результатом, являющимся константой, или значением результата некоторых встроенных функций, таких как min или max, применяемых к аргументам констант, unsafe.Sizeof, применяемых к определенным значениям, cap или len, применяемых к некоторым выражениям, real и imag, применяемых к комплексной константе, и complex, применяемых к числовым константам. Булевы истинностные значения представлены заранее объявленными константами true и false. Объявленный идентификатор iota обозначает целочисленную константу.

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

Числовые константы представляют собой точные значения произвольной точности и не переполняются. Следовательно, не существует констант, обозначающих отрицательный ноль, бесконечность и не-число по стандарту IEEE 754.

Константы могут быть типизированными или нетипизированными. Буквальные константы, true, false, iota, а также некоторые константные выражения, содержащие только нетипизированные операнды констант, являются нетипизированными.

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

Ограничение на реализацию:

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

  • Представлять целочисленные константы не менее чем 256 битами.
  • Представлять константы с плавающей точкой, включая части комплексной константы, с мантиссой не менее 256 бит и знаковой двоичной экспонентой не менее 16 бит.
  • Указывать ошибку, если не может точно представить целую константу.
  • Давать ошибку, если не может представить константу с плавающей точкой или комплексную константу из-за переполнения.
  • Округлять до ближайшей представимой константы, если не может представить константу с плавающей точкой или комплексную константу из-за ограничений на точность.

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

Переменные

Переменная - это место хранения значения. Набор допустимых значений определяется типом переменной.

Объявление переменной или, для параметров и результатов функций, подпись объявления функции или литерала функции резервирует место для хранения именованной переменной. Вызов встроенной функции new или получение адреса составного литерала выделяет место для хранения переменной во время выполнения. На такую анонимную переменную ссылаются через (возможно, неявный) указатель.

Структурированные переменные типов array, slice и struct имеют элементы и поля, к которым можно обращаться по отдельности. Каждый такой элемент действует как переменная.

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

var x interface{}  // x is nil and has static type interface{}
var v *T           // v has value nil, static type *T
x = 42             // x has value 42 and dynamic type int
x = v              // x has value (*T)(nil) and dynamic type *T

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

Типы

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

Type     = TypeName [ TypeArgs ] | TypeLit | "(" Type ")" .
TypeName = identifier | QualifiedIdent .
TypeArgs = "[" TypeList [ "," ] "]" .
TypeList = Type { "," Type } .
TypeLit  = ArrayType | StructType | PointerType | FunctionType | InterfaceType |
           SliceType | MapType | ChannelType .

Язык заранее объявляет некоторые имена типов. Другие вводятся с помощью деклараций типов или списков параметров типов. Составные типы - массив, структура, указатель, функция, интерфейс, фрагмент, карта и канальный тип - могут быть построены с помощью литералов типов.

Объявленные типы, определенные типы и параметры типов называются именованными типами. Псевдоним обозначает именованный тип, если тип, указанный в объявлении псевдонима, является именованным типом.

Boolean types (Булевы типы)

Булевый тип представляет собой набор булевых истинностных значений, обозначаемых заранее объявленными константами true и false. Предварительно объявленный булевский тип - bool; это определенный тип.

Numeric types (Числовые типы)

Целый, плавающий или комплексный тип представляет собой набор целых, плавающих или комплексных значений, соответственно. Все они называются числовыми типами. К заранее объявленным независимым от архитектуры числовым типам относятся:

uint8       the set of all unsigned  8-bit integers (0 to 255)
uint16      the set of all unsigned 16-bit integers (0 to 65535)
uint32      the set of all unsigned 32-bit integers (0 to 4294967295)
uint64      the set of all unsigned 64-bit integers (0 to 18446744073709551615)

int8        the set of all signed  8-bit integers (-128 to 127)
int16       the set of all signed 16-bit integers (-32768 to 32767)
int32       the set of all signed 32-bit integers (-2147483648 to 2147483647)
int64       the set of all signed 64-bit integers (-9223372036854775808 to 9223372036854775807)

float32     the set of all IEEE 754 32-bit floating-point numbers
float64     the set of all IEEE 754 64-bit floating-point numbers

complex64   the set of all complex numbers with float32 real and imaginary parts
complex128  the set of all complex numbers with float64 real and imaginary parts

byte        alias for uint8
rune        alias for int32

Значение n-битного целого числа имеет ширину n бит и представляется с помощью арифметики дополнения двойки.

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

uint //32 или 64 бита
int //тот же размер, что и uint
uintptr //целое число без знака, достаточно большое для хранения неинтерпретированных битов значения указателя

Чтобы избежать проблем с переносимостью, все числовые типы являются определенными типами и, следовательно, отдельными, за исключением byte, который является псевдонимом для uint8, и rune, который является псевдонимом для int32. Явные преобразования требуются, когда различные числовые типы смешиваются в выражении или присваивании.

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

String types (Строковые типы)

Строковый тип представляет собой набор строковых значений.

Строковое значение - это последовательность байтов (возможно, пустая). Количество байтов называется длиной строки и никогда не бывает отрицательным. Строки неизменяемы: после создания содержимое строки невозможно изменить. Объявленный тип строки - string; это определенный тип.

Длина строки s может быть определена с помощью встроенной функции len. Длина является константой времени компиляции, если строка является константой. Доступ к байтам строки осуществляется по целочисленным индексам от 0 до len(s)-1. Взятие адреса такого элемента недопустимо; если s[i] - это i-й байт строки, то &s[i] недопустимо.

Array types (Типы массивов)

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

ArrayType   = "[" ArrayLength "]" ElementType .
ArrayLength = Expression .
ElementType = Type .

Длина является частью типа массива; она должна быть оценена неотрицательной константой, представляемой значением типа int. Длину массива a можно узнать с помощью встроенной функции len. Элементы можно адресовать по целочисленным индексам от 0 до len(a)-1. Типы массивов всегда одномерны, но могут образовывать многомерные типы.

[32]byte
[2*N] struct { x, y int32 }
[1000]*float64
[3][5]int
[2][2][2]float64  // same as [2]([2]([2]float64))

Массив типа T не может иметь элемент типа T или типа, содержащего T в качестве компонента, прямо или косвенно, если эти содержащие типы являются только типами array или struct.

// недопустимые типы массивов
type (
T1 [10]T1 // тип элемента T1 - T1
T2 [10]struct{ f T2 }     // T2 содержит T2 как компонент struct
T3 [10]T4 // T3 содержит T3 как компонент struct в T4
T4 struct{ f T3 }         // T4 содержит T4 как компонент массива T3 в struct
)

// допустимые типы массивов
type (
T5 [10]*T5 // T5 содержит T5 как компонент указателя
T6 [10]func() T6 // T6 содержит T6 как компонент функции типа
T7 [10]struct{ f []T7 }   // T7 содержит T7 как компонент фрагмента в структуре
)

Slice types (Типы срезов)

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

SliceType = "[" "]" ElementType .

Длину фрагмента s можно определить с помощью встроенной функции len; в отличие от массивов, она может меняться в процессе выполнения. К элементам можно обращаться по целочисленным индексам от 0 до len(s)-1. Индекс фрагмента данного элемента может быть меньше, чем индекс того же элемента в нижележащем массиве.

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

Массив, лежащий в основе фрагмента, может простираться за пределы фрагмента. Емкость - это мера этой протяженности: она равна сумме длины среза и длины массива за пределами среза; срез длиной до этой емкости может быть создан путем нарезки нового среза из исходного среза. Вместимость среза a можно узнать с помощью встроенной функции cap(a).

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

make([]T, length, capacity)

дает тот же срез, что и выделение массива и его срез, поэтому эти два выражения эквивалентны:

make([]int, 50, 100)
new([100]int)[0:50]

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

Struct types (Типы структур)

Структура - это последовательность именованных элементов, называемых полями, каждый из которых имеет имя и тип. Имена полей могут быть заданы явно (IdentifierList) или неявно (EmbeddedField). Внутри структуры непустые имена полей должны быть уникальными.

StructType    = "struct" "{" { FieldDecl ";" } "}" .
FieldDecl     = (IdentifierList Type | EmbeddedField) [ Tag ] .
EmbeddedField = [ "*" ] TypeName .
Tag           = string_lit .
// Пустая структура.
struct {}

// Структура с 6 полями.
struct {
x, y int
u float32
_ float32 // подложка (пустышка)
A *[]int
F func()
}

Поле, объявленное с типом, но без явного имени поля, называется встроенным полем. Встроенное поле должно быть указано как имя типа T или как указатель на имя неинтерфейсного типа *T, причем сам T не может быть типом-указателем или параметром типа. Неквалифицированное имя типа выступает в качестве имени поля.

// Структура с четырьмя встроенными полями типов T1, *T2, P.T3 и *P.T4
struct {
	T1 // имя поля - T1
	*T2 // имя поля - T2
	P.T3 // имя поля - T3
	*P.T4 // имя поля - T4
	x, y int // имена полей - x и y
}

Следующее объявление является незаконным, поскольку имена полей должны быть уникальными в типе struct:

struct {
	T // конфликтует со встроенным полем *T и *P.T
	*T // конфликтует со встроенным полем T и *P.T
	*P.T // конфликтует со встроенным полем T и *T
}

Поле или метод f встроенного поля в структуре x называется продвинутым, если x.f является легальным селектором, обозначающим это поле или метод f.

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

Если задан тип struct S и имя типа T, то продвигаемые методы включаются в набор методов struct следующим образом:

  • Если S содержит встроенное поле T, то наборы методов S и *S оба включают продвигаемые методы с приемником T. Набор методов *S также включает продвигаемые методы с приемником *T.
  • Если S содержит встроенное поле *T, то наборы методов S и *S оба включают продвигаемые методы с приемником T или *T.

За объявлением поля может следовать необязательный строковый литерал tag, который становится атрибутом для всех полей в соответствующем объявлении поля.

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

struct {
	x, y float64 "" // пустая строка тега подобна отсутствующему тегу
	name string "в качестве тега допускается любая строка"
	_ [4]byte "Это не структурное поле"
}

// Структура, соответствующая буферу протокола TimeStamp.
// Строки тегов определяют номера полей буфера протокола;
// они следуют соглашению, изложенному в пакете reflect.
struct {
	microsec uint64 `protobuf: "1"`
	serverIP6 uint64 `protobuf: "2"`
}

Тип struct T не может содержать поле типа T или типа, содержащего T в качестве компонента, прямо или косвенно, если эти содержащие типы являются только типами array или struct.

// недопустимые типы структур
type (
	T1 struct{ T1 }            // T1 содержит поле T1
	T2 struct{ f [10]T2 }      // T2 содержит T2 как компонент массива
	T3 struct{ T4 }            // T3 содержит T3 как компонент массива в struct T4
	T4 struct{ f [10]T3 }      // T4 содержит T4 как компонент struct T3 в массиве
)

// допустимые типы struct
type (
	T5 struct{ f *T5 }         // T5 содержит T5 как компонент указателя
	T6 struct{ f func() T6 }   // T6 содержит T6 как компонент функции type
	T7 struct{ f [10][]T7 }    // T7 содержит T7 как компонент фрагмента массива
)

Pointer types (Типы указателей)

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

PointerType = "*" BaseType .
BaseType    = Type .

*Point
*[4]int

Function types (Типы функций)

Тип функции обозначает множество всех функций с одинаковыми типами параметров и результатов. Значением неинициализированной переменной типа функции является nil.

FunctionType   = "func" Signature .
Signature      = Parameters [ Result ] .
Result         = Parameters | Type .
Parameters     = "(" [ ParameterList [ "," ] ] ")" .
ParameterList  = ParameterDecl { "," ParameterDecl } .
ParameterDecl  = [ IdentifierList ] [ "..." ] Type .

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

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

func()
func(x int) int
func(a, _ int, z float32) bool
func(a, b int, z float32) (bool)
func(prefix string, values ...int)
func(a, b int, z float64, opt ...interface{}) (success bool)
func(int, int, float64) (float64, *[]int)
func(n int) func(p *T)

Interface types (Типы интерфейсов)

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

InterfaceType  = "interface" "{" { InterfaceElem ";" } "}" .
InterfaceElem  = MethodElem | TypeElem .
MethodElem     = MethodName Signature .
MethodName     = identifier .
TypeElem       = TypeTerm { "|" TypeTerm } .
TypeTerm       = Type | UnderlyingType .
UnderlyingType = "~" Type .

Тип интерфейса задается списком элементов интерфейса. Элемент интерфейса - это либо метод, либо элемент типа, где элемент типа - это объединение одного или нескольких терминов типа. Термин типа - это либо один тип, либо один базовый тип.

Основные интерфейсы

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

// Простой интерфейс File.
interface {
	Read([]byte) (int, error)
	Write([]byte) (int, error)
	Close() error
}

Имя каждого явно указанного метода должно быть уникальным и не пустым.

interface {
	String() string
	String() string  // ошибка: String не уникальна
	_(x int)         // ошибка: метод не должен иметь пустого имени
}

Интерфейс может быть реализован более чем одним типом. Например, если два типа S1 и S2 имеют набор методов

func (p T) Read(p []byte) (n int, err error)
func (p T) Write(p []byte) (n int, err error)
func (p T) Close() error

(где T обозначает S1 или S2), то интерфейс File реализуется как S1, так и S2, независимо от того, какие еще методы могут быть у S1 и S2.

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

interface{}

Для удобства предопределенный тип any является псевдонимом для пустого интерфейса. [Go 1.18].

Аналогично, рассмотрим эту спецификацию интерфейса, которая появляется внутри объявления типа для определения интерфейса под названием Locker:

type Locker interface {
	Lock()
	Unlock()
}

Если S1 и S2 также реализуют

func (p T) Lock() {  }
func (p T) Unlock() {  }

они реализуют интерфейс Locker, а также интерфейс File.

Встроенные интерфейсы

В несколько более общей форме интерфейс T может использовать (возможно, квалифицированное) имя типа интерфейса E в качестве элемента интерфейса. Это называется встраиванием интерфейса E в T [Go 1.14]. Набор типов T - это пересечение наборов типов, определяемых явно объявленными методами T, и наборов типов встроенных интерфейсов T. Другими словами, набор типов T - это множество всех типов, реализующих все явно объявленные методы T, а также все методы E [Go 1.18].

type Reader interface {
	Read(p []byte) (n int, err error)
	Close() error
}

type Writer interface {
	Write(p []byte) (n int, err error)
	Close() error
}

// Методы ReadWriter - это Read, Write, и Close.
type ReadWriter interface {
	Reader  // includes methods of Reader in ReadWriter's method set
	Writer  // includes methods of Writer in ReadWriter's method set
}

При встраивании интерфейсов методы с одинаковыми именами должны иметь идентичные сигнатуры.

type ReadCloser interface {
	Reader   // includes methods of Reader in ReadCloser's method set
	Close()  // ошибка: сигнатуры Reader.Close и Close отличаются друг от друга
}

Общие интерфейсы

В самом общем виде элемент интерфейса может быть также термином произвольного типа T, или термином вида ~T, задающим базовый тип T, или объединением терминов t1|t2|...|tn [Go 1.18]. Вместе со спецификациями методов эти элементы позволяют точно определить набор типов интерфейса следующим образом:

  • Набор типов пустого интерфейса - это набор всех типов, не входящих в интерфейс.
  • Набор типов непустого интерфейса - это пересечение наборов типов его интерфейсных элементов.
  • Набор типов спецификации метода - это набор всех неинтерфейсных типов, чьи наборы методов включают этот метод.
  • Набор типов термина типа, не являющегося интерфейсом, - это множество, состоящее только из этого типа.
  • Множество типов термина вида ~T - это множество всех типов, базовым типом которых является T.
  • Множество типов объединения терминов t1|t2|...|tn - это объединение множеств типов этих терминов.

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

По конструкции, набор типов интерфейса никогда не содержит интерфейсного типа.

// Интерфейс, представляющий только тип int.
interface {
int
}

// Интерфейс, представляющий все типы с базовым типом int.
interface {
~int
}

// Интерфейс, представляющий все типы с базовым типом int, реализующие метод String.
interface {
~int
String() string
}

// Интерфейс, представляющий пустой набор типов: не существует типа, который был бы одновременно и int, и string.
interface {
int
string
}

В термине вида ~T базовым типом T должен быть он сам, и T не может быть интерфейсом.

type MyInt int

interface {
	~[]byte // базовым типом []byte является он сам
	~MyInt // ошибка: базовым типом MyInt не является MyInt
	~error // ошибка: ошибка является интерфейсом
}

Элементы Union обозначают объединения наборов типов:

// Интерфейс Float представляет все типы с плавающей точкой
// (включая любые именованные типы, базовыми типами которых являются
// либо float32, либо float64).
type Float interface {
	~float32 | ~float64
}

Тип T в термине вида T или ~T не может быть параметром типа, а наборы типов всех неинтерфейсных терминов должны быть попарно расходящимися (попарное пересечение наборов типов должно быть пустым). Если задан параметр типа P:

interface {
	P // ошибка: P является параметром типа
	int | ~P // ошибка: P является параметром типа
	~int | MyInt // ошибка: наборы типов для ~int и MyInt не разделяются (~int включает MyInt)
	float32 | Float // пересекающиеся наборы типов, но Float - это интерфейс
}

Ограничение на реализацию: Объединение (с более чем одним членом) не может содержать заранее объявленный идентификатор comparable или интерфейсов, определяющих методы, или встраивать comparable или интерфейсы, определяющие методы.

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

var x Float                     // illegal: Float is not a basic interface

var x interface{} = Float(nil)  // illegal

type Floatish struct {
	f Float                 // illegal
}

Интерфейсный тип T не может встраивать элемент типа, который является, содержит или встраивает T, прямо или косвенно.

// illegal: Bad не может встраивать себя
type Bad interface {
	Bad
}

// illegal: Bad1 не может встраивать себя, используя Bad2
type Bad1 interface {
	Bad2
}
type Bad2 interface {
	Bad1
}

// illegal: Bad3 не может встраивать объединение, содержащее Bad3
type Bad3 interface {
	~int | ~string | Bad3
}

// illegal: Bad4 не может встраивать массив, содержащий Bad4 в качестве элемента type
type Bad4 interface {
	[10]Bad4
}

Реализация интерфейса

Тип T реализует интерфейс I, если

  • T не является интерфейсом и является элементом множества типов I; или
  • T является интерфейсом и множество типов T является подмножеством множества типов I.

Значение типа T реализует интерфейс, если T реализует интерфейс.

Map types (Типы карт)

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

MapType = "map" "[" KeyType "]" ElementType .
KeyType = Type .

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

map[string]int
map[*T]struct{ x, y float64 }
map[string]interface{}

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

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

make(map[string]int)
make(map[string]int, 100)

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

Channel types (Типы каналов)

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

ChannelType = ( "chan" | "chan" "<-" | "<-" "chan" ) ElementType .

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

chan T // может использоваться для отправки и получения значений типа T
chan <- float64 // может использоваться только для отправки float64
<-chan int // может использоваться только для получения ints

Оператор <- ассоциируется с самым левым возможным chan:

chan<- chan int // то же, что chan <- (chan int)
chan<- <-chan int // то же, что chan<- (<-chan int)
<-chan <-chan int // то же, что <-chan (<-chan int)
chan (<-chan int)

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

make(chan int, 100)

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

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

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

Каналы действуют как очереди “первым пришел - первым ушел”.

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

Свойства типов и значений

Представление значений

Значения предопределенных типов (см. ниже интерфейсы any и error), массивов и структур являются самодостаточными: Каждое такое значение содержит полную копию всех своих данных, а переменные таких типов хранят все значение.

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

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

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

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

Например, изменение элемента среза приводит к изменению этого элемента в базовом массиве для всех срезов, разделяющих этот массив.

Базовые типы

Каждый тип T имеет базовый тип: Если T - один из предопределенных булевых, числовых или строковых типов или литерал типа, то соответствующий базовый тип - это сам T. В противном случае базовый тип T - это базовый тип типа, на который T ссылается в своем объявлении. Для параметра типа это базовый тип его ограничения типа, которое всегда является интерфейсом.

type (
	A1 = string
	A2 = A1
)

type (
	B1 string
	B2 B1
	B3 []B1
	B4 B3
)

func f[P any](x P) {  }

Базовым типом строк A1, A2, B1 и B2 является string. Базовым типом []B1, B3 и B4 является []B1. Базовым типом P является interface{}.

Основные типы

Каждый неинтерфейсный тип T имеет основной тип, который совпадает с базовым типом T.

Интерфейс T имеет основной тип, если выполняется одно из следующих условий:

  1. Существует единственный тип U, который является базовым типом всех типов в наборе типов T; или
  2. набор типов T содержит только типы каналов с идентичным типом элемента E, и все направленные каналы имеют одинаковое направление.

Никакие другие интерфейсы не имеют базового типа.

Основным типом интерфейса, в зависимости от выполняемого условия, является либо:

  1. тип U; либо
  2. тип chan E, если T содержит только двунаправленные каналы, либо тип chan<- E или <-chan E в зависимости от направления присутствующих направленных каналов.

По определению, основной тип никогда не является определенным типом, параметром типа или типом интерфейса.

Примеры интерфейсов с основными типами:

type Celsius float32
type Kelvin  float32

interface{ int }                          // int
interface{ Celsius|Kelvin }               // float32
interface{ ~chan int }                    // chan int
interface{ ~chan int|~chan<- int }        // chan<- int
interface{ ~[]*data; String() string }    // []*data

Примеры интерфейсов без основных типов:

interface{}                               // нет ни одного базового типа
interface{ Celsius|float64 }              // нет единого базового типа
interface{ chan int | chan<- string }     // каналы имеют разные типы элементов
interface{ <-chan int | chan<- int }      // направленные каналы имеют разные направления

Некоторые операции (выражения slice, append и copy) опираются на несколько более свободную форму основных типов, которые принимают байтовые фрагменты и строки. В частности, если существует ровно два типа, []byte и string, которые являются базовыми типами всех типов в наборе типов интерфейса T, то основной тип T называется bytestring.

Примеры интерфейсов с базовыми типами bytestring:

interface{ int }                          // int (то же, что и обычный основной тип)
interface{ []byte | string }              // bytestring
interface{ ~[]byte | myString }           // bytestring

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

Идентичность типов

Два типа могут быть либо одинаковыми, либо разными.

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

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

Учитывая скаазанное:

type (
	A0 = []string
	A1 = A0
	A2 = struct{ a, b int }
	A3 = int
	A4 = func(A3, float64) *A0
	A5 = func(x int, _ float64) *[]string

	B0 A0
	B1 []string
	B2 struct{ a, b int }
	B3 struct{ a, c int }
	B4 func(int, float64) *B0
	B5 func(x int, y float64) *A1

	C0 = B0
	D0[P1, P2 any] struct{ x P1; y P2 }
	E0 = D0[int, string]
)

эти типы идентичны:

A0, A1, and []string
A2 and struct{ a, b int }
A3 and int
A4, func(int, float64) *[]string, and A5

B0 and C0
D0[int, string] and E0
[]int and []int
struct{ a, b *B5 } and struct{ a, b *B5 }
func(x int, y float64) *[]string, func(int, float64) (result *[]string), and A5

B0 и B1 отличаются, потому что это новые типы, созданные разными определениями типов; func(int, float64) *B0 и func(x int, y float64) *[]string отличаются, потому что B0 отличается от []string; а P1 и P2 отличаются, потому что это разные параметры типа. D0[int, string] и struct{ x int; y string } отличаются тем, что первый является инстанцированным определенным типом, а второй - литералом типа (но они все равно присваиваемые).

Назначаемость

Значение x типа V является назначаемым переменной типа T ("x является назначаемым T"), если выполняется одно из следующих условий:

  1. V и T идентичны.
  2. V и T имеют одинаковые базовые типы, но не являются параметрами типа, и хотя бы один из V или T не является именованным типом.
  3. V и T - канальные типы с идентичными типами элементов, V - двунаправленный канал, и хотя бы один из V или T не является именованным типом.
  4. T - интерфейсный тип, но не параметр типа, и x реализует T.
  5. x - это объявленный идентификатор nil, а T - указатель, функция, фрагмент, карта, канал или интерфейсный тип, но не параметр типа.
  6. x - это нетипизированная константа, представляемая значением типа T.

Кроме того, если для x тип V или T является параметром типа, x можно присвоить переменной типа T, если выполняется одно из следующих условий:

  • x - предопределенный идентификатор nil, T - параметр типа, и x может быть присвоен каждому типу в наборе типов T.
  • V не является именованным типом, T - параметр типа, и x может быть присвоен каждому типу в наборе типов T.
  • V - параметр типа, T - не именованный тип, и значения каждого типа в наборе типов V могут быть присвоены T.

Представимость

Константа x представима значением типа T, где T не является параметром типа, если выполняется одно из следующих условий:

  1. x находится в множестве значений, определяемых T.
  2. T - тип с плавающей точкой, и x может быть округлена до точности T без переполнения. При округлении используются правила округления IEEE 754, но при этом отрицательный ноль IEEE упрощается до беззнакового нуля. Обратите внимание, что постоянные значения никогда не приводят к отрицательному нулю IEEE, NaN или бесконечности.
  3. T - комплексный тип, и компоненты x - real(x) и imag(x) - могут быть представлены значениями типа компонента T (float32 или float64).

Если T - параметр типа, то x может быть представлен значением типа T, если x может быть представлен значением каждого типа в наборе типов T.

x T x может быть представлено значением T, потому что
‘a’ bite 97 находится в наборе значений байтов
97 rune rune - псевдоним для int32, и 97 находится в наборе 32-битных целых чисел
“foo” string “foo” находится в наборе значений строк
1024 int16 1024 находится в наборе 16-битных целых чисел
42.0 byte 42 находится в наборе беззнаковых 8-битных целых чисел
1e10 uint64 10000000000 находится в наборе беззнаковых 64-битных целых чисел
2.718281828459045 float32 2.718281828459045 округляется до 2.7182817, который находится в наборе значений float32
-1e-1000 float64 -1e-1000 округляется до IEEE -0.0, которое далее упрощается до 0.0
0i int 0 - целое значение
(42 + 0i) float32 42.0 (с нулевой мнимой частью) находится в наборе значений float32
x T x не может быть представлено значением T, потому что
0 bool 0 не входит в набор булевых значений
‘a’ string ‘a’ - это руна, она не входит в набор строковых значений
1024 byte 1024 не входит в набор беззнаковых 8-битных целых чисел
-1 uint16 -1 не входит в набор беззнаковых 16-битных целых чисел
1.1 int 1.1 не является целым числом
42i float32 (0 + 42i) не входит в набор значений float32
1e1000 float64 1e1000 после округления переполняется до IEEE +Inf

Наборы методов

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

  1. Набор методов определенного типа T состоит из всех методов, объявленных для типа-приемника T.
  2. Набор методов указателя на определенный тип T (где T не является ни указателем, ни интерфейсом) - это набор всех методов, объявленных с приемником *T или T.
  3. Набор методов интерфейсного типа - это пересечение наборов методов каждого типа в наборе типов интерфейса (результирующий набор методов обычно представляет собой просто набор объявленных методов в интерфейсе).

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

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

Blocks (Блоки)

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

Block = "{" StatementList "}" .
StatementList = { Statement ";" } .

Помимо явных блоков в исходном коде, существуют и неявные блоки:

  1. Вселенский блок universe block включает в себя весь исходный текст Go.
  2. Каждый пакет имеет package block блок пакета, содержащий весь исходный текст Go для этого пакета.
  3. Каждый файл имеет file block блок файла, содержащий весь исходный текст Go в этом файле.
  4. Каждый оператор “if”, “for” и “switch” считается находящимся в своем собственном неявном блоке.
  5. Каждый пункт в операторе “switch” или “select” действует как неявный блок.

Блоки вложены друг в друга и влияют на область видимости.

Декларации и области видимости

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

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

Declaration  = ConstDecl | TypeDecl | VarDecl .
TopLevelDecl = Declaration | FunctionDecl | MethodDecl .

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

В Go лексическая область видимости определяется с помощью блоков:

  1. Область видимости заранее объявленного идентификатора - это блок universe (вселенной).
  2. Областью видимости идентификатора, обозначающего константу, тип, переменную или функцию (но не метод), объявленную на верхнем уровне (вне любой функции), является блок package.
  3. Областью видимости имени импортируемого пакета является блок файла, содержащий объявление импорта.
  4. Областью видимости идентификатора, обозначающего приемник метода, параметр функции или переменную результата, является тело функции.
  5. Область видимости идентификатора, обозначающего параметр типа функции или объявленный приемником метода, начинается после имени функции и заканчивается в конце тела функции.
  6. Область видимости идентификатора, обозначающего параметр типа, начинается после имени типа и заканчивается в конце TypeSpec.
  7. Область видимости идентификатора константы или переменной, объявленной внутри функции, начинается в конце ConstSpec или VarSpec (ShortVarDecl для объявлений коротких переменных) и заканчивается в конце самого внутреннего содержащего блока.
  8. Область видимости идентификатора типа, объявленного внутри функции, начинается с идентификатора в TypeSpec и заканчивается в конце самого внутреннего содержащего блока.

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

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

Области применения меток

Метки объявляются в операторах с метками и используются в операторах “break”, “continue” и “goto”. Запрещено определять метку, которая никогда не используется. В отличие от других идентификаторов, метки не имеют блочного диапазона и не конфликтуют с идентификаторами, которые не являются метками. Областью действия метки является тело функции, в которой она объявлена, и исключает тело любой вложенной функции.

Пустой идентификатор

Пустой идентификатор обозначается символом подчеркивания _. Он служит анонимным заполнителем вместо обычного (непустого) идентификатора и имеет особое значение в объявлениях, в качестве операнда и в операторах присваивания.

Объявленные идентификаторы

Следующие идентификаторы неявно объявлены в блоке universe [Go 1.18] [Go 1.21]:

Types:
	any bool byte comparable
	complex64 complex128 error float32 float64
	int int8 int16 int32 int64 rune string
	uint uint8 uint16 uint32 uint64 uintptr

Constants:
	true false iota

Zero value:
	nil

Functions:
	append cap clear close complex copy delete imag len
	make max min new panic print println real recover

Экспортируемые идентификаторы

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

  1. первый символ имени идентификатора является заглавной буквой Юникода (категория символов Юникода Lu); и
  2. идентификатор объявлен в блоке пакета или является именем поля или метода.

Все остальные идентификаторы не экспортируются.

Уникальность идентификаторов

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

Объявление констант

Объявление констант связывает список идентификаторов (имена констант) со значениями списка константных выражений. Количество идентификаторов должно быть равно количеству выражений, и n-й идентификатор слева привязывается к значению n-го выражения справа.

ConstDecl      = "const" ( ConstSpec | "(" { ConstSpec ";" } ")" ) .
ConstSpec      = IdentifierList [ [ Type ] "=" ExpressionList ] .

IdentifierList = identifier { "," identifier } .
ExpressionList = Expression { "," Expression } .

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

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

Если значения выражений являются нетипизированными константами, объявленные константы остаются нетипизированными, а идентификаторы констант обозначают значения констант.

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

const Pi float64 = 3.14159265358979323846
const zero = 0.0         // untyped floating-point constant
const (
	size int64 = 1024
	eof        = -1  // untyped integer constant
)
const a, b, c = 3, 4, "foo"  // a = 3, b = 4, c = "foo", untyped integer and string constants
const u, v float32 = 0, 3    // u = 0.0, v = 3.0

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

const (
	Sunday = iota
	Monday
	Tuesday
	Wednesday
	Thursday
	Friday
	Partyday
	numberOfDays  // this constant is not exported
)

Iota

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

const (
	c0 = iota  // c0 == 0
	c1 = iota  // c1 == 1
	c2 = iota  // c2 == 2
)

const (
	a = 1 << iota  // a == 1  (iota == 0)
	b = 1 << iota  // b == 2  (iota == 1)
	c = 3          // c == 3  (iota == 2, unused)
	d = 1 << iota  // d == 8  (iota == 3)
)

const (
	u         = iota * 42  // u == 0     (untyped integer constant)
	v float64 = iota * 42  // v == 42.0  (float64 constant)
	w         = iota * 42  // w == 84    (untyped integer constant)
)

const x = iota  // x == 0
const y = iota  // y == 0

По определению, несколько применений iota в одном и том же ConstSpec имеют одно и то же значение:

const (
	bit0, mask0 = 1 << iota, 1<<iota - 1  // bit0 == 1, mask0 == 0  (iota == 0)
	bit1, mask1                           // bit1 == 2, mask1 == 1  (iota == 1)
	_, _                                  //                        (iota == 2, unused)
	bit3, mask3                           // bit3 == 8, mask3 == 7  (iota == 3)
)

В последнем примере используется неявное повторение последнего непустого списка выражений.

Декларации типов

Декларация типа связывает идентификатор, имени типа, с типом. Декларации типов бывают двух видов: декларации псевдонимов и определения типов.

TypeDecl = "type" ( TypeSpec | "(" { TypeSpec ";" } ")" ) .
TypeSpec = AliasDecl | TypeDef .

Объявление псевдонимов

Объявление псевдонимов связывает идентификатор с заданным типом [Go 1.9].

AliasDecl = identifier [ TypeParameters ] "=" Type .

В пределах области действия идентификатора он служит псевдонимом для данного типа.

type (
	nodeList = []*Node // nodeList и []*Node являются идентичными типами
	Polar = polar // Polar и polar обозначают идентичные типы
)

Если в объявлении псевдонима указаны параметры типа [Go 1.24], то имя типа обозначает общий псевдоним. Общие псевдонимы должны быть инстанцированы при их использовании.

type set[P comparable] = map[P]bool

В объявлении псевдонима заданный тип не может быть параметром типа.

type A[P any] = P // ошибка: P является параметром типа

Определения типов

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

TypeDef = identifier [ TypeParameters ] Type .

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

type (
	Point struct{ x, y float64 }  // Point and struct{ x, y float64 } are different types
	polar Point                   // polar and Point denote different types
)

type TreeNode struct {
	left, right *TreeNode
	value any
}

type Block interface {
	BlockSize() int
	Encrypt(src, dst []byte)
	Decrypt(src, dst []byte)
}

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

// Mutex - это тип данных с двумя методами, Lock и Unlock.
type Mutex struct { /* Поля Mutex */ }
func (m *Mutex) Lock() { /* Реализация Lock */ }
func (m *Mutex) Unlock() { /* Реализация Unlock */ }

// NewMutex имеет тот же состав, что и Mutex, но его набор методов пуст.
type NewMutex Mutex

// Набор методов PtrMutex, лежащий в основе типа *Mutex, остается неизменным,
// но набор методов PtrMutex пуст.
type PtrMutex *Mutex

// Набор методов *PrintableMutex содержит методы
// Lock и Unlock, привязанные к его встроенному полю Mutex.
type PrintableMutex struct {
Mutex
}

// MyBlock - интерфейсный тип, имеющий тот же набор методов, что и Block.
type MyBlock Block

Определения типов могут использоваться для определения различных булевых, числовых или строковых типов и связывания с ними методов:

type TimeZone int

const (
	EST TimeZone = -(5 + iota)
	CST
	MST
	PST
)

func (tz TimeZone) String() string {
	return fmt.Sprintf("GMT%+dh", tz)
}

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

type List[T any] struct {
	next  *List[T]
	value T
}

В определении типа заданный тип не может быть параметром типа.

type T[P any] P // ошибка: P - параметр типа

func f[T any]() {
	type L T // ошибка: T - параметр типа, объявленный в объемлющей функции
}

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

// Метод Len возвращает количество элементов в связанном списке l.
func (l *List[T]) Len() int { ... }

Объявление параметров типа

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

TypeParameters = "[" TypeParamList [ "," ] "]" .
TypeParamList  = TypeParamDecl { "," TypeParamDecl } .
TypeParamDecl  = IdentifierList TypeConstraint .

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

[P any]
[S interface{ ~[]byte|string }]
[S ~[]E, E any]
[P Constraint[int]]
[_ any]

Подобно тому, как каждый параметр обычной функции имеет тип параметра, каждый параметр типа имеет соответствующий (мета-)тип, который называется ограничением типа.

Неоднозначность при разборе возникает, когда в списке параметров типа для общего типа объявляется единственный параметр типа P с ограничением C таким образом, что текст P C образует допустимое выражение:

type T[P *C] 
type T[P (C)] 
type T[P *C|Q] 

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

type T[P interface{*C}] 
type T[P *C,] 

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

В списке параметров типа родового типа T ограничение типа не может (прямо или косвенно через список параметров типа другого родового типа) ссылаться на T.

type T1[P T1[P]] ...                  // illegal: T1 ссылается на себя
type T2[P interface{ T2[int] }] ...   // illegal: T2 ссылается на себя
type T3[P interface{ m(T3[int])}] ... // illegal: T3 ссылается на себя
type T4[P T5[P]] ...                  // illegal: T4 ссылается на T5 и
type T5[P T4[P]] ...                  // T5 ссылается на T4

type T6[P int] struct{ f *T6[P] }     // нормально: ссылка на T6 отсутствует в списке параметров типа

Ограничения типа

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

TypeConstraint = TypeElem .

Если ограничение представляет собой интерфейсный литерал вида interface{E}, где E - элемент встроенного типа (не метод), то в списке параметров типа вложенный интерфейс{ ...} может быть опущен для удобства:

[T []P]                      // = [T interface{[]P}]
[T ~int]                     // = [T interface{~int}]
[T int|string]               // = [T interface{int|string}]
type Constraint ~int         // illegal: ~int is not in a type parameter list

Объявленный тип интерфейса comparable обозначает множество всех неинтерфейсных типов, которые строго сравнимы [Go 1.18].

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

int                          // реализует сравнимость (int строго сравнимо)
[]byte                       // не реализует сравнимость (кусочки не могут быть сравнимы)
interface{}                  // не реализует сравнимое (см. выше)
interface{ ~int | ~string }  // только параметр типа: реализует сравнимость (типы int, string строго сравнимы)
interface{ comparable }      // только параметр типа: реализует comparable (comparable реализует сам себя)
interface{ ~int | ~[]byte }  // только параметр типа: не реализует сравнимое (срезы не сравнимы)
interface{ ~struct{ any } }  // только параметр типа: не реализует сравнимое (поле any не является строго сравнимым)

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

Удовлетворение ограничения типа

Аргумент типа T удовлетворяет ограничению типа C, если T является элементом множества типов, определяемого C; другими словами, если T реализует C. В качестве исключения, строго сравнимое ограничение типа может быть также удовлетворено сравнимым (не обязательно строго сравнимым) аргументом типа [Go 1.20]. Точнее:

Тип T удовлетворяет ограничению C, если

  1. T реализует C; или
  2. C может быть записан в виде interface{ comparable; E }, где E - базовый интерфейс, а T сопоставим и реализует E.
тип аргумента тип ограничения удовлетворение ограничений
int interface{ ~int } удовлетворено: int реализует интерфейс{ ~int }
string comparable удовлетворено: string реализует comparable (string строго сравнима)
[]byte comparable не удовлетворено: срезы не сравнимы
any interface{ comparable; int } не удовлетворено: any не реализует интерфейс{ int }
any comparable удовлетворено: any сравнимо и реализует базовый интерфейс any
struct{f any} comparable удовлетворено: struct{f any} сравнимо и реализует базовый интерфейс any
any interface{ comparable; m() } не удовлетворено: any не реализует базовый интерфейс interface{ m() }
interface{ m() } interface{ comparable; m() } удовлетворено: интерфейс{ m() } сопоставим и реализует базовый интерфейс интерфейс{ m() }

Из-за исключения в правиле удовлетворения ограничений сравнение операндов типа parameter может вызвать панику во время выполнения (несмотря на то, что параметры сравнимого типа всегда строго сравнимы).

Объявление переменных

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

VarDecl = "var" ( VarSpec | "(" { VarSpec ";" } ")" ) .
VarSpec = IdentifierList ( Type [ "=" ExpressionList ] | "=" ExpressionList ) .
var i int
var U, V, W float64
var k = 0
var x, y float32 = -1, -2
var (
	i       int
	u, v, s = 2.0, 3.0, "bar"
)
var re, im = complexSqrt(-1)
var _, found = entries[name]  // map lookup; only interested in "found"

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

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

var d = math.Sin(0.5)  // d is float64
var i = 42             // i is int
var t, ok = x.(T)      // t is T, ok is bool
var n = nil            // illegal

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

Объявление коротких переменных

Объявление короткой переменной использует синтаксис:

ShortVarDecl = IdentifierList ":=" ExpressionList .

Это сокращение для обычного объявления переменной с выражениями-инициализаторами, но без типов:

"var" IdentifierList "=" ExpressionList .
i, j := 0, 10
f := func() int { return 7 }
ch := make(chan int)
r, w, _ := os.Pipe() // os. Pipe() возвращает соединенную пару Files и ошибку, если таковая имеется
_, y, _ := coord(p)  // coord() возвращает три значения; интересует только координата y

В отличие от обычных объявлений переменных, короткое объявление переменных может повторно объявлять переменные, если они были первоначально объявлены ранее в том же блоке (или в списках параметров, если блок является телом функции) с тем же типом, и хотя бы одна из непустых переменных является новой. Как следствие, повторное объявление может появиться только в коротком объявлении с несколькими переменными. Повторное объявление не вводит новую переменную; оно просто присваивает новое значение исходной. Непустые имена переменных в левой части := должны быть уникальными.

field1, offset := nextField(str, 0)
field2, offset := nextField(str, offset) // повторно объявляет смещение
x, y, x := 1, 2, 3 // незаконно: x повторяется в левой части :=

Короткие объявления переменных могут появляться только внутри функций. В некоторых контекстах, например в инициализаторах операторов “if”, “for” или “switch”, они могут использоваться для объявления локальных временных переменных.

Объявление функций

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

FunctionDecl = "func" FunctionName [ TypeParameters ] Signature [ FunctionBody ] .
FunctionName = identifier .
FunctionBody = Block .

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

func IndexRune(s string, r rune) int {
for i, c := range s {
if c == r {
return i
}
	}
	// недопустимо: отсутствует оператор return
}

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

func min[T ~int|~float64](x, y T) T {
if x < y {
return x
}
return y
}

Объявление функции без параметров типа может не содержать тела. Такое объявление представляет собой сигнатуру для функции, реализованной вне Go, например, в ассемблере.

func flushICache(begin, end uintptr) // реализовано извне

Объявление методов

Метод - это функция с приемником. Объявление метода привязывает к методу идентификатор, имя метода, и связывает метод с базовым типом приемника.

MethodDecl = "func" Receiver MethodName Signature [ FunctionBody ] .
Receiver   = Parameters .

Приемник указывается в дополнительной секции параметров, предшествующей имени метода. В этой секции параметров должен быть объявлен единственный невариативный параметр - приемник. Его тип должен быть определенным типом T или указателем на определенный тип T, за которым, возможно, следует список имен параметров типа [P1, P2, ...], заключенных в квадратные скобки. T называется базовым типом приемника. Базовый тип-приемник не может быть указателем или интерфейсным типом и должен быть определен в том же пакете, что и метод. Считается, что метод привязан к базовому типу-приемнику, и имя метода видно только в селекторах для типа T или *T.

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

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

Заданный определенный тип Point

func (p *Point) Length() float64 {
	return math.Sqrt(p.x * p.x + p.y * p.y)
}

func (p *Point) Scale(factor float64) {
	p.x *= factor
	p.y *= factor
}

привязать методы Length и Scale, с типом приемника *Point, к базовому типу Point.

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

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

Ограничения параметров типа-приемника подразумеваются определением базового типа-приемника: соответствующие параметры типа имеют соответствующие ограничения.

type Pair[A, B any] struct {
	a A
	b B
}

func (p Pair[A, B]) Swap() Pair[B, A] { ... }  // приемник объявляет A, B
func (p Pair[First, _]) First() First { ... }  // получатель объявляет First, соответствует A в Pair

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

type GPoint[P any] = Point
type HPoint = *GPoint[int]
type IPair = Pair[int, int]

func (*GPoint[P]) Draw(P) { ... }   // illegal: alias не должен быть общим
func (HPPoint) Draw(P) { ... }      // illegal: alias не должен обозначать инстанцированный тип GPoint[int]
func (*IPair) Second() int { ... }  // illegal: псевдоним не должен обозначать инстанцированный тип Pair[int, int]

Выражения

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

Операнды

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

Operand     = Literal | OperandName [ TypeArgs ] | "(" Expression ")" .
Literal     = BasicLit | CompositeLit | FunctionLit .
BasicLit    = int_lit | float_lit | imaginary_lit | rune_lit | string_lit .
OperandName = identifier | QualifiedIdent .

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

Пустой идентификатор может появляться в качестве операнда только в левой части оператора присваивания.

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

Квалифицированные идентификаторы

Квалифицированный идентификатор - это идентификатор, снабженный префиксом имени пакета. И имя пакета, и идентификатор не должны быть пустыми.

QualifiedIdent = PackageName "." identifier .

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

math.Sin // обозначает функцию Sin в пакете math

Составные литералы

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

CompositeLit = LiteralType LiteralValue .
LiteralType  = StructType | ArrayType | "[" "..." "]" ElementType |
               SliceType | MapType | TypeName [ TypeArgs ] .
LiteralValue = "{" [ ElementList [ "," ] ] "}" .
ElementList  = KeyedElement { "," KeyedElement } .
KeyedElement = [ Key ":" ] Element .
Key          = FieldName | Expression | LiteralValue .
FieldName    = identifier .
Element      = Expression | LiteralValue .

Основной тип LiteralType T должен быть типом struct, array, slice или map (в синтаксисе это ограничение соблюдается, за исключением случаев, когда тип задан как TypeName). Типы элементов и ключей должны быть присваиваемыми соответствующим типам поля, элемента и ключа типа T; дополнительное преобразование не требуется.

Ключ интерпретируется как имя поля для литералов struct, индекс для литералов array и slice и ключ для литералов map. Для литералов map все элементы должны иметь ключ. Ошибкой является указание нескольких элементов с одним и тем же именем поля или постоянным значением ключа. О непостоянных ключах map см. раздел о порядке объявлений.

Для литералов struct действуют следующие правила:

  1. Ключ должен быть именем поля, объявленного в типе struct.
  2. Список элементов, не содержащий ключей, должен содержать элемент для каждого поля struct в том порядке, в котором эти поля объявлены.
  3. Если любой элемент имеет ключ, то каждый элемент должен иметь ключ.
  4. Список элементов, содержащий ключи, не обязательно должен содержать элемент для каждого поля struct. Опущенные поля получают нулевое значение для этого поля.
  5. Литерал может не содержать список элементов; такой литерал оценивается в нулевое значение для своего типа.
  6. Ошибкой является указание элемента для неэкспортируемого поля структуры, принадлежащей другому пакету.

Учитывая сказанное

type Point3D struct { x, y, z float64 }
type Line struct { p, q Point3D }

и теперь можно написать

origin := Point3D{}                            // zero value for Point3D
line := Line{origin, Point3D{y: -4, z: 12.3}}  // zero value for line.q.x

Для литералов массивов и фрагментов действуют следующие правила:

  1. Каждый элемент имеет связанный с ним целочисленный индекс, отмечающий его положение в массиве.
  2. Элемент с ключом использует ключ в качестве своего индекса. Ключ должен быть неотрицательной константой, представляемой значением типа int; а если он типизирован, то должен быть целочисленного типа.
  3. Элемент без ключа использует индекс предыдущего элемента плюс один. Если первый элемент не имеет ключа, его индекс равен нулю.

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

var pointer *Point3D = &Point3D{y: 1000}

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

p1 := &[]int{}    // p1 указывает на инициализированный, пустой фрагмент со значением []int{} и длиной 0
p2 := new([]int) // p2 указывает на неинициализированный фрагмент со значением nil и длиной 0

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

buffer := [10]string{}             // len(buffer) == 10
intSet := [6]int{1, 2, 3, 5}       // len(intSet) == 6
days := [...]string{"Sat", "Sun"}  // len(days) == 2

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

Литерал среза имеет вид

[]T{x1, x2, ... xn}

и является сокращением для операции slice, применяемой к массиву:

tmp := [n]T{x1, x2, ... xn}
tmp[0 : n]

Внутри составного литерала массива, среза или карты типа T элементы или ключи карты, которые сами являются составными литералами, могут элиминировать соответствующий тип литерала, если он идентичен типу элемента или ключа T. Аналогично, элементы или ключи, которые являются адресами составных литералов, могут элиминировать &T, если тип элемента или ключа *T.

[...]Point{{1.5, -3.5}, {0, 0}}     // аналогично [...]Point{Point{1.5, -3.5}, Point{0, 0}}
[][]int{{1, 2, 3}, {4, 5}}          // аналогично [][]int{[]int{1, 2, 3}, []int{4, 5}}
[][]Point{{{0, 1}, {1, 2}}}         // аналогично map[string]Point{"orig": Point{0, 0}}
map[Point]string{{0, 0}: "orig"}    // аналогично map[Point]string{Point{0, 0}: "orig"}

type PPoint *Point
[2]*Point{{1.5, -3.5}, {}}          // аналогично [2]*Point{&Point{1.5, -3.5}, &Point{}}
[2]PPoint{{1.5, -3.5}, {}}          // аналогично [2]PPoint{PPoint(&Point{1.5, -3.5}), PPoint(&Point{})}

Неоднозначность разбора возникает, когда составной литерал, использующий форму TypeName типа LiteralType, появляется в качестве операнда между ключевым словом и открывающей скобкой блока оператора “if”, “for” или “switch”, а составной литерал не заключен в круглые скобки, квадратные скобки или фигурные скобки. В этом редком случае открывающая скобка литерала ошибочно воспринимается как скобка, вводящая блок операторов. Чтобы устранить двусмысленность, составной литерал должен быть заключен в круглые скобки.

if x == (T{a,b,c}[i]) {  }
if (x == T{a,b,c}[i]) {  }

Примеры допустимых литералов массивов, фрагментов и карт:

// список простых чисел
primes := []int{2, 3, 5, 7, 9, 2147483647}

// vowels[ch] - true, если ch - гласная
vowels := [128]bool{'a': true, 'e': true, 'i': true, 'o': true, 'u': true, 'y': true}

// массив [10]float32{-1, 0, 0, 0, -0.1, -0.1, 0, 0, 0, -1}
filter := [10]float32{-1, 4: -0.1, -0.1, 9: -1}

// частоты в Гц для равнотемперированной шкалы (A4 = 440 Гц)
noteFrequency := map[string]float32{
"C0": 16.35, "D0": 18.35, "E0": 20.60, "F0": 21.83,
"G0": 24.50, "A0": 27.50, "B0": 30.87,
}

Литералы функций

Литерал функции представляет собой анонимную функцию. Функциональные литералы не могут объявлять параметры типа.

FunctionLit = "func" Signature FunctionBody .
func(a, b int, z float64) bool { return a*b < int(z) }

Литерал функции может быть присвоен переменной или вызван напрямую.

f := func(x, y int) int { return x + y }
func(ch chan int) { ch <- ACK }(replyChan)

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

Первичные выражения

Первичные выражения - это операнды для унарных и бинарных выражений.

PrimaryExpr   = Operand |
                Conversion |
                MethodExpr |
                PrimaryExpr Selector |
                PrimaryExpr Index |
                PrimaryExpr Slice |
                PrimaryExpr TypeAssertion |
                PrimaryExpr Arguments .

Selector      = "." identifier .
Index         = "[" Expression [ "," ] "]" .
Slice         = "[" [ Expression ] ":" [ Expression ] "]" |
                "[" [ Expression ] ":" Expression ":" Expression "]" .
TypeAssertion = "." "(" Type ")" .
Arguments     = "(" [ ( ExpressionList | Type [ "," ExpressionList ] ) [ "..." ] [ "," ] ] ")" .
x
2
(s + ".txt")
f(3.1415, true)
Point{1, 2}
m["foo"]
s[i : j + 1]
obj.color
f.p[i].x()

Selectors (Селекторы)

Для первичного выражения x, не являющегося именем пакета, селекторное выражение

x.f

обозначает поле или метод f значения x (или иногда *x; см. ниже). Идентификатор f называется селектором (поля или метода); он не должен быть пустым идентификатором. Тип выражения селектора равен типу f. Если x - это имя пакета, см. раздел о квалифицированных идентификаторах.

Селектор f может обозначать поле или метод f типа T, либо ссылаться на поле или метод f вложенного в него вложенного поля T. Количество вложенных полей, пройденных для достижения f, называется его глубиной в T. Глубина поля или метода f, объявленного в T, равна нулю. Глубина поля или метода f, объявленного во вложенном поле A в T, равна глубине f в A плюс один.

К селекторам применяются следующие правила:

  1. Для значения x типа T или *T, где T не является указателем или интерфейсным типом, x.f обозначает поле или метод на самой малой глубине в T, где есть такой f. Если не существует ровно одного f с самой малой глубиной, то выражение селектора является незаконным.
  2. Для значения x типа I, где I - интерфейсный тип, x.f обозначает фактический метод с именем f динамического значения x. Если в наборе методов I нет метода с именем f, то селекторное выражение незаконно.
  3. В качестве исключения, если тип x является определенным типом указателя и (*x).f является допустимым селекторным выражением, обозначающим поле (но не метод), x.f является сокращением для (*x).f.
  4. Во всех остальных случаях x.f является незаконным.
  5. Если x имеет тип указателя и значение nil, а x.f обозначает поле struct, присваивание или вычисление x.f вызовет панику во время выполнения.
  6. Если x имеет тип интерфейса и значение nil, вызов или оценка метода x.f приводит к панике во время выполнения.

Например, учитывая сказанное:

type T0 struct {
	x int
}

func (*T0) M0()

type T1 struct {
	y int
}

func (T1) M1()

type T2 struct {
	z int
	T1
	*T0
}

func (*T2) M2()

type Q *T2

var t T2     // with t.T0 != nil
var p *T2    // with p != nil and (*p).T0 != nil
var q Q = p

можно написать:

t.z // t.z
t.y // t.T1.y
t.x // (*t.T0).x

p.z // (*p).z
p.y // (*p).T1.y
p.x // (*(*p).T0).x

q.x // (*(*q).T0).x (*q).x является допустимым селектором поля

p.M0() // ((*p).T0).M0() M0 ожидает приемник *T0
p.M1() // ((*p).T1).M1() M1 ожидает приемник T1
p.M2() // p.M2() M2 ожидает приемник *T2
t.M2() // (&t).M2() M2 ожидает *T2-приемник, см. раздел Calls (Вызовы)

но следующее недействительно:

q.M0() // (*q).M0 действителен, но не является селектором поля

Выражения в методах

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

MethodExpr = ReceiverType "." MethodName .
ReceiverType = Type .

Рассмотрим структуру типа T с двумя методами, Mv, приемник который имеет тип T, и Mp, приемник который имеет тип *T.

type T struct {
a int
}
func (tv T) Mv(a int) int { return 0 }  // приемник значений
func (tp *T) Mp(f float32) float32 { return 1 }  // приемник указателей

var t T

Выражение

T.Mv

дает функцию, эквивалентную Mv, но с явным приемником в качестве первого аргумента; она имеет сигнатуру

func(tv T, a int) int

Эта функция может быть вызвана обычным образом с явным приемником, так что эти пять вызовов эквивалентны:

t.Mv(7)
T.Mv(t, 7)
(T).Mv(t, 7)
f1 := T.Mv; f1(t, 7)
f2 := (T).Mv; f2(t, 7)

Аналогично, выражение

(*T).Mp

дает функцию, представляющую значение Mp с сигнатурой

func(tp *T, f float32) float32

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

(*T).Mv

дает функцию, представляющую значение Mv с сигнатурой

func(tv *T, a int) int

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

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

Функции-значения, получаемые из методов, вызываются с помощью синтаксиса вызова функции; приемник предоставляется в качестве первого аргумента вызова. То есть, если дано f := T.Mv, то f вызывается как f(t, 7), а не t.f(7). Чтобы сконструировать функцию, связывающую приемник, используйте литерал функции или значение метода.

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

Значения метода

Если выражение x имеет статический тип T, а M находится в наборе методов типа T, то x.M называется значением метода. Значение метода x.M - это значение функции, которое можно вызвать с теми же аргументами, что и вызов метода x.M. Выражение x рассчитывается и сохраняется во время вычислений значения метода; сохраненная копия затем используется в качестве приемника в любых вызовах, которые могут быть выполнены позже.

type S struct { *T }
type T int
func (t T) M() { print(t) }

t := new(T)
s := S{T: t}
f := t.M // приемник *t рассчитывается и хранится в f
g := s.M // приемник *(s.T) рассчитывается и хранится в g
*t = 42 // не влияет на сохраненные приемники в f и g

Тип T может быть интерфейсным или неинтерфейсным типом.

Как и при обсуждении выражений методов выше, рассмотрим тип ёstruct Tё с двумя методами, Mv, приемник которого имеет тип T, и Mp, приемник которого имеет тип *T.

type T struct {
	a int
}
func (tv  T) Mv(a int) int         { return 0 }  // value receiver
func (tp *T) Mp(f float32) float32 { return 1 }  // pointer receiver

var t T
var pt *T
func makeT() T

Выражение

t.Mv

дает значение функции типа

func(int) int

Эти два вызова эквивалентны:

t.Mv(7)
f := t.Mv; f(7)

Аналогично, выражение

pt.Mp

дает значение функции типа

func(float32) float32

Как и в случае с селекторами, ссылка на неинтерфейсный метод с приемником значения, использующим указатель, автоматически разыменовывает этот указатель: pt.Mv эквивалентно (*pt).Mv.

Как и в случае с вызовами методов, ссылка на неинтерфейсный метод с приемником указателя, использующим адресуемое значение, автоматически принимает адрес этого значения: t.Mp эквивалентно (&t).Mp.

f := t.Mv; f(7)   // like t.Mv(7)
f := pt.Mp; f(7)  // like pt.Mp(7)
f := pt.Mv; f(7)  // like (*pt).Mv(7)
f := t.Mp; f(7)   // like (&t).Mp(7)
f := makeT().Mp   // invalid: result of makeT() is not addressable

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

var i interface { M(int) } = myVal
f := i.M; f(7)  // like i.M(7)

Индексные выражения

Первичное выражение вида

a[x]

обозначает элемент массива, указатель на массив, фрагмент, строку или карту a, проиндексированный по x. Значение x называется индексом или ключом карты соответственно. При этом действуют следующие правила:

Если a не является ни картой, ни параметром типа:

  • индекс x должен быть нетипизированной константой или его основной тип должен быть целым числом
  • константный индекс должен быть неотрицательным и представляемым значением типа int
  • нетипизированный константный индекс имеет тип int
  • индекс x находится в диапазоне, если 0 <= x < len(a), иначе он вне диапазона

Для a типа массива A:

  • постоянный индекс должен быть в диапазоне
  • если x выходит за пределы диапазона во время выполнения, возникает паника во время выполнения
  • a[x] - это элемент массива с индексом x, а тип a[x] - это тип элемента A

Для a типа указателя на массив:

  • a[x] - сокращение от (*a)[x]

Для a типа slice S:

  • если x выходит за пределы диапазона во время выполнения, возникает паника во время выполнения
  • a[x] - это элемент среза по индексу x, а тип a[x] - это тип элемента S

Для a строкового типа:

  • константный индекс должен быть в диапазоне, если строка a также константна
  • если x выходит за пределы диапазона во время выполнения, происходит паника во время выполнения
  • a[x] - это непостоянное байтовое значение по индексу x и тип a[x] - байт
  • a[x] не может быть присвоен

Для a типа map M:

  • Тип x должен быть назначен типу ключа M
  • если map содержит запись с ключом x, a[x] является элементом map с ключом x и тип a[x] является типом элемента M
  • если map равен nil или не содержит такой записи, a[x] является нулевым значением для типа элемента M

Для a типа параметра типа P:

  • Индексное выражение a[x] должно быть допустимым для значений всех типов в наборе типов P.
  • Типы элементов всех типов в наборе типов P должны быть одинаковыми. В данном контексте типом элемента строкового типа является байт.
  • Если в наборе типов P есть тип map, то все типы в этом наборе типов должны быть типами map, а соответствующие типы ключей должны быть идентичными.
  • a[x] - это элемент массива, среза или строки по индексу x или элемент map с ключом x аргумента типа, с которым инстанцируется P, а тип a[x] - это тип (идентичных) типов элементов.
  • a[x] не может быть присвоен, если набор типов P включает типы string.

В противном случае a[x] является незаконным.

Индексное выражение на карте a типа map[K]V, используемое в операторе присваивания или инициализации специальной формы

v, ok = a[x]
v, ok := a[x]
var v, ok = a[x]

дает дополнительное нетипизированное булево значение. Значение ok равно true, если ключ x присутствует в карте, и false в противном случае.

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

Выражения срезов (частей массивов)

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

Простые выражения срезов

Основное выражение

a[low : high]

строит подстроку или фрагмент. Основной тип a должен быть строкой, массивом, указателем на массив, фрагментом или байтовой строкой. Индексы low и high определяют, какие элементы операнда a появятся в результате. Результат имеет индексы, начинающиеся с 0, и длину, равную high - low. После нарезки массива a

a := [5]int{1, 2, 3, 4, 5}
s := a[1:4]

срез s имеет тип []int, длину 3, емкость 4 и элементы

s[0] == 2
s[1] == 3
s[2] == 4

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

a[2:] // то же, что a[2 : len(a)]
a[:3] // то же, что a[0 : 3]
a[:] // то же, что a[0 : len(a)]

Если a - указатель на массив, то a[low : high] - это сокращение от (*a)[low : high].

Для массивов или строк индексы находятся в диапазоне, если 0 <= low <= high <= len(a), в противном случае они выходят за пределы диапазона. Для срезов верхней границей индекса является емкость среза cap(a), а не его длина. Постоянный индекс должен быть неотрицательным и представляться значением типа int; для массивов или постоянных строк постоянные индексы также должны находиться в диапазоне. Если оба индекса постоянны, они должны удовлетворять условию low <= high. Если индексы выходят за пределы диапазона во время выполнения, происходит паника во время выполнения.

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

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

var a [10]int
s1 := a[3:7] // базовый массив s1 - это массив a; &s1[2] == &a[5]
s2 := s1[1:4] // базовый массив s2 - это базовый массив s1, который является массивом a; &s2[1] == &a[5]
s2[1] = 42 // s2[1] == s1[2] == a[5] == 42; все они ссылаются на один и тот же элемент базового массива

var s []int
s3 := s[:0] // s3 == nil

Выражения полного среза

Первичное выражение

a[low : high : max]

строит срез того же типа, с той же длиной и элементами, что и простое выражение a[low : high]. Кроме того, оно контролирует емкость результирующего среза, устанавливая ее в значение max - low. Только первый индекс может быть опущен; по умолчанию он равен 0. Основной тип a должен быть массивом, указателем на массив или срез (но не строкой). После нарезки массива a

a := [5]int{1, 2, 3, 4, 5}
t := a[1:3:5]

Срез t имеет тип []int, длину 2, емкость 4 и элементы

t[0] == 2
t[1] == 3

Что касается простых выражений нарезки, то если a - указатель на массив, то a[low : high : max] - это сокращение от (*a)[low : high : max]. Если нарезаемый операнд является массивом, он должен быть адресуемым.

Индексы находятся в диапазоне, если 0 <= low <= high <= max <= cap(a), иначе они выходят за пределы диапазона. Постоянный индекс должен быть неотрицательным и представляться значением типа int; для массивов постоянные индексы также должны находиться в диапазоне. Если константными являются несколько индексов, то присутствующие константы должны находиться в диапазоне относительно друг друга. Если индексы выходят за пределы диапазона во время выполнения, возникает паника.

Утверждения типа

Для выражения x интерфейсного типа, но не параметра типа, и типа T, первичное выражение

x.(T)

утверждает, что x не является nil и что значение, хранящееся в x, имеет тип T. Выражение x.(T) называется утверждением типа.

Точнее, если T не является типом интерфейса, x.(T) утверждает, что динамический тип x идентичен типу T. В этом случае T должен реализовывать (интерфейсный) тип x; в противном случае утверждение типа недействительно, поскольку для x невозможно хранить значение типа T. Если T - интерфейсный тип, то x.(T) утверждает, что динамический тип x реализует интерфейс T.

Если утверждение типа верно, то значением выражения будет значение, хранящееся в x, а его типом - T. Если утверждение типа ложно, то произойдет паника во время выполнения. Другими словами, несмотря на то, что динамический тип x известен только во время выполнения, в корректной программе тип x.(T) заведомо равен T.

var x interface{} = 7          // x has dynamic type int and value 7
i := x.(int)                   // i has type int and value 7

type I interface { m() }

func f(y I) {
	s := y.(string)        // illegal: string does not implement I (missing method m)
	r := y.(io.Reader)     // r has type io.Reader and the dynamic type of y must implement both I and io.Reader
	
}

Утверждение типа, используемое в операторе присваивания или инициализации специального вида

v, ok = x.(T)
v, ok := x.(T)
var v, ok = x.(T)
var v, ok interface{} = x.(T) // динамические типы v и ok - T и bool

дает дополнительное нетипизированное булево значение. Значение ok будет истинным, если утверждение верно. В противном случае оно ложно, а значение v является нулевым значением для типа T. Паники во время выполнения в этом случае не возникает.

Calls (Вызовы)

Дано выражение типа функции f с основным типом F

f(a1, a2, ... an)

вызывает f с аргументами a1, a2, ... an. За исключением одного особого случая, аргументы должны быть однозначными выражениями, присваиваемыми типам параметров F, и оцениваются до вызова функции. Тип выражения - это тип результата F. Вызов метода аналогичен, но сам метод указывается как селектор на значение типа приемника метода.

math.Atan2(x, y) // вызов функции
var pt *Point
pt.Scale(3.5) // вызов метода с приемником pt

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

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

Вызов функции с нулевым значением вызывает панику во время выполнения.

В качестве особого случая, если возвращаемые значения функции или метода g равны по числу и могут быть индивидуально назначены параметрам другой функции или метода f, то вызов f(g(параметры_от_g)) вызовет f после передачи возвращаемых значений g параметрам f по порядку. Вызов f не должен содержать никаких параметров, кроме вызова g, а g должен иметь хотя бы одно возвращаемое значение. Если у f есть конечный параметр ..., ему присваиваются возвращаемые значения g, оставшиеся после присвоения обычных параметров.

func Split(s string, pos int) (string, string) {
	return s[0:pos], s[pos:]
}

func Join(s, t string) string {
	return s + t
}

if Join(Split(value, len(value)/2)) != value {
	log.Panic("test fails")
}

Вызов метода x.m() допустим, если набор методов (или типов) x содержит m и список аргументов может быть присвоен списку параметров m. Если x является адресуемым и набор методов &x содержит m, то x.m() - это сокращение от (&x).m():

var p Point
p.Scale(3.5)

Не существует отдельного типа метода и литералов метода.

Передача аргументов в ... параметры

Если f является переменной с конечным параметром p типа ...T, то внутри f тип p эквивалентен типу []T. Если f вызывается без фактических аргументов для p, то значение, передаваемое p, равно nil. В противном случае передаваемое значение - это новый фрагмент типа []T с новым нижележащим массивом, последовательными элементами которого являются фактические аргументы, которые все должны быть присваиваемыми в T. Длина и емкость фрагмента, таким образом, равна количеству аргументов, связанных с p, и может быть разной для каждого места вызова.

Даны функция и вызовы

func Greeting(prefix string, who ...string)
Greeting("nobody")
Greeting("hello:", "Joe", "Anna", "Eileen")

внутри Greeting, who будет иметь значение nil в первом вызове, и []string{"Joe", "Anna", "Eileen"} во втором

Если последний аргумент может быть отнесен к типу slice []T и за ним следует ..., он передается без изменений как значение для параметра ...T. В этом случае новый срез не создается.

Учитывая срез s и вызов

s := []string{"Джеймс", "Жасмин"}
Greeting("goodbye:", s...)

внутри Greeting, который будет иметь то же значение, что и s, с тем же базовым массивом.

Инстанцирование

Общая функция или тип инстанцируется путем замены аргументов типа на параметры типа [Go 1.18]. Инстанцирование происходит в два этапа:

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

Инстанцирование типа приводит к созданию нового негенетического именованного типа; инстанцирование функции приводит к созданию новой негенетической функции.

список параметров типа аргументы типа после подстановки
[P any] int int удовлетворяет any
[S ~[]E, E any] []int, int []int удовлетворяет ~[]int, int удовлетворяет any
[P io.Writer] string illegal: string не удовлетворяет io.Writer
[P comparable] any any удовлетворяет (но не реализует) comparable

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

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

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

// sum возвращает сумму (конкатенацию, для строк) своих аргументов.
func sum[T ~int | ~float64 | ~string](x... T) T { ... }

x := sum // illegal: тип x неизвестен
intSum := sum[int] // intSum имеет тип func(x... int) int
a := intSum(2, 3) // a имеет значение 5 типа int
b := sum[float64](2.0, 3) // b имеет значение 5.0 типа float64
c := sum(b, -1) // c имеет значение 4.0 типа float64

type sumFunc func(x... string) string
var f sumFunc = sum // то же, что var f sumFunc = sum[string]
f = sum // то же, что f = sum[string]

Частичный список аргументов типа не может быть пустым; в нем должен присутствовать как минимум первый аргумент. Список является префиксом полного списка аргументов типа, оставляя остальные аргументы для вывода. Грубо говоря, аргументы типа могут быть опущены “справа налево”.

func apply[S ~[]E, E any](s S, f func(E) E) S { ... }

f0 := apply[] // illegal: список аргументов типа не может быть пустым
f1 := apply[[]int] // аргумент типа для S указан явно, аргумент типа для E выведен
f2 := apply[[]string, string]  // оба аргумента типа указаны явно

var bytes []byte
r := apply(bytes, func(byte) byte { ... })  // оба аргумента типа выводятся из аргументов функции

Для общего типа все аргументы типа всегда должны быть предоставлены явно.

Выведение типа

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

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

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

Например, учитывая

// dedup возвращает копию аргумента slice с удаленными дублирующимися записями.
func dedup[S ~[]E, E comparable](S) S { ... }

type Slice []int
var s Slice
s = dedup(s) // то же самое, что s = dedup[Slice, int](s)

переменная s типа Slice должна быть присваиваема параметру функции типа S, чтобы программа была корректной. Для уменьшения сложности вывод типов игнорирует направленность присваиваний, поэтому отношение типов между Slice и S можно выразить через (симметричное) уравнение типа (Slice ≡_A S) (или (S ≡_A Slice)), где A в (≡_A) указывает на то, что типы LHS и RHS должны совпадать в соответствии с правилами присваиваемости (подробнее см. раздел об унификации типов). Аналогично, параметр типа S должен удовлетворять своему ограничению ~[]E. Это можно выразить как (S ≡_C ~[\ ]E), где (X ≡_C Y) означает X удовлетворяет ограничению Y. Эти наблюдения приводят к набору двух уравнений

$$ Slice ≡_A S (1) $$ $$ S ≡_C ~[\ ]E (2) $$

который теперь можно решить для параметров типа S и E. Из (1) компилятор может сделать вывод, что аргументом типа для S является Slice. Аналогично, поскольку базовым типом Slice является []int, а []int должен соответствовать []E ограничения, компилятор может сделать вывод, что E должно быть int. Таким образом, для этих двух уравнений вывод типа дает следующее

	S  Slice
	E  int

Если задан набор уравнений типа, то параметрами типа для решения являются параметры типа функций, которые должны быть инстанцированы и для которых не заданы явные аргументы типа. Такие параметры типа называются связанными параметрами типа. Например, в приведенном выше примере dedup параметры типа S и E привязаны к dedup. Аргументом вызова обобщенной функции может быть сама обобщенная функция. Параметры типа этой функции включаются в набор связанных параметров типа. Типы аргументов функции могут содержать параметры типа из других функций (например, родовой функции, заключающей в себе вызов функции). Эти параметры типа также могут встречаться в уравнениях типов, но в данном контексте они не связаны. Уравнения типов всегда решаются только для связанных параметров типа.

Вывод типов поддерживает вызовы общих функций и присваивание общих функций переменным (явно типизированным для функций). Это включает передачу общих функций в качестве аргументов другим (возможно, также общим) функциям и возврат общих функций в качестве результатов. Вывод типа оперирует набором уравнений, специфичных для каждого из этих случаев. Уравнения выглядят следующим образом (списки аргументов типов опущены для ясности): Для вызова функции (f(a_0, a_1, …)), где (f) или аргумент функции (a_i) - общая функция:

  1. Каждая пара ((a_i, p_i)) соответствующих аргументов и параметров функции, где (a_i) не является нетипизированной константой, дает уравнение (typeof(p_i) ≡_A typeof(a_i)).

Если (a_i) - нетипизированная константа (c_j), а (typeof(p_i)) - параметр связанного типа (P_k), то пара ((c_j, P_k)) собирается отдельно от уравнений типа.

  1. Для присваивания (v = f) родовой функции (f) переменной v (неродовой) типа функции: (typeof(v) ≡_A typeof(f)).

  2. Для оператора return ..., f, ..., где f - родовая функция, возвращаемая в качестве результата в (неродовую) переменную r типа функции result: (typeof(r) ≡_A typeof(f)).

Кроме того, каждый параметр типа (P_k) и соответствующее ограничение типа (C_k) дают уравнение типа (P_k ≡_C C_k).

При выводе типов приоритет отдается информации о типе, полученной от типизированных операндов, перед рассмотрением нетипизированных констант. Поэтому вывод выполняется в два этапа:

  1. Уравнения типов решаются для параметров связанных типов с помощью унификации типов. Если унификация не удается, то вывод типа не удается.

  2. Для каждого связанного параметра типа (P_k), для которого еще не выведен аргумент типа и для которого собраны одна или несколько пар ((c_j, P_k)) с этим же параметром типа, определите константный вид констант (c_j) во всех этих парах так же, как и для константных выражений. Аргумент типа для (P_k) - это тип по умолчанию для определенного вида константы. Если вид константы не может быть определен из-за конфликтующих видов констант, вывод типа завершается неудачей.

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

Если эти две фазы прошли успешно, то вывод типа определил аргумент типа для каждого связанного параметра типа: (P_k ➞ A_k)

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

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

Унификация типов

Вывод типа решает уравнения типа с помощью унификации типов.

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

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

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

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

Например, для уравнения типа с параметром связанного типа P

([10]struct{ elem P, list [\ ]P } ≡_A [10]struct{ elem string; list [\ ]string })

Вывод типа начинается с пустой карты. Сначала унификация сравнивает структуру верхнего уровня типов LHS и RHS. Оба типа являются массивами одинаковой длины; они унифицируются, если типы элементов унифицируются. Оба типа элементов являются структурами; они унифицируются, если у них одинаковое количество полей с одинаковыми именами и если типы полей унифицируются.

Аргумент типа для P пока неизвестен (нет записи в карте), поэтому унификация P со строкой добавляет в карту отображение P ➞ string. Унификация типов поля списка требует унификации []P и []string, а значит, P и string. Поскольку аргумент типа P на данный момент известен (есть запись map для P), его аргумент типа string занимает место P. А поскольку string идентичен string, этот шаг унификации также успешен. Унификация LHS и RHS уравнения теперь завершена. Вывод типа прошел успешно, поскольку существует только одно уравнение типа, ни один шаг унификации не завершился неудачей, и карта полностью заполнена.

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

Для уравнения вида (X ≡_A Y), где X и Y - типы, участвующие в присваивании (включая передачу параметров и операторы возврата), структуры типов верхнего уровня могут объединяться слабо, но типы элементов должны объединяться точно, что соответствует правилам для присваиваний.

Для уравнения вида (P ≡_C C), где P - параметр типа, а C - соответствующее ограничение, правила унификации немного сложнее:

  • Если C имеет основной тип core(C), а P имеет аргумент известного типа A, то core(C) и A должны слабо унифицироваться. Если у P нет аргумента известного типа и C содержит ровно один терм типа T, который не является базовым (тильда) типом, унификация добавляет отображение P ➞ T в карту.
  • Если C не имеет базового типа, а P имеет аргумент известного типа A, то A должен иметь все методы C, если таковые имеются, и соответствующие типы методов должны точно унифицироваться.

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

Операторы

Операторы объединяют операнды в выражения.

Expression = UnaryExpr | Expression binary_op Expression .
UnaryExpr  = PrimaryExpr | unary_op UnaryExpr .

binary_op  = "||" | "&&" | rel_op | add_op | mul_op .
rel_op     = "==" | "!=" | "<" | "<=" | ">" | ">=" .
add_op     = "+" | "-" | "|" | "^" .
mul_op     = "*" | "/" | "%" | "<<" | ">>" | "&" | "&^" .

unary_op   = "+" | "-" | "!" | "^" | "*" | "&" | "<-" .

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

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

Правый операнд в выражении сдвига должен иметь тип integer [Go 1.13] или быть нетипизированной константой, представляемой значением типа uint. Если левый операнд неконстантного выражения сдвига является нетипизированной константой, он сначала неявно преобразуется к тому типу, который он принял бы, если бы выражение сдвига было заменено только его левым операндом.

var a [1024]byte
var s uint = 33

// Результаты следующих примеров приведены для 64-битных интов.
var i = 1<<s // 1 имеет тип int
var j int32 = 1<<s // 1 имеет тип int32; j == 0
var k = uint64(1<<s) // 1 имеет тип uint64; k == 1<<33
var m int = 1. 0<<s // 1.0 имеет тип int; m == 1<<33
var n = 1.0<<s == j // 1.0 имеет тип int32; n == true
var o = 1<<s == 2<<s // 1 и 2 имеют тип int; o == false
var p = 1<<s == 1<<33 // 1 имеет тип int; p == true
var u = 1.0<<s // illegal: 1.0 имеет тип float64, не может сдвигаться
var u1 = 1.0<<s != 0 // illegal: 1.0 имеет тип float64, сдвиг невозможен
var u2 = 1<<s != 1.0 // illegal: 1 имеет тип float64, сдвиг невозможен
var v1 float32 = 1<<s // illegal: 1 имеет тип float32, сдвиг невозможен
var v2 = string(1<<s) // illegal: 1 преобразуется в строку, сдвиг невозможен
var w int64 = 1.0<<33 // 1.0<<33 - постоянное выражение сдвига; w == 1<<33
var x = a[1.0<<s] // паника: 1.0 имеет тип int, но 1<<33 переполняет границы массива
var b = make([]byte, 1.0<<s) // 1.0 имеет тип int; len(b) == 1<<33

// Результаты следующих примеров приведены для 32-битных интов,
// что означает, что сдвиги будут переполняться.
var mm int = 1.0<<s // 1.0 имеет тип int; mm == 0
var oo = 1<<s == 2<<s // 1 и 2 имеют тип int; oo == true
var pp = 1<<s == 1<<33 // незаконно: 1 имеет тип int, но 1<<33 переполняет int
var xx = a[1.0<<s] // 1.0 имеет тип int; xx == a[0]
var bb = make([]byte, 1.0<<s) // 1.0 имеет тип int; len(bb) == 0

Старшинство операторов

Унарные операторы имеют наивысшее старшинство. Поскольку операторы ++ и -- формируют утверждения, а не выражения, они не входят в иерархию операторов. Как следствие, оператор *p++ - это то же самое, что и (*p)++.

Существует пять уровней старшинства для бинарных операторов. Операторы умножения связываются сильнее всего, затем следуют операторы сложения, сравнения, && (логическое И) и, наконец, || (логическое ИЛИ):

Старшинство Оператор
5 * / % << >> & &^
4 + - ^ |
3 == != < <= > >=
2 &&
1 ||

Бинарные операторы с одинаковым старшинством связываются слева направо. Например, x / y * z - это то же самое, что (x / y) * z.

+x                         // x
42 + a - b                 // (42 + a) - b
23 + 3*x[i]                // 23 + (3 * x[i])
x <= f()                   // x <= f()
^a >> b                    // (^a) >> b
f() || g()                 // f() || g()
x == y+1 && <-chanInt > 0  // (x == (y+1)) && ((<-chanInt) > 0)

Арифметические операторы

Арифметические операторы применяются к числовым значениям и дают результат того же типа, что и первый операнд. Четыре стандартных арифметических оператора (+, -, *, /) применяются к целым, плавающим точкам и комплексным типам; + также применяется к строкам. Побитовые логические операторы и операторы сдвига применяются только к целым числам.

+    sum                    integers, floats, complex values, strings
-    difference             integers, floats, complex values
*    product                integers, floats, complex values
/    quotient               integers, floats, complex values
%    remainder              integers

&    bitwise AND            integers
|    bitwise OR             integers
^    bitwise XOR            integers
&^   bit clear (AND NOT)    integers

<<   left shift             integer << integer >= 0
>>   right shift            integer >> integer >= 0

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

func dotProduct[F ~float32|~float64](v1, v2 []F) F {
	var s F
	for i, x := range v1 {
		y := v2[i]
		s += x * y
	}
	return s
}

произведение x * y и сложение s += x * y вычисляются с точностью float32 или float64, соответственно, в зависимости от аргумента типа для F.

Целочисленные операторы¶

Для двух целых значений x и y целочисленный коэффициент q = x / y и остаток r = x % y удовлетворяют следующим соотношениям:

x = q*y + r and |r| < |y|

при усечении x / y до нуля (“усеченное деление”).

 x     y     x / y     x % y
 5     3       1         2
-5     3      -1        -2
 5    -3      -1         2
-5    -3       1        -2

Единственным исключением из этого правила является то, что если делитель x является отрицательным значением для типа int, то коэфициент q = x / -1 равен xr = 0) из-за переполнения двух дополняющих целых чисел:

                         x, q
int8                     -128
int16                  -32768
int32             -2147483648
int64    -9223372036854775808

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

 x     x / 4     x % 4     x >> 2     x & 3
 11      2         3         2          3
-11     -2        -3        -3          1

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

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

Верхнее ограничение на количество сдвигов отсутствует. Операторы сдвига ведут себя так, как будто левый операнд сдвигается n раз на 1 при количестве сдвигов n. В результате x << 1 - то же самое, что x*2, а x >> 1 - то же самое, что x/2, но усеченное в сторону отрицательной бесконечности.

Для целочисленных операндов унарные операторы +, - и ^ определяются следующим образом:

+x                          0 + x
-x    негатив               0 - x
^x    побитовое дополнение  m ^ x, причем m = "все биты установлены в 1" для беззнакового x
                            и m = -1 для знакового x

Переполнение целого числа

Для беззнаковых целых значений операции +, -, * и << вычисляются по модулю 2n, где n - битовая ширина типа беззнакового целого числа. Грубо говоря, эти беззнаковые целочисленные операции отбрасывают старшие биты при переполнении, и программы могут полагаться на “обход”.

Для знаковых целых чисел операции +, -, *, / и << могут легально переполняться, а результирующее значение существует и детерминированно определяется представлением знакового целого, операцией и ее операндами. Переполнение не вызывает паники во время выполнения. Компилятор не может оптимизировать код в предположении, что переполнение не произойдет. Например, он не может считать, что x < x + 1 всегда истинно.

Операторы с плавающей точкой

Для чисел с плавающей точкой и комплексных чисел +x равно x, а -x - отрицание x. Результат деления на ноль с плавающей точкой или комплексного числа не определен за пределами стандарта IEEE 754; возникнет ли паника во время выполнения, зависит от конкретной реализации.

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

Например, в некоторых архитектурах предусмотрена инструкция “слитного умножения и сложения” (FMA), которая вычисляет x*y + z без округления промежуточного результата x*y. Эти примеры показывают, когда реализация Go может использовать эту инструкцию:

// FMA допускает вычисление r, поскольку x*y не округляется явно:
r = x*y + z
r = z; r += x*y
t = x*y; r = t + z
*p = x*y; r = *p + z
r = x*y + float64(z)

// FMA запрещено для вычисления r, так как при этом опускается округление x*y:
r = float64(x*y) + z
r = z; r += float64(x*y)
t = float64(x*y); r = t + z

Конкатенация строк

Строки можно конкатенировать с помощью оператора + или оператора присваивания +=

s := "hi" + string(c)
s += " and good bye"

Сложение строк создает новую строку путем конкатенации операндов.

Операторы сравнения

Операторы сравнения сравнивают два операнда и выдают нетипизированное булево значение.

==    equal
!=    not equal
<     less
<=    less or equal
>     greater
>=    greater or equal

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

Операторы равенства == и != применяются к операндам сравниваемых типов. Операторы упорядочивания <, <=, > и >= применяются к операндам упорядоченных типов. Эти термины и результат сравнения определяются следующим образом:

  • Булевы типы сравнимы. Два булевых значения равны, если они либо оба истинны, либо оба ложны.
  • Целочисленные типы сравнимы и упорядочены. Два целочисленных значения сравниваются обычным способом.
  • Типы с плавающей точкой сравнимы и упорядочены. Два значения с плавающей точкой сравниваются в соответствии со стандартом IEEE 754.
  • Комплексные типы сравнимы. Два комплексных значения u и v равны, если real(u) == real(v) и imag(u) == imag(v).
  • Строковые типы сравнимы и упорядочены. Два строковых значения сравниваются лексически побайтно.
  • Типы указателей сравнимы. Два значения указателя равны, если они указывают на одну и ту же переменную или если оба имеют значение nil. Указатели на разные переменные нулевого размера могут быть равны, а могут и не быть.
  • Типы каналов сопоставимы. Два значения канала равны, если они были созданы одним и тем же вызовом make или если оба имеют значение nil.
  • Типы интерфейсов, которые не являются параметрами типа, сравнимы. Два значения интерфейса равны, если у них одинаковые динамические типы и одинаковые динамические значения или если оба имеют значение nil.
  • Значение x неинтерфейсного типа X и значение t интерфейсного типа T можно сравнивать, если тип X сопоставим и X реализует T. Они равны, если динамический тип t идентичен X, а динамическое значение t равно x.
  • Типы структур сопоставимы, если все их типы полей сопоставимы. Два значения структуры равны, если их соответствующие непустые значения полей равны. Поля сравниваются в исходном порядке, и сравнение прекращается, как только два значения полей различаются (или все поля были сравнены).
  • Типы массивов сравнимы, если типы их элементов сравнимы. Два значения массива равны, если равны значения соответствующих элементов. Элементы сравниваются в порядке возрастания индекса, и сравнение прекращается, как только два значения элемента различаются (или все элементы были сравнены).
  • Параметры типа сравнимы, если они строго сравнимы (см. ниже).

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

Типы slice, map и function не поддаются сравнению. Однако в качестве особого случая значение slice, map или function может быть сравнено с заранее объявленным идентификатором nil. Сравнение значений указателя, канала и интерфейса с nil также допускается и следует из общих правил, приведенных выше.

const c = 3 < 4            // c — нетипизированная булева константа true

type MyBool bool
var x, y int
var (
    // Результат сравнения — булево значение без типа.
    // Применяются обычные правила присваивания.
    b3        = x == y // b3 имеет тип bool
    b4 bool   = x == y // b4 имеет тип bool
    b5 MyBool = x == y // b5 имеет тип MyBool)

Тип строго сопоставим, если он сопоставим, не является интерфейсным типом и не состоит из интерфейсных типов. В частности:

  • Булевы, числовые, строковые, указательные и канальные типы строго сопоставимы.
  • Типы структур строго сопоставимы, если все их типы полей строго сопоставимы.
  • Типы массивов строго сопоставимы, если типы их элементов строго сопоставимы.
  • Параметры типа строго сравнимы, если все типы в их наборе типов строго сравнимы.

Логические операторы

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

&&    conditional AND    p && q  is  "if p then q else false"
||    conditional OR     p || q  is  "if p then true else q"
!     NOT                !p      is  "not p"

Операторы адресации

Для операнда x типа T операция адресации &x генерирует указатель типа *T на x. Операнд должен быть адресуемым, то есть либо операцией индексации переменной, указателя или фрагмента; либо селектором поля адресуемого операнда struct; либо операцией индексации массива адресуемого массива. В качестве исключения из требования адресуемости x может также быть составным литералом (возможно, со скобками). Если вычисление x приведет к панике во время выполнения, то и вычисление &x тоже.

Для операнда x типа указателя *T, указатель *x обозначает переменную типа T, на которую указывает x. Если x - nil, попытка вычислить *x вызовет панику во время выполнения.

&x
&a[f(2)]
&Point{2, 3}
*p
*pf(x)

var x *int = nil
*x // вызывает панику во время выполнения
&*x // вызывает панику во время выполнения

Оператор приема

Для операнда ch, основной тип которого - канал, значением операции приема <-ch является значение, полученное из канала ch. Направление канала должно разрешать операции приема, а тип операции приема - тип элемента канала. Выражение блокируется до тех пор, пока не будет получено значение. Получение из канала нулевого значения блокируется навсегда. Операция приема по закрытому каналу всегда может быть выполнена немедленно, выдавая нулевое значение типа элемента после получения всех ранее отправленных значений.

v1 := <-ch
v2 = <-ch
f(<-ch)
<-strobe  // подождать до появления тактового импульса и отбросить полученное значение

Выражение приема, используемое в операторе присваивания или инициализации специальной формы

x, ok = <-ch
x, ok := <-ch
var x, ok = <-ch
var x, ok T = <-ch

возвращает дополнительный нетипизированный результат типа boolean, сообщающий об успешности связи. Значение ok равно true, если полученное значение было успешно отправлено в канал, или false, если это нулевое значение, сгенерированное из-за того, что канал закрыт и пуст.

Преобразования

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

Явное преобразование — это выражение вида T(x), где T — тип, а x — выражение, которое может быть преобразовано в тип T.

Conversion = Type "(" Expression [ "," ] ")" .

Если тип начинается с оператора * или <-, или если тип начинается с ключевого слова func и не имеет списка результатов, он должен быть заключен в круглые скобки, когда это необходимо, чтобы избежать двусмысленности:

*Point(p)       // то же, что *(Point(p))
(*Point)(p)     // p преобразуется в *Point
<-chan int(c)   // то же, что <-(chan int(c))
(<-chan int)(c) // c преобразуется в <- chan int
func()(x)       // сигнатура функции func() x
(func())(x)     // x преобразуется в func()
(func() int)(x) // x преобразуется в func() int
func() int(x)   // x преобразуется в func() int (недвусмысленно)

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

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

uint(iota)            // йота значение типа uint
float32(2.718281828)  // 2.718281828 типа float32
complex128(1)         // 1.0 + 0.0i типа complex128
float32(0.49999999)   // 0.5 типа float32
float64(-1e-1000)     // 0.0 типа float64
string('x')           // "x" типа string
string(0x266c)        // "♬" типа string
myString("foo" + "bar") // "foobar" типа myString
string([]byte{'a'})   // не константа: []byte{'a'} не является константой
(*int)(nil)           // не является константой: nil не является константой, *int не является булевым, числовым или строковым типом
int(1.2)              // незаконно: 1.2 не может быть представлено как int
string(65.0)          // незаконно: 65.0 не является целочисленной константой

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

func f[P ~float32|~float64]() {
	 P(1.1) 
}

преобразование P(1.1) приводит к непостоянному значению типа P, а значение 1.1 представляется как float32 или float64 в зависимости от аргумента типа для f. Соответственно, если f инстанцировано с типом float32, то числовое значение выражения P(1.1) + 1.2 будет вычислено с той же точностью, что и соответствующее непостоянное сложение float32.

Неконстантное значение x может быть преобразовано к типу T в любом из этих случаев:

  • x присваивается T.
  • без учета тегов struct (см. ниже), тип x и T не являются параметрами типа, но имеют одинаковые базовые типы.
  • без учета тегов struct (см. ниже), тип x и T являются типами-указателями, которые не являются именованными типами, а их базовые типы-указатели не являются параметрами типа, но имеют одинаковые базовые типы.
  • Тип x и T - оба целочисленные типы или типы с плавающей точкой.
  • Тип x и T - оба комплексные типы.
  • x - целое число или фрагмент байтов или рун, а T - строковый тип.
  • x - строка, а T - фрагмент байтов или рун.
  • x - фрагмент, T - массив [Go 1.20] или указатель на массив [Go 1.17], причем типы фрагмента и массива имеют одинаковые типы элементов.

Кроме того, если T или x типа V являются параметрами типа, x также может быть преобразован в тип T, если выполняется одно из следующих условий:

  • И V, и T являются параметрами типа, и значение каждого типа в наборе типов V может быть преобразовано в каждый тип в наборе типов T.
  • Только V является параметром типа и значение каждого типа из набора типов V может быть преобразовано в T.
  • Только T является параметром типа и x может быть преобразовано в каждый тип из набора типов T.
  • Теги struct игнорируются при сравнении типов struct на идентичность для целей преобразования:
type Person struct {
	Name    string
	Address *struct {
		Street string
		City   string
	}
}

var data *struct {
	Name    string `json:"name"`
	Address *struct {
		Street string `json:"street"`
		City   string `json:"city"`
	} `json:"address"`
}

var person = (*Person)(data)  // // игнорируя теги, базовые типы идентичны

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

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

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

Для преобразования неконстантных числовых значений применяются следующие правила:

  • При преобразовании между целочисленными типами, если значение является целым числом со знаком, оно расширяется по знаку до неявной бесконечной точности; в противном случае оно расширяется до нуля. Затем оно усекается, чтобы поместиться в размер типа результата. Например, если v := uint16(0x10F0), то uint32(int8(v)) == 0xFFFFFFF0. Преобразование всегда дает действительное значение; нет никаких признаков переполнения.
  • При преобразовании числа с плавающей запятой в целое число дробная часть отбрасывается (усечение в сторону нуля).
  • При преобразовании целого числа или числа с плавающей запятой в тип с плавающей запятой или комплексного числа в другой комплексный тип, значение результата округляется с точностью, указанной типом назначения. Например, значение переменной x типа float32 может храниться с дополнительной точностью, превышающей точность 32-разрядного числа IEEE 754, но float32(x) представляет результат округления значения x до 32-разрядной точности. Аналогично, x + 0.1 может использовать более 32 бит точности, но float32(x + 0.1) — нет.
  • Во всех неконстантных преобразованиях, включающих значения с плавающей запятой или комплексные значения, если тип результата не может представить значение, преобразование выполняется успешно, но значение результата зависит от реализации.

Преобразования в строковый тип и из него

  1. Преобразование фрагмента байтов в строковый тип дает строку, последовательные байты которой являются элементами фрагмента.
string([]byte{'h', 'e', 'l', 'l', '\xc3', '\xb8'})   // "hellø"
string([]byte{})                                     // ""
string([]byte(nil))                                  // ""

type bytes []byte
string(bytes{'h', 'e', 'l', 'l', '\xc3', '\xb8'})    // "hellø"

type myByte byte
string([]myByte{'w', 'o', 'r', 'l', 'd', '!'})       // "world!"
myString([]myByte{'\xf0', '\x9f', '\x8c', '\x8d'})   // "🌍"
  1. Преобразование фрагмента рун в строковый тип дает строку, которая представляет собой конкатенацию отдельных значений рун, преобразованных в строки.
string([]rune{0x767d, 0x9d6c, 0x7fd4})   // "\u767d\u9d6c\u7fd4" == "白鵬翔"
string([]rune{})                         // ""
string([]rune(nil))                      // ""

type runes []rune
string(runes{0x767d, 0x9d6c, 0x7fd4})    // "\u767d\u9d6c\u7fd4" == "白鵬翔"

type myRune rune
string([]myRune{0x266b, 0x266c})         // "\u266b\u266c" == "♫♬"
myString([]myRune{0x1f30e})              // "\U0001f30e" == "🌎"
  1. Преобразование значения типа string в фрагмент типа bytes дает ненулевой фрагмент, последовательными элементами которого являются байты строки. Объем результирующего фрагмента зависит от реализации и может быть больше длины фрагмента.
[]byte("hellø")             // []byte{'h', 'e', 'l', 'l', '\xc3', '\xb8'}
[]byte("")                  // []byte{}

bytes("hellø")              // []byte{'h', 'e', 'l', 'l', '\xc3', '\xb8'}

[]myByte("world!")          // []myByte{'w', 'o', 'r', 'l', 'd', '!'}
[]myByte(myString("🌏"))    // []myByte{'\xf0', '\x9f', '\x8c', '\x8f'}
  1. Преобразование значения типа string в фрагмент типа runes дает фрагмент, содержащий отдельные кодовые позиции Unicode строки. Объем результирующего фрагмента зависит от реализации и может быть больше длины фрагмента.
[]rune(myString("白鵬翔"))   // []rune{0x767d, 0x9d6c, 0x7fd4}
[]rune("")                  // []rune{}

runes("白鵬翔")              // []rune{0x767d, 0x9d6c, 0x7fd4}

[]myRune("♫♬")              // []myRune{0x266b, 0x266c}
[]myRune(myString("🌐"))    // []myRune{0x1f310}
  1. Наконец, по историческим причинам целочисленное значение может быть преобразовано в строковый тип. При таком преобразовании получается строка, содержащая (возможно, многобайтовое) представление UTF-8 кодовой точки Юникода с заданным целочисленным значением. Значения, выходящие за пределы диапазона допустимых кодовых точек Юникода, преобразуются в "\uFFFD".
string('a')          // "a"
string(65)           // "A"
string('\xf8')       // "\u00f8" == "ø" == "\xc3\xb8"
string(-1)           // "\ufffd" == "\xef\xbf\xbd"

type myString string
myString('\u65e5')   // "\u65e5" == "日" == "\xe6\x97\xa5"

Примечание: Возможно, со временем эта форма преобразования будет удалена из языка. Инструмент go vet отмечает некоторые преобразования целых чисел в строки как потенциальные ошибки. Вместо них следует использовать библиотечные функции, такие как utf8.AppendRune или utf8.EncodeRune.

Преобразования из среза в массив или указатель массива

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

s := make([]byte, 2, 4)

a0 := [0]byte(s)
a1 := [1]byte(s[1:])     // a1[0] == s[1]
a2 := [2]byte(s)         // a2[0] == s[0]
a4 := [4]byte(s)         // panics: len([4]byte) > len(s)

s0 := (*[0]byte)(s)      // s0 != nil
s1 := (*[1]byte)(s[1:])  // &s1[0] == &s[1]
s2 := (*[2]byte)(s)      // &s2[0] == &s[0]
s4 := (*[4]byte)(s)      // panics: len([4]byte) > len(s)

var t []string
t0 := [0]string(t)       // ok for nil slice t
t1 := (*[0]string)(t)    // t1 == nil
t2 := (*[1]string)(t)    // panics: len([1]string) > len(t)

u := make([]byte, 0)
u0 := (*[0]byte)(u)      // u0 != nil

Константные выражения

Константные выражения могут содержать только константные операнды и оцениваются во время компиляции.

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

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

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

const a = 2 + 3.0          // a == 5.0   (untyped floating-point constant)
const b = 15 / 4           // b == 3     (untyped integer constant)
const c = 15 / 4.0         // c == 3.75  (untyped floating-point constant)
const Θ float64 = 3/2      // Θ == 1.0   (type float64, 3/2 is integer division)
const Π float64 = 3/2.     // Π == 1.5   (type float64, 3/2. is float division)
const d = 1 << 3.0         // d == 8     (untyped integer constant)
const e = 1.0 << 3         // e == 8     (untyped integer constant)
const f = int32(1) << 33   // illegal    (constant 8589934592 overflows int32)
const g = float64(2) >> 1  // illegal    (float64(2) is a typed floating-point constant)
const h = "foo" > "bar"    // h == true  (untyped boolean constant)
const j = true             // j == true  (untyped boolean constant)
const k = 'w' + 1          // k == 'x'   (untyped rune constant)
const l = "hi"             // l == "hi"  (untyped string constant)
const m = string(k)        // m == "x"   (type string)
const Σ = 1 - 0.707i       //            (untyped complex constant)
const Δ = Σ + 2.0e-4       //            (untyped complex constant)
const Φ = iota*1i - 1/1i   //            (untyped complex constant)

Применение встроенной функции complex к нетипизированным константам типа integer, rune или floating-point дает нетипизированную константу типа complex.

const ic = complex(0, c)   // ic == 3.75i  (untyped complex constant)
const  = complex(0, Θ)   // iΘ == 1i     (type complex128)

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

const Huge = 1 << 100         // Huge == 1267650600228229401496703205376  (untyped integer constant)
const Four int8 = Huge >> 98  // Four == 4                                (type int8)

Делитель постоянной операции деления или остатка не должен быть равен нулю:

3.14 / 0.0   // illegal: division by zero

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

uint(-1)     // -1 cannot be represented as a uint
int(3.14)    // 3.14 cannot be represented as an int
int64(Huge)  // 1267650600228229401496703205376 cannot be represented as an int64
Four * 300   // operand 300 cannot be represented as an int8 (type of Four)
Four * 100   // product 400 cannot be represented as an int8 (type of Four)

Маска, используемая унарным оператором побитового дополнения ^, соответствует правилу для неконстант: маска - это все 1 для беззнаковых констант и -1 для знаковых и нетипизированных констант.

^1         // untyped integer constant, equal to -2
uint8(^1)  // illegal: same as uint8(-2), -2 cannot be represented as a uint8
^uint8(1)  // typed uint8 constant, same as 0xFF ^ uint8(1) = uint8(0xFE)
int8(^1)   // same as int8(-2)
^int8(1)   // same as -1 ^ int8(1) = -2

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

Порядок вычислений

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

Например, в присваивании (локальном для функции)

y[f()], ok = g(z || h(), i()+x[j()], <-c), k()

вызовы функций и обмен данными происходят в порядке f(), h() (если z оценивается как false), i(), j(), <-c, g() и k(). Однако порядок этих событий по сравнению с вычислением и индексацией x и вычислением y и z не определен, за исключением лексических требований. Например, функция g не может быть вызвана до того, как будут вычислены ее аргументы.

a := 1
f := func() int { a++; return a }
x := []int{a, f()} // x может быть [1, 2] или [2, 2]: порядок оценки между a и f() не указан
m := map[int]int{a: 1, a: 2} // m может быть {2: 1} или {2: 2}: порядок оценки между двумя присваиваниями map не указан
n := map[int]int{a: f()} // n может быть {2: 3} или {3: 3}: порядок оценки между ключом и значением не указан

На уровне пакетов зависимости инициализации отменяют правило “слева направо” для отдельных выражений инициализации, но не для операндов внутри каждого выражения:

var a, b, c = f() + v(), g(), sqr(u()) + v()

func f() int        { return c }
func g() int        { return a }
func sqr(x int) int { return x*x }
// функции u и v независимы от всех других переменных и функций

Вызовы функций происходят в порядке u(), sqr(), v(), f(), v() и g().

Операции с плавающей точкой в одном выражении оцениваются в соответствии с ассоциативностью операторов. Явные круглые скобки влияют на оценку, переопределяя ассоциативность по умолчанию. В выражении x + (y + z) сложение y + z выполняется до сложения x.

Операторы

Операторы управляют исполнением.

Statement  = Declaration | LabeledStmt | SimpleStmt |
             GoStmt | ReturnStmt | BreakStmt | ContinueStmt | GotoStmt |
             FallthroughStmt | Block | IfStmt | SwitchStmt | SelectStmt | ForStmt |
             DeferStmt .

SimpleStmt = EmptyStmt | ExpressionStmt | SendStmt | IncDecStmt | Assignment | ShortVarDecl .

Завершающие операторы

Завершающий оператор прерывает обычный поток управления в блоке. Следующие операторы являются завершающими:

  1. Оператор «return» или «goto».
  2. Вызов встроенной функции panic.
  3. Блок, в котором список операторов заканчивается завершающим оператором.
  4. Оператор «if», в котором:
  • присутствует ветвь «else», и
  • обе ветви являются завершающими операторами.
  1. Оператор «for», в котором:
  • нет операторов «break», относящихся к оператору «for», и
  • отсутствует условие цикла, и
  • оператор «for» не использует диапазон (range).
  1. Оператор «switch», в котором:
  • нет операторов «break», относящихся к оператору «switch»,
  • есть default case, и
  • списки операторов в каждом случае, включая случай по умолчанию (default case), заканчиваются завершающим оператором или, возможно, помеченным оператором «fallthrough».
  1. Оператор «select», в котором:
  • нет операторов «break», относящихся к оператору «select», и
  • списки операторов в каждом случае, включая (default case), если он присутствует, заканчиваются завершающим оператором.
  1. Маркированный оператор, обозначает завершающий оператор.

Все остальные операторы не являются завершающими.

Список операторов заканчивается завершающим оператором, если список не пустой и его последний непустой оператор является завершающим.

Пустые операторы

Пустой оператор ничего не делает.

EmptyStmt = .

Маркированные операторы

Маркированный оператор может быть целью оператора goto, break или continue.

LabeledStmt = Label ":" Statement .
Label       = identifier .
Error: log.Panic("error encountered")

Выражения

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

ExpressionStmt = Expression .

Следующие встроенные функции не допускаются в контексте оператора:

append cap complex imag len make new real
unsafe.Add unsafe.Alignof unsafe.Offsetof unsafe.Sizeof unsafe.Slice unsafe.SliceData unsafe.String unsafe.StringData
h(x+y)
f.Close()
<-ch
(<-ch)
len("foo")  // illegal if len is the built-in function

Операторы отправки

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

SendStmt = Channel "<-" Expression .
Channel  = Expression .

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

ch <- 3 // отправляем значение 3 в канал ch

Операторы IncDec

Операторы "++" и "--" инкрементируют или декрементируют свои операнды на нетипизированную константу 1. Как и в случае с присваиванием, операнд должен быть адресуемым или выражением индекса карты.

IncDecStmt = Expression ( "++" | "--" ) .

Следующие операторы присваивания семантически эквивалентны:

IncDec statement    Assignment
x++                 x += 1
x--                 x -= 1

Операторы присваивания

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

Assignment = ExpressionList assign_op ExpressionList .

assign_op  = [ add_op | mul_op ] "=" .

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

x = 1
*p = f()
a[i] = 23
(k) = <-ch  // same as: k = <-ch

Операция присваивания x op= y, где op - двоичный арифметический оператор, эквивалентна x = x op (y), но оценивает x только один раз. Конструкция op= является единственной лексемой. В операциях присваивания и левый, и правый списки выражений должны содержать ровно по одному однозначному выражению, причем левое выражение не должно быть пустым идентификатором.

a[i] <<= 2
i &^= 1<<n

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

x, y = f()

присваивает первое значение x, а второе - y. Во второй форме количество операндов слева должно быть равно количеству выражений справа, каждое из которых должно быть однозначным, и n-е выражение справа присваивается n-му операнду слева:

one, two, three = '一', '二', '三'

Идентификатор blank _ позволяет игнорировать значения правой стороны в присваивании:

_ = x       // evaluate x but ignore it
x, _ = f()  // evaluate f() but ignore second result value

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

a, b = b, a  // exchange a and b

x := []int{1, 2, 3}
i := 0
i, x[i] = 1, 2  // set i = 1, x[0] = 2

i = 0
x[i], i = 2, 1  // set x[0] = 2, i = 1

x[0], x[0] = 1, 2  // set x[0] = 1, then x[0] = 2 (so x[0] == 2 at end)

x[1], x[3] = 4, 5  // set x[1] = 4, then panic setting x[3] = 5.

type Point struct { x, y int }
var p *Point
x[2], p.x = 6, 7  // set x[2] = 6, then panic setting p.x = 7

i = 2
x = []int{3, 5, 7}
for i, x[i] = range x {  // set i, x[2] = 0, x[0]
	break
}
// after this loop, i == 0 and x is []int{3, 5, 3}

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

  1. Любое типизированное значение может быть присвоено пустому идентификатору.
  2. Если нетипизированная константа присваивается переменной типа interface или идентификатору blank, константа сначала неявно преобразуется к своему типу по умолчанию.
  3. Если переменной типа interface или идентификатору blank присваивается нетипизированное булево значение, оно сначала неявно преобразуется к типу bool.

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

var s1 = []int{1, 2, 3}
var s2 = s1                     // s2 хранит дескриптор среза s1
s1 = s1[:1]                     // длина s1 равна 1, но он по-прежнему разделяет свой базовый массив с s2
s2[0] = 42                      // установка s2[0] изменяет и s1[0]
fmt.Println(s1, s2)             // выводит [42] [42 2 3]

var m1 = make(map[string]int)
var m2 = m1                     // m2 хранит дескриптор карты m1
m1["foo"] = 42                  // установка m1["foo"] изменяет m2["foo"] также
fmt.Println(m2["foo"])          // печатает 42

Операторы If

Операторы If задают условное выполнение двух ветвей в зависимости от значения булева выражения. Если выражение равно true, то выполняется ветвь "if", в противном случае, если оно присутствует, выполняется ветвь "else".

IfStmt = "if" [ SimpleStmt ";" ] Expression Block [ "else" ( IfStmt | Block ) ] .
if x > max {
	x = max
}

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

if x := f(); x < y {
	return x
} else if x > z {
	return z
} else {
	return y
}

Оператор Switch

Операторы "Switch" обеспечивают многоходовое выполнение. Выражение или тип сравнивается с "case" внутри "switch", чтобы определить, какую ветвь следует выполнить.

SwitchStmt = ExprSwitchStmt | TypeSwitchStmt .

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

Переключатели выражений

В switch оценивается выражение, а выражения case, которые не обязательно должны быть константами, оцениваются слева направо и сверху вниз; первое из них, которое совпадает с выражением switch, запускает выполнение утверждений связанного с ним case; остальные case пропускаются. Если ни один случай не совпадает и существует случай “по умолчанию”, то выполняются его утверждения. Может быть не более одного case по умолчанию, и он может находиться в любом месте оператора “switch”. Отсутствующее выражение switch эквивалентно булевому значению true.

ExprSwitchStmt = "switch" [ SimpleStmt ";" ] [ Expression ] "{" { ExprCaseClause } "}" .
ExprCaseClause = ExprSwitchCase ":" StatementList .
ExprSwitchCase = "case" ExpressionList | "default" .

Если выражение switch оценивается как константа без типа, оно сначала неявно преобразуется в свой тип по умолчанию. Предварительно объявленное значение без типа nil не может использоваться в качестве выражения switch. Тип выражения switch должен быть сопоставимым.

Если выражение case не имеет типа, оно сначала неявно преобразуется в тип выражения switch. Для каждого выражения case x и значения t выражения switch должно быть допустимым сравнение x == t.

Другими словами, выражение switch обрабатывается так, как если бы оно использовалось для объявления и инициализации временной переменной t без явного типа; именно это значение t используется для проверки равенства каждого выражения case x.

В case или default последнее непустое выражение может быть выражением «fallthrough», указывающим, что управление должно перейти от конца этого условия к первому выражению следующего условия. В противном случае управление переходит к концу выражения «switch». Выражение «fallthrough» может появляться в качестве последнего выражения всех условий, кроме последнего, выражения switch.

Выражение switch может предваряться простым оператором, который выполняется до вычисления выражения.

switch tag {
default: s3()
case 0, 1, 2, 3: s1()
case 4, 5, 6, 7: s2()
}

switch x := f(); {  // missing switch expression means "true"
case x < 0: return -x
default: return x
}

switch {
case x < y: f1()
case x < z: f2()
case x == 4: f3()
}

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

Переключатели типов

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

switch x.(type) {
// cases
}

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

TypeSwitchStmt  = "switch" [ SimpleStmt ";" ] TypeSwitchGuard "{" { TypeCaseClause } "}" .
TypeSwitchGuard = [ identifier ":=" ] PrimaryExpr "." "(" "type" ")" .
TypeCaseClause  = TypeSwitchCase ":" StatementList .
TypeSwitchCase  = "case" TypeList | "default" .

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

Вместо типа в case может использоваться заранее объявленный идентификатор nil; этот case выбирается, когда выражение в TypeSwitchGuard является значением интерфейса nil. Может существовать не более одного case nil.

Для выражения x типа interface{} используется следующий переключатель типов:

switch i := x.(type) {
case nil:
	printString("x is nil")                // type of i is type of x (interface{})
case int:
	printInt(i)                            // type of i is int
case float64:
	printFloat64(i)                        // type of i is float64
case func(int) float64:
	printFunction(i)                       // type of i is func(int) float64
case bool, string:
	printString("type is bool or string")  // type of i is type of x (interface{})
default:
	printString("don't know the type")     // type of i is type of x (interface{})
}

или в нотации if else

v := x  // x is evaluated exactly once
if v == nil {
	i := v                                 // type of i is type of x (interface{})
	printString("x is nil")
} else if i, isInt := v.(int); isInt {
	printInt(i)                            // type of i is int
} else if i, isFloat64 := v.(float64); isFloat64 {
	printFloat64(i)                        // type of i is float64
} else if i, isFunc := v.(func(int) float64); isFunc {
	printFunction(i)                       // type of i is func(int) float64
} else {
	_, isBool := v.(bool)
	_, isString := v.(string)
	if isBool || isString {
		i := v                         // type of i is type of x (interface{})
		printString("type is bool or string")
	} else {
		i := v                         // type of i is type of x (interface{})
		printString("don't know the type")
	}
}

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

func f[P any](x any) int {
	switch x.(type) {
	case P:
		return 0
	case string:
		return 1
	case []P:
		return 2
	case []byte:
		return 3
	default:
		return 4
	}
}

var v1 = f[string]("foo")   // v1 == 0
var v2 = f[byte]([]byte{})  // v2 == 2

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

Оператор "fallthrough" не допускается в переключателе типов.

Операторы for

Оператор "for" задает повторное выполнение блока. Существует три формы: Итерация может управляться одним условием, предложением "for" или предложением "range".

ForStmt   = "for" [ Condition | ForClause | RangeClause ] Block .
Condition = Expression .

Операторы For с одним условием

В своей простейшей форме оператор "for" задает повторное выполнение блока, пока булево условие оценивается как истинное. Условие оценивается перед каждой итерацией. Если условие отсутствует, оно эквивалентно булевому значению true.

for a < b {
	a *= 2
}

Операторы for с предложением for

Оператор "for" с предложением ForClause также управляется условием, но дополнительно может содержать начальный и конечный оператор, например присваивание, оператор инкремента или декремента. Оператор InitStmt может быть коротким объявлением переменной, а оператор PostStmt - нет.

ForClause = [ InitStmt ] ";" [ Condition ] ";" [ PostStmt ] .
InitStmt  = SimpleStmt .
PostStmt  = SimpleStmt .
for i := 0; i < 10; i++ {
	f(i)
}

Если он непустой, оператор init выполняется один раз перед оценкой условия для первой итерации; оператор post выполняется после каждого выполнения блока (и только если блок был выполнен). Любой элемент ForClause может быть пустым, но точка с запятой обязательна, если нет только условия. Если условие отсутствует, оно эквивалентно булевому значению true.

for cond { S() }    is the same as    for ; cond ; { S() }
for      { S() }    is the same as    for true     { S() }

Каждая итерация имеет свою отдельную объявленную переменную (или переменные) [Go 1.22]. Переменная, используемая в первой итерации, объявляется в операторе init. Переменная, используемая в каждой последующей итерации, объявляется неявно перед выполнением оператора post и инициализируется значением переменной предыдущей итерации в этот момент.

var prints []func()
for i := 0; i < 5; i++ {
	prints = append(prints, func() { println(i) })
	i++
}
for _, p := range prints {
	p()
}

напечатает

1
3
5

До версии [Go 1.22] итерации использовали один набор переменных, а не отдельные переменные. В этом случае пример выше выводит

6
6
6

Операторы for с предложением range

Оператор "for" с предложением range выполняет итерацию по всем элементам массива, фрагмента, строки или карты, значениям, полученным по каналу, целочисленным значениям от нуля до верхнего предела [Go 1.22] или значениям, переданным в функцию yield функции итератора [Go 1.23]. Для каждой записи он присваивает значения итерации соответствующим переменным итерации, если они есть, а затем выполняет блок.

RangeClause = [ ExpressionList "=" | IdentifierList ":=" ] "range" Expression .

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

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

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

Выражение диапазона x оценивается перед началом цикла, за одним исключением: если присутствует не более одной итерационной переменной и x или len(x) постоянны, выражение диапазона не оценивается.

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

Range expression                                       1st value                2nd value

array or slice      a  [n]E, *[n]E, or []E             index    i  int          a[i]       E
string              s  string type                     index    i  int          see below  rune
map                 m  map[K]V                         key      k  K            m[k]       V
channel             c  chan E, <-chan E                element  e  E
integer value       n  integer type, or untyped int    value    i  see below
function, 0 values  f  func(func() bool)
function, 1 value   f  func(func(V) bool)              value    v  V
function, 2 values  f  func(func(K, V) bool)           key      k  K            v          V
  1. Для массива, указателя на массив или значения среза a значения итерации индекса генерируются в порядке возрастания, начиная с индекса элемента 0. Если присутствует не более одной переменной итерации, цикл range генерирует значения итерации от 0 до len(a)-1 и не индексирует сам массив или срез. Для нулевого среза количество итераций равно 0.
  2. Для строкового значения оператор «range» итерирует кодовые позиции Unicode в строке, начиная с байтового индекса 0. При последующих итерациях значение индекса будет индексом первого байта последующих кодовых позиций, закодированных в UTF-8, в строке, а второе значение типа rune будет значением соответствующей кодовой позиции. Если при итерации встречается недопустимая последовательность UTF-8, второе значение будет 0xFFFD, замещающий символ Unicode, и следующая итерация продвинется на один байт в строке.
  3. Порядок итерации по картам не указан и не гарантируется, что он будет одинаковым от одной итерации к другой. Если запись карты, которая еще не была достигнута, удаляется во время итерации, соответствующее значение итерации не будет произведено. Если запись карты создается во время итерации, эта запись может быть произведена во время итерации или может быть пропущена. Выбор может варьироваться для каждой созданной записи и от одной итерации к другой. Если карта равна nil, количество итераций равно 0.
  4. Для каналов произведенные значения итерации являются последовательными значениями, отправленными по каналу, пока канал не будет закрыт. Если канал равен nil, выражение диапазона блокируется навсегда.
  5. Для целочисленного значения n, где n является целочисленным типом или нетипизированной целочисленной константой, значения итерации от 0 до n-1 генерируются в порядке возрастания. Если n является целочисленным типом, значения итерации имеют тот же тип. В противном случае тип n определяется так, как если бы он был присвоен переменной итерации. В частности: если переменная итерации уже существует, тип значений итерации является типом переменной итерации, который должен быть целочисленным типом. В противном случае, если переменная итерации объявлена с помощью клаузулы «range» или отсутствует, тип значений итерации является типом по умолчанию для n. Если n <= 0, цикл не выполняет никаких итераций.
  6. Для функции f итерация продолжается путем вызова f с новой синтезированной функцией yield в качестве аргумента. Если yield вызывается до возврата f, аргументы yield становятся значениями итерации для однократного выполнения тела цикла. После каждой последующей итерации цикла yield возвращает true и может быть вызван снова для продолжения цикла. Пока тело цикла не завершается, команда «range» будет продолжать генерировать значения итерации таким образом для каждого вызова yield, пока f не вернет результат. Если тело цикла завершается (например, оператором break), yield возвращает false и не должен вызываться снова.

Переменные итерации могут быть объявлены командой «range» с использованием формы короткого объявления переменной (:=). В этом случае их область действия — блок оператора «for», и каждая итерация имеет свои собственные новые переменные [Go 1.22] (см. также операторы «for» с ForClause). Переменные имеют типы своих соответствующих значений итерации.

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

var testdata *struct {
	a *[7]int
}
for i, _ := range testdata.a {
	// testdata.a is never evaluated; len(testdata.a) is constant
	// i ranges from 0 to 6
	f(i)
}

var a [10]string
for i, s := range a {
	// type of i is int
	// type of s is string
	// s == a[i]
	g(i, s)
}

var key string
var val interface{}  // element type of m is assignable to val
m := map[string]int{"mon":0, "tue":1, "wed":2, "thu":3, "fri":4, "sat":5, "sun":6}
for key, val = range m {
	h(key, val)
}
// key == last map key encountered in iteration
// val == map[key]

var ch chan Work = producer()
for w := range ch {
	doWork(w)
}

// empty a channel
for range ch {}

// call f(0), f(1), ... f(9)
for i := range 10 {
	// type of i is int (default type for untyped constant 10)
	f(i)
}

// invalid: 256 cannot be assigned to uint8
var u uint8
for u = range 256 {
}

// invalid: 1e3 is a floating-point constant
for range 1e3 {
}

// fibo generates the Fibonacci sequence
fibo := func(yield func(x int) bool) {
	f0, f1 := 0, 1
	for yield(f0) {
		f0, f1 = f1, f0+f1
	}
}

// print the Fibonacci numbers below 1000:
for x := range fibo {
	if x >= 1000 {
		break
	}
	fmt.Printf("%d ", x)
}
// output: 0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987

// iteration support for a recursive tree data structure
type Tree[K cmp.Ordered, V any] struct {
	left, right *Tree[K, V]
	key         K
	value       V
}

func (t *Tree[K, V]) walk(yield func(key K, val V) bool) bool {
	return t == nil || t.left.walk(yield) && yield(t.key, t.value) && t.right.walk(yield)
}

func (t *Tree[K, V]) Walk(yield func(key K, val V) bool) {
	t.walk(yield)
}

// walk tree t in-order
var t Tree[string, int]
for k, v := range t.Walk {
	// process k, v
}

Операторы Go

Оператор "go" запускает выполнение вызова функции как независимого параллельного потока управления, или goroutine, в том же адресном пространстве.

GoStmt = «go» Expression .

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

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

go Server()
go func(ch chan<- bool) { for { sleep(10); ch <- true }} (c)

Операторы select

Оператор select выбирает, какая из множества возможных операций отправки или получения будет выполнена. Он похож на оператор "switch", но все случаи относятся к коммуникационным операциям.

SelectStmt = "select" "{" { CommClause } "}" .
CommClause = CommCase ":" StatementList .
CommCase   = "case" ( SendStmt | RecvStmt ) | "default" .
RecvStmt   = [ ExpressionList "=" | IdentifierList ":=" ] RecvExpr .
RecvExpr   = Expression .

В случае с RecvStmt результат RecvExpr может быть присвоен одной или двум переменным, которые могут быть объявлены с помощью короткого объявления переменной. RecvExpr должен быть операцией приема (возможно в скобках). Может быть не более одного случая по умолчанию, и он может появляться в любом месте списка случаев.

Выполнение оператора «select» происходит в несколько этапов:

  1. Для всех случаев в операторе канал-операнды операций приема и канал и выражения правой части операторов отправки оцениваются ровно один раз, в порядке источника, при входе в оператор «select». Результатом является набор каналов для приема или отправки и соответствующие значения для отправки. Любые побочные эффекты в этой оценке будут происходить независимо от того, какая (если таковая имеется) операция связи выбрана для выполнения. Выражения в левой части RecvStmt с коротким объявлением переменной или присваиванием еще не оцениваются.
  2. Если одна или несколько коммуникаций могут быть выполнены, выбирается одна из них с помощью единого псевдослучайного выбора. В противном случае, если есть случай по умолчанию, выбирается этот случай. Если случая по умолчанию нет, оператор «select» блокируется до тех пор, пока хотя бы одна из коммуникаций не сможет быть выполнена.
  3. Если выбранный случай не является случаем по умолчанию, выполняется соответствующая операция связи.
  4. Если выбранный случай является RecvStmt с коротким объявлением переменной или присваиванием, выражения в левой части оцениваются, и полученное значение (или значения) присваиваются.
  5. Выполняется список операторов выбранного случая.

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

var a []int
var c, c1, c2, c3, c4 chan int
var i1, i2 int
select {
case i1 = <-c1:
	print("received ", i1, " from c1\n")
case c2 <- i2:
	print("sent ", i2, " to c2\n")
case i3, ok := (<-c3):  // same as: i3, ok := <-c3
	if ok {
		print("received ", i3, " from c3\n")
	} else {
		print("c3 is closed\n")
	}
case a[f()] = <-c4:
	// same as:
	// case t := <-c4
	//	a[f()] = t
default:
	print("no communication\n")
}

for {  // send random sequence of bits to c
	select {
	case c <- 0:  // note: no statement, no fallthrough, no folding of cases
	case c <- 1:
	}
}

select {}  // block forever

Операторы возврата

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

ReturnStmt = "return" [ ExpressionList ] .

В функции без типа результата оператор "return" не должен указывать никаких значений результата.

func noResult() {
return
}

Существует три способа возврата значений из функции с типом результата:

  1. Возвращаемое значение или значения могут быть явно перечислены в операторе "return". Каждое выражение должно быть однозначным и присваиваться соответствующему элементу типа результата функции.
func simpleF() int {
	return 2
}

func complexF1() (re float64, im float64) {
	return -7.0, -4.0
}
  1. Список выражений в операторе "return" может быть одним вызовом многозначной функции. Эффект будет таким, как если бы каждое значение, возвращаемое функцией, присваивалось временной переменной с типом соответствующего значения, а затем следовал оператор "return", перечисляющий эти переменные, и в этот момент применялись бы правила предыдущего случая.
func complexF2() (re float64, im float64) {
	return complexF1()
}
  1. Список выражений может быть пустым, если тип результата функции задает имена для параметров результата. Параметры результата действуют как обычные локальные переменные, и функция может присваивать им значения по мере необходимости. Оператор "return" возвращает значения этих переменных.
func complexF3() (re float64, im float64) {
	re = 7.0
	im = 4.0
	return
}

func (devnull) Write(p []byte) (n int, _ error) {
	n = len(p)
	return
}

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

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

func f(n int) (res int, err error) {
	if _, err := f(n-1); err != nil {
		return  // invalid return statement: err is shadowed
	}
	return
}

Операторы break

Оператор break завершает выполнение самого внутреннего оператора "for", "switch" или "select" внутри одной и той же функции.

BreakStmt = "break" [ Label ] .

Если есть метка, то она должна быть меткой вложенного оператора "for", "switch" или "select", и именно он завершает выполнение.

OuterLoop:
	for i = 0; i < n; i++ {
		for j = 0; j < m; j++ {
			switch a[i][j] {
			case nil:
				state = Error
				break OuterLoop
			case item:
				state = Found
				break OuterLoop
			}
		}
	}

Операторы continue

Оператор "continue" начинает следующую итерацию внутреннего цикла for, переводя управление в конец блока цикла. Цикл "for" должен находиться внутри одной и той же функции.

ContinueStmt = "continue" [ Label ] .

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

RowLoop:
	for y, row := range rows {
		for x, data := range row {
			if data == endOfRow {
				continue RowLoop
			}
			row[x] = data + bias(x, y)
		}
	}

Операторы goto

Оператор "goto" передает управление оператору с соответствующей меткой в той же функции.

GotoStmt = "goto" Label .
goto Error

Выполнение оператора "goto" не должно приводить к появлению в области видимости переменных, которые еще не были в области видимости в момент перехода к нему.

Например:

	goto L // BAD
v := 3
L:

является ошибочным, поскольку переход к метке L пропускает создание v.

Оператор "goto" вне блока не может перейти к метке внутри этого блока.

Например:

if n%2 == 1 {
	goto L1
}
for n > 0 {
	f()
	n--
L1:
	f()
	n--
}

является ошибочным, поскольку метка L1 находится внутри блока оператора "for", а goto - нет.

Операторы перехода (fallthrough)

Оператор “fallthrough” передает управление первому оператору следующего выражения case в выражении "switch". Он может использоваться только в качестве последнего непустого оператора в таком выражении.

FallthroughStmt = "fallthrough" .

Операторы defer

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

DeferStmt = «defer» Expression .

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

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

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

lock(l)
defer unlock(l)  // unlocking happens before surrounding function returns

// prints 3 2 1 0 before surrounding function returns
for i := 0; i <= 3; i++ {
	defer fmt.Print(i)
}

// f returns 42
func f() (result int) {
	defer func() {
		// result is accessed after it was set to 6 by the return statement
		result *= 7
	}()
	return 6
}

Built-in функции

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

  • append, cap, close, complex, copy, delete, imag, len, make, new, panic, print, println, real, recover

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

Добавление append к срезам и копирование copy срезов

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

Вариативная функция append добавляет ноль или более значений x к срезу s и возвращает результирующий срез того же типа, что и s. Основной тип s должен быть срезом типа []E. Значения x передаются в параметр типа ...E, и к ним применяются соответствующие правила передачи параметров. В качестве особого случая, если основной тип s - []byte, append также принимает второй аргумент с основным типом bytestring, за которым следует .... Эта форма добавляет байты байтового фрагмента или строки.

append(s S, x ...E) S  // core type of S is []E

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

s0 := []int{0, 0}
s1 := append(s0, 2)                // append a single element     s1 is []int{0, 0, 2}
s2 := append(s1, 3, 5, 7)          // append multiple elements    s2 is []int{0, 0, 2, 3, 5, 7}
s3 := append(s2, s0...)            // append a slice              s3 is []int{0, 0, 2, 3, 5, 7, 0, 0}
s4 := append(s3[3:6], s3[2:]...)   // append overlapping slice    s4 is []int{3, 5, 7, 2, 3, 5, 7, 0, 0}

var t []interface{}
t = append(t, 42, 3.1415, "foo")   //                             t is []interface{}{42, 3.1415, "foo"}

var b []byte
b = append(b, "bar"...)            // append string contents      b is []byte{'b', 'a', 'r' }

Функция copy копирует элементы среза из источника src в пункт назначения dst и возвращает количество скопированных элементов. Типы ядра обоих аргументов должны быть срезами с одинаковым типом элемента. Количество скопированных элементов равно минимальному из len(src) и len(dst). В качестве особого случая, если основной тип места назначения - []byte, copy также принимает аргумент источника с основным типом bytestring. Эта форма копирует байты из байтового фрагмента или строки в байтовый фрагмент.

copy(dst, src []T) int
copy(dst []byte, src string) int

Например:

var a = [...]int{0, 1, 2, 3, 4, 5, 6, 7}
var s = make([]int, 6)
var b = make([]byte, 5)
n1 := copy(s, a[0:])            // n1 == 6, s is []int{0, 1, 2, 3, 4, 5}
n2 := copy(s, s[2:])            // n2 == 4, s is []int{2, 3, 4, 5, 4, 5}
n3 := copy(b, "Hello, World!")  // n3 == 5, b is []byte("Hello")

Clear

Встроенная функция clear принимает аргумент типа map, slice или параметр type и удаляет или обнуляет все элементы [Go 1.21].

Вызов Тип аргумента Результат

clear(m) map[K]T удаляет все элементы, в результате чего получается пустая карта
 (len(m) == 0)

clear(s) []T устанавливает все элементы длиной до
 s в нулевое значение T

clear(t) параметр типа см. ниже

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

Если map или slice равен nil, clear не выполняется.

Close

Для аргумента ch с типом ядра, являющимся каналом, встроенная функция close фиксирует, что по этому каналу больше не будут передаваться значения. Ошибкой будет, если ch является каналом только для приема. Отправка в закрытый канал или его закрытие вызывают панику во время выполнения. Закрытие канала nil также вызывает панику во время выполнения. После вызова close и после получения всех ранее отправленных значений операции приема возвращают нулевое значение для типа канала без блокировки. Многозначная операция receive возвращает полученное значение вместе с указанием того, закрыт ли канал.

Работа с комплексными числами

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

complex(realPart, imaginaryPart floatT) complexT
real(complexT) floatT
imag(complexT) floatT

Тип аргументов и возвращаемого значения соответствуют друг другу. Для complex оба аргумента должны быть одного типа с плавающей точкой, а возвращаемое значение - это комплексный тип с соответствующими составляющими с плавающей точкой: complex64 для аргументов float32 и complex128 для аргументов float64.

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

Для real и imag аргумент должен быть комплексного типа, а возвращаемый тип - соответствующий тип с плавающей точкой: float32 для аргумента complex64 и float64 для аргумента complex128. Если аргумент оценивается в нетипизированную константу, он должен быть числом, а возвращаемое значение функции - нетипизированная константа с плавающей точкой.

Функции real и imag вместе образуют обратную функцию complex, поэтому для значения z комплексного типа Z, z == Z(complex(real(z), imag(z))).

Если все операнды этих функций - константы, то возвращаемое значение - константа.

var a = complex(2, -2) // complex128
const b = complex(1.0, -1.4) // нетипизированная комплексная константа 1 - 1.4i
x := float32(math.Cos(math.Pi/2))  // float32
var c64 = complex(5, -x) // complex64
var s int = complex(1, 0) // нетипизированная комплексная константа 1 + 0i может быть преобразована в int
_ = complex(1, 2<<s) // illegal: 2 предполагает тип с плавающей точкой, нельзя сдвигать
var rl = real(c64) // float32
var im = imag(a) // float64
const c = imag(b) // нетипизированная константа -1.4
_ = imag(3 << s) // illegal: 3 предполагает комплексный тип, сдвиг невозможен

Аргументы типа parameter type недопустимы.

Удаление элементов карты (map)

Встроенная функция delete удаляет элемент с ключом k из карты m. Значение k должно быть присваиваемым типу ключа m.

delete(m, k) // удалить элемент m[k] из карты m

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

Если map m равен nil или элемент m[k] не существует, delete не выполняется.

Длина и емкость

Встроенные функции len и cap принимают аргументы различных типов и возвращают результат типа int. Реализация гарантирует, что результат всегда помещается в int.

Вызов      Тип аргумента    Результат

len(s)    тип string      длина строки в байтах
          [n]T, *[n]T      длина массива (== n)
          []T              длина фрагмента
          map[K]T          длина карты (количество определенных ключей)
          chan T           количество элементов в очереди буфера канала
          тип параметра   см. ниже

cap(s)    [n]T, *[n]T      длина массива (== n)
          []T              емкость фрагмента
          chan T           емкость буфера канала
          тип параметра   см. ниже

Если тип аргумента является типовым параметром P, вызов len(e) (или cap(e) соответственно) должен быть действительным для каждого типа в наборе типов P. Результатом является длина (или емкость, соответственно) аргумента, тип которого соответствует типовому аргументу, с которым был создан экземпляр P.

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

0 <= len(s) <= cap(s)

Длина нулевого фрагмента, карты или канала равна 0. Емкость нулевого фрагмента или канала равна 0.

Выражение len(s) является константой, если s является строковой константой. Выражения len(s) и cap(s) являются константами, если тип s является массивом или указателем на массив, а выражение s не содержит приёмов канала или (неконстантных) вызовов функций; в этом случае s не вычисляется. В противном случае вызовы len и cap не являются константами, и s вычисляется.

const (
	c1 = imag(2i)                    // imag(2i) = 2.0 is a constant
	c2 = len([10]float64{2})         // [10]float64{2} contains no function calls
	c3 = len([10]float64{c1})        // [10]float64{c1} contains no function calls
	c4 = len([10]float64{imag(2i)})  // imag(2i) is a constant and no function call is issued
	c5 = len([10]float64{imag(z)})   // invalid: imag(z) is a (non-constant) function call
)
var z complex128

Make (Создание срезов, карт и каналов)

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

Вызов             Основной тип    Результат

make(T, n)       slice        срез типа T с длиной n и емкостью n
make(T, n, m)    slice        срез типа T с длиной n и емкостью m

make(T)          map          карта типа T
make(T, n)       map          карта типа T с начальным пространством примерно для n элементов

make(T)          channel      небуферизованный канал типа T
make(T, n)       канал      буферизованный канал типа T, размер буфера n

Каждый из аргументов размера n и m должен быть целочисленного типа, иметь набор типов, содержащий только целочисленные типы, или быть константой без типа. Аргумент постоянного размера должен быть неотрицательным и представляться значением типа int; если это константа без типа, ей присваивается тип int. Если указаны оба аргумента n и m и они являются константами, то n не должен быть больше m. Для срезов и каналов, если n является отрицательным или больше m во время выполнения, возникает паника во время выполнения.

s := make([]int, 10, 100)       // срез с len(s) == 10, cap(s) == 100
s := make([]int, 1e3)           // срез с len(s) == cap(s) == 1000
s := make([]int, 1<<63)         // недопустимо: len(s) не может быть представлено значением типа int
s := make([]int, 10, 0)         // недопустимо: len(s) > cap(s)
c := make(chan int, 10)         // канал с размером буфера 10
m := make(map[string]int, 100)  // карта с начальным пространством примерно для 100 элементов

Вызов make с типом карты и подсказкой размера n создаст карту с начальным пространством для хранения n элементов карты. Точное поведение зависит от реализации.

Min и max

Встроенные функции min и max вычисляют соответственно наименьшее или наибольшее значение из фиксированного числа аргументов упорядоченных типов. Должно быть как минимум один аргумент [Go 1.21].

Применяются те же правила типов, что и для операторов: для упорядоченных аргументов x и y min(x , y) является действительным, если x + y является действительным, и тип min(x, y) является типом x + y (аналогично для max). Если все аргументы являются константами, результат является константой.

var x, y int
m := min(x)                 // m == x
m := min(x, y)              // m — меньшее из x и y
m := max(x, y, 10)          // m — большее из x и y, но не меньше 10
c := max(1, 2.0, 10)        // c == 10.0 (тип с плавающей запятой)
f := max(0, float32(x))     // тип f — float32
var s []string
_ = min(s...)               // недопустимо: аргументы slice не допускаются
t := max(«», «foo», bar)  // t == «foo» (тип string)

Для числовых аргументов, предполагая, что все NaN равны, min и max являются коммутативными и ассоциативными:

min(x, y)    == min(y, x)
min(x, y, z) == min(min(x, y), z) == min(x, min(y, z))

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

   x        y    min(x, y)    max(x, y)

  -0.0    0.0         -0.0          0.0    // отрицательный ноль меньше (неотрицательного) нуля
  -Inf      y         -Inf            y    // отрицательная бесконечность меньше любого другого числа
  +Inf      y            y         +Inf    // положительная бесконечность больше любого другого числа
   NaN      y          NaN          NaN    // если любой аргумент является NaN, результат будет NaN

Для строковых аргументов результатом для min будет первый аргумент с наименьшим (или для max — наибольшим) значением, сравниваемым лексически побайтно:

min(x, y)    == if x <= y then x else y
min(x, y, z) == min(min(x, y), z)

New (Выделение памяти)

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

new(T)

Например

type S struct { a int; b float64 }
new(S)

выделяет память для переменной типа S, инициализирует ее (a=0, b=0.0) и возвращает значение типа *S, содержащее адрес этого места.

Обработка панических состояний

Две встроенные функции, panic и recover, помогают в отчетности и обработке панических состояний во время выполнения и определенных программой условий ошибок.

func panic(interface{})
func recover() interface{}

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

panic(42)
panic(«unreachable»)
panic(Error(«cannot parse»))

Функция recover позволяет программе управлять поведением паникующего goroutine. Предположим, что функция G откладывает функцию D, которая вызывает recover, и происходит паника в функции того же goroutine, в котором выполняется G. Когда выполнение отложенных функций достигает D, возвращаемое значение вызова D для recover будет значением, переданным вызову panic. Если D возвращается нормально, не запуская новую панику, последовательность паники останавливается. В этом случае состояние функций, вызванных между G и вызовом panic, отбрасывается, и возобновляется нормальное выполнение. Затем выполняются все функции, отложенные G перед D, и выполнение G завершается возвратом к вызывающему.

Возвращаемое значение recover равно nil, если goroutine не находится в состоянии паники или recover не был вызван непосредственно отложенной функцией. И наоборот, если goroutine находится в состоянии паники и recover был вызван непосредственно отложенной функцией, возвращаемое значение recover гарантированно не будет nil. Чтобы обеспечить это, вызов panic с нулевым значением интерфейса (или нетипизированным nil) вызывает панику во время выполнения.

Функция protect в примере ниже вызывает функцию-аргумент g и защищает вызывающих от паники во время выполнения, вызванной g.

func protect(g func()) {
    defer func() {
        log.Println(«done»)  // Println выполняется нормально, даже если происходит паника
        if x := recover(); x != nil {
            log.Printf(«run time panic: %v», x)
        }
    }()
    log.Println(«start»)
    g()
}

Загрузка

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

Функция   Поведение

print      выводит все аргументы; форматирование аргументов зависит от реализации
println    как print, но выводит пробелы между аргументами и новую строку в конце

Ограничение реализации: print и println не обязательно должны принимать произвольные типы аргументов, но должна поддерживаться печать типов boolean, numeric и string.

Packages (Пакеты)

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

Организация исходных файлов

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

SourceFile = PackageClause «;» { ImportDecl «;» } { TopLevelDecl «;» } .

Пакетное предложение

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

PackageClause = «package» PackageName .
PackageName   = identifier .

PackageName не должен быть пустым идентификатором.

package math

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

Декларации импорта

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

ImportDecl = «import» ( ImportSpec | «(» { ImportSpec «;» } «)» ) .
ImportSpec = [ «.» | PackageName ] ImportPath .
ImportPath = string_lit .

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

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

Ограничение реализации: компилятор может ограничить ImportPaths непустыми строками, использующими только символы, принадлежащие общим категориям Unicode L, M, N, P и S (графические символы без пробелов), а также может исключить символы !"#$%&'()*,:;<=>?[\]^`{\|} и символ замены Unicode U+FFFD.

Рассмотрим скомпилированный пакет, содержащий объявление package math, которая экспортирует функцию Sin, и установим скомпилированный пакет в файл, идентифицируемый как «lib/math». В этой таблице показано, как Sin доступен в файлах, которые импортируют пакет после различных типов деклараций импорта.

Декларация импорта          Локальное имя Sin

import   "lib/math"         math.Sin
import m "lib/math"         m.Sin
import . "lib/math"         Sin

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

import _ "lib/math"

Пример package

Полный пакет Go, который реализует параллельное сито простых чисел.

package main

import «fmt»

// Отправить последовательность 2, 3, 4, … в канал „ch“.
func generate(ch chan<- int) {
    for i := 2; ; i++ {
        ch <- i  // Отправить „i“ в канал „ch“.
    }
}

// Скопировать значения из канала „src“ в канал „dst“,
// удалив те, которые делятся на „prime“.
func filter(src <-chan int, dst chan<- int, prime int) {
    for i := range src {  // Цикл по значениям, полученным из „src“.
        if i%prime != 0 {
            dst <- i  // Отправить „i“ в канал „dst“.
		}
    }
}

// Простое сито: последовательный фильтр обрабатывает вместе.
func sieve() {
    ch := make(chan int)  // Создать новый канал.
    go generate(ch)       // Запустить generate() как подпроцесс.
    for {
        prime := <-ch
        fmt.Print(prime, «\n»)
		ch1 := make(chan int)
        go filter(ch, ch1, prime)
        ch = ch1
    }
}

func main() {
    sieve()
}

Инициализация и выполнение программы

Нулевое значение

Когда для переменной выделяется память, либо через объявление, либо через вызов new, либо когда создается новое значение, либо через составной литерал, либо через вызов make, и явная инициализация не предоставляется, переменной или значению присваивается значение по умолчанию. Каждый элемент такой переменной или значения устанавливается в нулевое значение для своего типа: false для булевых значений, 0 для числовых типов, "" для строк и nil для указателей, функций, интерфейсов, срезов, каналов и карт. Эта инициализация выполняется рекурсивно, поэтому, например, каждый элемент массива структур будет иметь обнуленные поля, если значение не указано.

Эти два простых объявления эквивалентны:

var i int
var i int = 0

После

type T struct { i int; f float64; next *T }
t := new(T)

имеет место следующее:

t.i == 0
t.f == 0.0
t.next == nil

То же самое будет верно и после

var t T

Инициализация пакета

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

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

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

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

var x = a
var a, b = f() // a и b инициализируются вместе, перед инициализацией x

Для целей инициализации пакета пустые переменные обрабатываются как любые другие переменные в объявлениях.

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

Анализ зависимостей не зависит от фактических значений переменных, а только от лексических ссылок на них в исходном коде, анализируемых транзитивно. Например, если выражение инициализации переменной x ссылается на функцию, тело которой ссылается на переменную y, то x зависит от y. В частности:

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

Например, при следующих объявлениях

var (
    a = c + b  // == 9
    b = f()    // == 4
    c = f()    // == 5
	d = 3      // == 5 после завершения инициализации)


func f() int {
    d++
    return d
}

порядок инициализации будет d, b, c, a. Обратите внимание, что порядок подвыражений в выражениях инициализации не имеет значения: a = c + b и a = b + c приводят к одинаковому порядку инициализации в этом примере.

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

Например, при следующих объявлениях

var x = I(T{}).ab()   // x имеет не обнаруженную, скрытую зависимость от a и b
var _ = sideEffect()  // не связан с x, a или b
var a = b
var b = 42

type I interface      { ab() []int }
type T struct{}
func (T) ab() []int   { return []int{a, b} }

переменная a будет инициализирована после b, но не определено, будет ли x инициализирована до b, между b и a или после a, и, следовательно, также не определено, в какой момент будет вызвана sideEffect() (до или после инициализации x).

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

func init() {  }

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

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

Инициализация программы

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

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

Инициализация пакета — инициализация переменных и вызов функций init — происходит в одной goroutine, последовательно, по одному пакету за раз. Функция init может запускать другие goroutines, которые могут выполняться одновременно с кодом инициализации. Однако инициализация всегда последовательно выполняет функции init: она не вызовет следующую, пока предыдущая не вернется.

Выполнение программы

Полная программа создается путем связывания одного неимпортированного пакета, называемого основным пакетом, со всеми пакетами, которые он импортирует, транзитивно.

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

func main() {  }

Выполнение программы начинается с инициализации программы, а затем вызова функции main в пакете main. Когда этот вызов функции возвращается, программа завершается. Она не ждет завершения других (не основных) горутин.

Errors (Ошибки)

Объявленный тип error определяется как

type error interface {
    Error() string
}

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

func Read(f *File, b []byte) (n int, err error)

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

Ошибки выполнения, такие как попытка индексировать массив за пределами границ, вызывают панику во время выполнения, эквивалентную вызову встроенной функции panic со значением типа runtime.Error, определяемого интерфейсом реализации. Этот тип удовлетворяет заранее объявленному интерфейсному типу error. Точные значения ошибок, которые представляют различные условия ошибки во время выполнения, не определены.

package runtime

type Error interface {
error
// и, возможно, другие методы
}

Системные аспекты

Пакет unsafe

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

package unsafe

type ArbitraryType int  // shorthand for an arbitrary Go type; it is not a real type
type Pointer *ArbitraryType

func Alignof(variable ArbitraryType) uintptr
func Offsetof(selector ArbitraryType) uintptr
func Sizeof(variable ArbitraryType) uintptr

type IntegerType int  // shorthand for an integer type; it is not a real type
func Add(ptr Pointer, len IntegerType) Pointer
func Slice(ptr *ArbitraryType, len IntegerType) []ArbitraryType
func SliceData(slice []ArbitraryType) *ArbitraryType
func String(ptr *byte, len IntegerType) string
func StringData(str string) *byte

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

var f float64
bits = *(*uint64)(unsafe.Pointer(&f))

type ptr unsafe.Pointer
bits = *(*uint64)(ptr(&f))

func f[P ~*B, B any](p P) uintptr {
	return uintptr(unsafe.Pointer(p))
}

var p ptr = nil

Функции Alignof и Sizeof принимают выражение x любого типа и возвращают выравнивание или размер, соответственно, гипотетической переменной v, как если бы v была объявлена через var v = x.

Функция Offsetof принимает (возможно, заключенный в скобки) селектор s.f, обозначающий поле f структуры, обозначенной s или *s, и возвращает смещение поля в байтах относительно адреса структуры. Если f является встроенным полем, оно должно быть доступно без косвенных указателей через поля структуры. Для структуры s с полем f:

uintptr(unsafe.Pointer(&s)) + unsafe.Offsetof(s.f) == uintptr(unsafe.Pointer(&s.f))

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

uintptr(unsafe.Pointer(&x)) % unsafe.Alignof(x) == 0

Переменная типа T имеет переменный размер, если T является параметром типа или если это тип массива или структуры, содержащий элементы или поля переменного размера. В противном случае размер является постоянным. Вызовы Alignof, Offsetof и Sizeof являются константами времени компиляции типа uintptr, если их аргументы (или структура s в выражении селектора s.f для Offsetof) являются типами постоянного размера.

Функция Add добавляет len к ptr и возвращает обновленный указатель unsafe.Pointer(uintptr(ptr) + uintptr(len)) [Go 1.17]. Аргумент len должен быть целочисленного типа или нетипизированной константой. Константный аргумент len должен быть представлен значением типа int; если он является нетипизированной константой, ему присваивается тип int. Правила допустимого использования Pointer по-прежнему применяются.

Функция Slice возвращает фрагмент, базовый массив которого начинается с ptr, а длина и емкость — len. Slice(ptr, len) эквивалентно

(*[len]ArbitraryType)(unsafe.Pointer(ptr))[:]

за исключением того, что в особом случае, если ptr равно nil, а len равно нулю, Slice возвращает nil [Go 1.17].

Аргумент len должен быть целочисленного типа или нетипизированной константой. Константа len должна быть неотрицательной и представляться значением типа int; если это нетипизированная константа, ей присваивается тип int. Во время выполнения, если len отрицательно, или если ptr равно nil, а len не равно нулю, возникает паника во время выполнения [Go 1.17].

Функция SliceData возвращает указатель на базовый массив аргумента slice. Если емкость slice cap(slice) не равна нулю, этот указатель равен &slice[:1][0]. Если slice равен nil, результат равен nil. В противном случае это не равный nil указатель на неуказанный адрес памяти [Go 1.20].

Функция String возвращает строковое значение, байты которого начинаются с ptr и длина которых равна len. К аргументам ptr и len применяются те же требования, что и в функции Slice. Если len равно нулю, результатом будет пустая строка "". Поскольку строки Go являются неизменяемыми, байты, переданные в String, не должны изменяться впоследствии [Go 1.20].

Функция StringData возвращает указатель на базовые байты аргумента str. Для пустой строки возвращаемое значение не определено и может быть nil. Поскольку строки Go являются неизменяемыми, байты, возвращаемые StringData, не должны изменяться [Go 1.20].

Гарантии размеров и выравнивания

Для числовых типов гарантируются следующие размеры:

type                                 size in bytes

byte, uint8, int8                     1
uint16, int16                         2
uint32, int32, float32                4
uint64, int64, float64, complex64     8
complex128                           16

Гарантируются следующие минимальные свойства выравнивания:

  1. Для переменной x любого типа: unsafe.Alignof(x) не меньше 1.
  2. Для переменной x типа struct: unsafe.Alignof(x) - наибольшее из всех значений unsafe.Alignof(x.f) для каждого поля f из x, но не менее 1.
  3. Для переменной x типа array: unsafe.Alignof(x) - это то же самое, что и выравнивание переменной типа элемента массива.

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

Приложение

Версии языка

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

Например, возможность использовать префикс 0b для двоичных целочисленных литералов была введена в Go 1.13, на что указывает [Go 1.13] в разделе о целочисленных литералах. Исходный код, содержащий целочисленный литерал, например 0b1011, будет отклонен, если подразумеваемая или требуемая версия языка, используемая компилятором, старше Go 1.13.

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

Go 1.9

  • Декларация псевдонима может использоваться для объявления псевдонима для типа.

Go 1.13

  • Целые литералы могут использовать префиксы 0b, 0B, 0o и 0O для двоичных и восьмеричных литералов соответственно.
  • Шестнадцатеричные литералы с плавающей запятой могут быть написаны с использованием префиксов 0x и 0X.
  • Суффикс i может использоваться с любыми (двоичными, десятичными, шестнадцатеричными) целыми или плавающими литералами, а не только с десятичными литералами.
  • Цифры любого числового литерала могут быть разделены (сгруппированы) с помощью подчеркиваний _.
  • Число сдвига в операции сдвига может быть целым числом со знаком.

Go 1.14

  • Внедрение метода более одного раза через разные встроенные интерфейсы не является ошибкой.

Go 1.17

  • Срез может быть преобразован в указатель массива, если типы элементов среза и массива совпадают, а массив не длиннее среза.
  • Встроенный пакет unsafe включает новые функции Add и Slice.

Go 1.18

В версии 1.18 в язык добавлены полиморфные функции и типы («генерики»). В частности:

  • Набор операторов и знаков препинания включает новый токен ~.
  • В объявлениях функций и типов можно объявлять параметры типов.
  • Типы интерфейсов могут встраивать произвольные типы (а не только имена типов интерфейсов), а также элементы типов union и ~T.
  • Набор предварительно объявленных типов включает новые типы any и comparable.

Go 1.20

  • Срез может быть преобразован в массив, если типы элементов среза и массива совпадают, а массив не длиннее среза.
  • Встроенный пакет unsafe включает новые функции SliceData, String и StringData.
  • Сравнимые типы (такие как обычные интерфейсы) могут удовлетворять сравнимым ограничениям, даже если аргументы типа не являются строго сравнимыми.

Go 1.21

  • Набор предварительно объявленных функций включает новые функции min, max и clear.
  • Типовое выведение использует типы методов интерфейса для выведения. Оно также выводит типовые аргументы для обобщенных функций, назначенных переменным или переданных в качестве аргументов другим (возможно, обобщенным) функциям.

Go 1.22

  • В операторе «for» каждая итерация имеет свой собственный набор переменных итерации, а не использует одни и те же переменные в каждой итерации.
  • Оператор «for» с клаузулой «range» может выполнять итерацию над целыми значениями от нуля до верхнего предела.

Go 1.23

  • Оператор «for» с клаузулой «range» принимает функцию итератора в качестве выражения диапазона.

Go 1.24

  • Объявление псевдонима может объявлять параметры типа.

Правила унификации типов

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

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

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

  • Оба типа идентичны.
  • Оба типа имеют идентичную структуру, и их типы элементов унифицируются точно.
  • Точно один тип является несвязанным типовым параметром с базовым типом, и этот базовый тип унифицируется с другим типом в соответствии с правилами унификации для ≡A (нестрогая унификация на верхнем уровне и точная унификация для типов элементов).

Если оба типа являются связанными параметрами типа, они унифицируются в соответствии с заданными режимами сопоставления, если:

  • Оба параметра типа идентичны.
  • Не более одного из параметров типа имеет известный аргумент типа. В этом случае параметры типа объединяются: оба они обозначают один и тот же аргумент типа. Если ни один из параметров типа еще не имеет известного аргумента типа, будущий аргумент типа, выведенный для одного из параметров типа, одновременно выводится для обоих.
  • Оба типа-параметра имеют известный тип-аргумент, и типы-аргументы унифицируются в соответствии с заданными режимами сопоставления.

Один связанный тип-параметр P и другой тип T унифицируются в соответствии с заданными режимами сопоставления, если:

  • P не имеет известного типа-аргумента. В этом случае T выводится как тип-аргумент для P.
  • P имеет известный тип-аргумент A, A и T унифицируются в соответствии с заданными режимами сопоставления, и выполняется одно из следующих условий:
    • И A, и T являются типами интерфейса: в этом случае, если A и T также являются определёнными типами, они должны быть идентичными. В противном случае, если ни один из них не является определённым типом, они должны иметь одинаковое количество методов (унификация A и T уже установила, что методы совпадают).
    • Ни A, ни T не являются типами интерфейса: в этом случае, если T является определённым типом, T заменяет A в качестве выведенного аргумента типа для P.

Наконец, два типа, которые не являются связанными параметрами типа, объединяются слабо (и в соответствии с режимом сопоставления элементов), если:

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