Спецификация языка программирования Go
Categories:
“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" .
Лексические элементы
Комментарии
Комментарии служат в качестве программной документации. Существует две формы:
- Строчные комментарии начинаются с последовательности символов
//
и останавливаются в конце строки. - Общие комментарии начинаются с последовательности символов
/*
и заканчиваются первой последующей последовательностью символов*/
.
Комментарий не может начинаться внутри руны или строкового литерала, а также внутри комментария. Общий комментарий, не содержащий новых строк, действует как пробел. Любой другой комментарий действует как новая строка.
Токены
Токены составляют словарный запас языка Go. Существует четыре класса:
- идентификаторы (Identifiers),
- ключевые слова, (Keywords)
- операторы и пунктуация,
- литералы.
Пространство пробелов, образованное символами (U+0020)
, горизонтальных табуляций (U+0009)
, возвратов каретки (U+000D)
и новых строк (U+000A)
, игнорируется, за исключением случаев, когда оно разделяет лексемы, которые в противном случае были бы объединены в одну лексему. Кроме того, новая строка или конец файла могут вызвать вставку точки с запятой.
При разбиении вводимого текста на лексемы следующей лексемой является самая длинная последовательность символов, образующая допустимую лексему.
Точки с запятой
Формальный синтаксис использует точки с запятой ";"
в качестве терминаторов в ряде последовательности команд. Программы на Go могут опускать большинство этих точек с запятой, используя следующие два правила:
- Когда ввод разбивается на токены, точка с запятой автоматически вставляется в поток токенов сразу после последнего токена строки, если этот токен
- идентификатор
- целое число, плавающая точка, мнимое число, руна или строковый литерал
- одно из ключевых слов break, continue, fallthrough или return
- один из операторов и пунктуации ++, –, ), ], или }.
- Чтобы сложные операторы занимали одну строку, перед закрывающим оператором “)” или “}” можно опустить точку с запятой.
Чтобы отразить идиоматическое использование, в примерах кода в этом документе точки с запятой опускаются в соответствии с этими правилами.
(Идентификаторы) 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
имеет основной тип, если выполняется одно из следующих условий:
- Существует единственный тип
U
, который является базовым типом всех типов в наборе типовT
; или - набор типов
T
содержит только типы каналов с идентичным типом элементаE
, и все направленные каналы имеют одинаковое направление.
Никакие другие интерфейсы не имеют базового типа.
Основным типом интерфейса, в зависимости от выполняемого условия, является либо:
- тип
U
; либо - тип
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
"), если выполняется одно из следующих условий:
V
иT
идентичны.V
иT
имеют одинаковые базовые типы, но не являются параметрами типа, и хотя бы один изV
илиT
не является именованным типом.V
иT
- канальные типы с идентичными типами элементов,V
- двунаправленный канал, и хотя бы один изV
илиT
не является именованным типом.T
- интерфейсный тип, но не параметр типа, иx
реализуетT
.x
- это объявленный идентификаторnil
, аT
- указатель, функция, фрагмент, карта, канал или интерфейсный тип, но не параметр типа.x
- это нетипизированная константа, представляемая значением типаT
.
Кроме того, если для x
тип V
или T
является параметром типа, x
можно присвоить переменной типа T
, если выполняется одно из следующих условий:
x
- предопределенный идентификаторnil
,T
- параметр типа, иx
может быть присвоен каждому типу в наборе типовT
.V
не является именованным типом,T
- параметр типа, иx
может быть присвоен каждому типу в наборе типовT
.V
- параметр типа,T
- не именованный тип, и значения каждого типа в наборе типовV
могут быть присвоеныT
.
Представимость
Константа x
представима значением типа T
, где T
не является параметром типа, если выполняется одно из следующих условий:
x
находится в множестве значений, определяемыхT
.T
- тип с плавающей точкой, иx
может быть округлена до точностиT
без переполнения. При округлении используются правила округленияIEEE 754
, но при этом отрицательный нольIEEE
упрощается до беззнакового нуля. Обратите внимание, что постоянные значения никогда не приводят к отрицательному нулюIEEE
,NaN
или бесконечности.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 |
Наборы методов
Набор методов типа определяет методы, которые могут быть вызваны на операнде этого типа. Каждый тип имеет (или пустой) набор методов, связанный с ним:
- Набор методов определенного типа
T
состоит из всех методов, объявленных для типа-приемникаT
. - Набор методов указателя на определенный тип
T
(гдеT
не является ни указателем, ни интерфейсом) - это набор всех методов, объявленных с приемником*T
илиT
. - Набор методов интерфейсного типа - это пересечение наборов методов каждого типа в наборе типов интерфейса (результирующий набор методов обычно представляет собой просто набор объявленных методов в интерфейсе).
К структурам (и указателям на структуры), содержащим встроенные поля, применяются дополнительные правила, описанные в разделе о типах struct
. Любой другой тип имеет пустой набор методов.
В наборе методов каждый метод должен иметь уникальное непустое имя метода.
Blocks (Блоки)
Блок - это возможно пустая последовательность деклараций и утверждений, заключенных в соответствующие скобки.
Block = "{" StatementList "}" .
StatementList = { Statement ";" } .
Помимо явных блоков в исходном коде, существуют и неявные блоки:
- Вселенский блок
universe block
включает в себя весь исходный текст Go. - Каждый пакет имеет
package block
блок пакета, содержащий весь исходный текст Go для этого пакета. - Каждый файл имеет
file block
блок файла, содержащий весь исходный текст Go в этом файле. - Каждый оператор “
if
”, “for
” и “switch
” считается находящимся в своем собственном неявном блоке. - Каждый пункт в операторе “
switch
” или “select
” действует как неявный блок.
Блоки вложены друг в друга и влияют на область видимости.
Декларации и области видимости
Декларация связывает непустой идентификатор с константой, типом, параметром типа, переменной, функцией, меткой или пакетом. Каждый идентификатор в программе должен быть объявлен. Ни один идентификатор не может быть объявлен дважды в одном и том же блоке, и ни один идентификатор не может быть объявлен как в блоке файла, так и в блоке пакета.
Пустой идентификатор может использоваться как любой другой идентификатор в объявлении, но он не вводит привязку и поэтому не объявляется. В блоке пакетов идентификатор init
может использоваться только для объявления функции init
, и, как и пустой идентификатор, он не вводит нового связывания.
Declaration = ConstDecl | TypeDecl | VarDecl .
TopLevelDecl = Declaration | FunctionDecl | MethodDecl .
Область видимости объявленного идентификатора - это часть исходного текста, в которой идентификатор обозначает указанную константу, тип, переменную, функцию, метку или пакет.
В Go лексическая область видимости определяется с помощью блоков:
- Область видимости заранее объявленного идентификатора - это блок
universe
(вселенной). - Областью видимости идентификатора, обозначающего константу, тип, переменную или функцию (но не метод), объявленную на верхнем уровне (вне любой функции), является блок
package
. - Областью видимости имени импортируемого пакета является блок файла, содержащий объявление импорта.
- Областью видимости идентификатора, обозначающего приемник метода, параметр функции или переменную результата, является тело функции.
- Область видимости идентификатора, обозначающего параметр типа функции или объявленный приемником метода, начинается после имени функции и заканчивается в конце тела функции.
- Область видимости идентификатора, обозначающего параметр типа, начинается после имени типа и заканчивается в конце
TypeSpec
. - Область видимости идентификатора константы или переменной, объявленной внутри функции, начинается в конце
ConstSpec
илиVarSpec
(ShortVarDecl
для объявлений коротких переменных) и заканчивается в конце самого внутреннего содержащего блока. - Область видимости идентификатора типа, объявленного внутри функции, начинается с идентификатора в
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
Экспортируемые идентификаторы
Идентификатор может быть экспортирован, чтобы разрешить доступ к нему из другого пакета. Идентификатор экспортируется, если:
- первый символ имени идентификатора является заглавной буквой Юникода (категория символов Юникода
Lu
); и - идентификатор объявлен в блоке пакета или является именем поля или метода.
Все остальные идентификаторы не экспортируются.
Уникальность идентификаторов
Если задан набор идентификаторов, то идентификатор называется уникальным, если он отличается от всех остальных в этом наборе. Два идентификатора отличаются друг от друга, если они пишутся по-разному или если они находятся в разных пакетах и не экспортируются. В противном случае они одинаковы.
Объявление констант
Объявление констант связывает список идентификаторов (имена констант) со значениями списка константных выражений. Количество идентификаторов должно быть равно количеству выражений, и 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
, если
T
реализуетC
; или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
действуют следующие правила:
- Ключ должен быть именем поля, объявленного в типе
struct
. - Список элементов, не содержащий ключей, должен содержать элемент для каждого поля
struct
в том порядке, в котором эти поля объявлены. - Если любой элемент имеет ключ, то каждый элемент должен иметь ключ.
- Список элементов, содержащий ключи, не обязательно должен содержать элемент для каждого поля
struct
. Опущенные поля получают нулевое значение для этого поля. - Литерал может не содержать список элементов; такой литерал оценивается в нулевое значение для своего типа.
- Ошибкой является указание элемента для неэкспортируемого поля структуры, принадлежащей другому пакету.
Учитывая сказанное
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
Для литералов массивов и фрагментов действуют следующие правила:
- Каждый элемент имеет связанный с ним целочисленный индекс, отмечающий его положение в массиве.
- Элемент с ключом использует ключ в качестве своего индекса. Ключ должен быть неотрицательной константой, представляемой значением типа
int
; а если он типизирован, то должен быть целочисленного типа. - Элемент без ключа использует индекс предыдущего элемента плюс один. Если первый элемент не имеет ключа, его индекс равен нулю.
При получении адреса составного литерала генерируется указатель на уникальную переменную, инициализированную значением литерала.
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
плюс один.
К селекторам применяются следующие правила:
- Для значения
x
типаT
или*T
, гдеT
не является указателем или интерфейсным типом,x.f
обозначает поле или метод на самой малой глубине вT
, где есть такойf
. Если не существует ровно одногоf
с самой малой глубиной, то выражение селектора является незаконным. - Для значения
x
типаI
, гдеI
- интерфейсный тип,x.f
обозначает фактический метод с именемf
динамического значенияx
. Если в наборе методовI
нет метода с именемf
, то селекторное выражение незаконно. - В качестве исключения, если тип
x
является определенным типом указателя и(*x).f
является допустимым селекторным выражением, обозначающим поле (но не метод),x.f
является сокращением для(*x).f
. - Во всех остальных случаях
x.f
является незаконным. - Если
x
имеет тип указателя и значениеnil
, аx.f
обозначает полеstruct
, присваивание или вычислениеx.f
вызовет панику во время выполнения. - Если
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]. Инстанцирование происходит в два этапа:
Пояснение инстанцирования
Подробнее
В языке программирования Go термин “instantiation” (инстанцирование) относится к процессу создания конкретного экземпляра (реализации) обобщённого типа или функции с подставленными конкретными типами или значениями.
Instantiations в Go (на примере дженериков, появившихся в Go 1.18)
В Go инстанцирование чаще всего связано с дженериками (generics).
-
Инстанцирование обобщённого типа
При объявлении обобщённого типа (с параметрами типа[T any]
) сам по себе тип является лишь шаблоном. Инстанцирование происходит, когда указывается конкретный тип вместо параметраT
.Пример:
type Stack[T any] struct { items []T } // Инстанцирование Stack для типа int var intStack Stack[int] // Stack[T] инстанцируется как Stack[int]
-
Инстанцирование обобщённой функции
Аналогично, обобщённые функции требуют подстановки конкретных типов при вызове, что приводит к созданию специализированной версии функции.Пример:
func PrintSlice[T any](slice []T) { for _, v := range slice { fmt.Println(v) } } // Инстанцирование функции для []string PrintSlice[string]([]string{"hello", "world"}) // Компилятор Go может также вывести тип автоматически: PrintSlice([]int{1, 2, 3}) // Инстанцируется как PrintSlice[int]
Когда происходит инстанцирование?
- Во время компиляции: Go создаёт конкретные реализации обобщённых типов и функций на этапе компиляции.
- Для каждого уникального типа: Например,
Stack[int]
иStack[string]
— это два разных инстанцированных типа.
Важно
- В Go инстанцирование не происходит во время выполнения (в отличие, например, от некоторых реализаций шаблонов в C++).
- Компилятор стремится избегать дублирования кода, где это возможно, используя оптимизации.
Если вы работаете с дженериками в Go, понимание инстанцирования помогает избежать неожиданностей при использовании обобщённых типов и функций.
- Каждый аргумент типа заменяется на соответствующий параметр типа в родовом объявлении. Эта замена происходит во всем объявлении функции или типа, включая сам список параметров типа и любые типы в этом списке.
- После подстановки каждый аргумент типа должен удовлетворять ограничениям (инстанцированным, если необходимо) соответствующего параметра типа. В противном случае инстанцирование не происходит.
Инстанцирование типа приводит к созданию нового негенетического именованного типа; инстанцирование функции приводит к созданию новой негенетической функции.
список параметров | типа аргументы | типа после подстановки |
---|---|---|
[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) - общая функция:
- Каждая пара ((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)) собирается отдельно от уравнений типа.
-
Для присваивания (v = f) родовой функции (f) переменной
v
(неродовой) типа функции: (typeof(v) ≡_A typeof(f)). -
Для оператора
return ..., f, ...,
гдеf
- родовая функция, возвращаемая в качестве результата в (неродовую) переменнуюr
типа функцииresult
: (typeof(r) ≡_A typeof(f)).
Кроме того, каждый параметр типа (P_k) и соответствующее ограничение типа (C_k) дают уравнение типа (P_k ≡_C C_k).
При выводе типов приоритет отдается информации о типе, полученной от типизированных операндов, перед рассмотрением нетипизированных констант. Поэтому вывод выполняется в два этапа:
-
Уравнения типов решаются для параметров связанных типов с помощью унификации типов. Если унификация не удается, то вывод типа не удается.
-
Для каждого связанного параметра типа (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
равен x
(и r = 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)
— нет. - Во всех неконстантных преобразованиях, включающих значения с плавающей запятой или комплексные значения, если тип результата не может представить значение, преобразование выполняется успешно, но значение результата зависит от реализации.
Преобразования в строковый тип и из него
- Преобразование фрагмента байтов в строковый тип дает строку, последовательные байты которой являются элементами фрагмента.
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'}) // "🌍"
- Преобразование фрагмента рун в строковый тип дает строку, которая представляет собой конкатенацию отдельных значений рун, преобразованных в строки.
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" == "🌎"
- Преобразование значения типа
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'}
- Преобразование значения типа
string
в фрагмент типаrunes
дает фрагмент, содержащий отдельные кодовые позицииUnicode
строки. Объем результирующего фрагмента зависит от реализации и может быть больше длины фрагмента.
[]rune(myString("白鵬翔")) // []rune{0x767d, 0x9d6c, 0x7fd4}
[]rune("") // []rune{}
runes("白鵬翔") // []rune{0x767d, 0x9d6c, 0x7fd4}
[]myRune("♫♬") // []myRune{0x266b, 0x266c}
[]myRune(myString("🌐")) // []myRune{0x1f310}
- Наконец, по историческим причинам целочисленное значение может быть преобразовано в строковый тип. При таком преобразовании получается строка, содержащая (возможно, многобайтовое) представление 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 iΘ = 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 .
Завершающие операторы
Завершающий оператор прерывает обычный поток управления в блоке. Следующие операторы являются завершающими:
- Оператор
«return»
или«goto»
. - Вызов встроенной функции
panic
. - Блок, в котором список операторов заканчивается завершающим оператором.
- Оператор
«if»
, в котором:
- присутствует ветвь
«else»
, и - обе ветви являются завершающими операторами.
- Оператор
«for»
, в котором:
- нет операторов
«break»
, относящихся к оператору«for»
, и - отсутствует условие цикла, и
- оператор
«for»
не использует диапазон (range
).
- Оператор
«switch»
, в котором:
- нет операторов
«break»
, относящихся к оператору«switch»
, - есть
default case
, и - списки операторов в каждом случае, включая случай по умолчанию (
default case
), заканчиваются завершающим оператором или, возможно, помеченным оператором«fallthrough»
.
- Оператор
«select»
, в котором:
- нет операторов
«break»
, относящихся к оператору«select»
, и - списки операторов в каждом случае, включая (
default case
), если он присутствует, заканчиваются завершающим оператором.
Маркированный оператор
, обозначает завершающий оператор.
Все остальные операторы не являются завершающими.
Список операторов заканчивается завершающим оператором, если список не пустой и его последний непустой оператор является завершающим.
Пустые операторы
Пустой оператор ничего не делает.
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}
В присваиваниях каждое значение должно быть присваиваемым типу операнда, которому оно присваивается, за исключением следующих особых случаев:
- Любое типизированное значение может быть присвоено пустому идентификатору.
- Если нетипизированная константа присваивается переменной типа
interface
или идентификаторуblank
, константа сначала неявно преобразуется к своему типу по умолчанию. - Если переменной типа
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
- Для массива, указателя на массив или значения среза a значения итерации индекса генерируются в порядке возрастания, начиная с индекса элемента
0
. Если присутствует не более одной переменной итерации, циклrange
генерирует значения итерации от0
доlen(a)-1
и не индексирует сам массив или срез. Для нулевого среза количество итераций равно0
. - Для строкового значения оператор
«range»
итерирует кодовые позиции Unicode в строке, начиная с байтового индекса0
. При последующих итерациях значение индекса будет индексом первого байта последующих кодовых позиций, закодированных в UTF-8, в строке, а второе значение типаrune
будет значением соответствующей кодовой позиции. Если при итерации встречается недопустимая последовательность UTF-8, второе значение будет0xFFFD
, замещающий символ Unicode, и следующая итерация продвинется на один байт в строке. - Порядок итерации по картам не указан и не гарантируется, что он будет одинаковым от одной итерации к другой. Если запись карты, которая еще не была достигнута, удаляется во время итерации, соответствующее значение итерации не будет произведено. Если запись карты создается во время итерации, эта запись может быть произведена во время итерации или может быть пропущена. Выбор может варьироваться для каждой созданной записи и от одной итерации к другой. Если карта равна
nil
, количество итераций равно0
. - Для каналов произведенные значения итерации являются последовательными значениями, отправленными по каналу, пока канал не будет закрыт. Если канал равен
nil
, выражение диапазона блокируется навсегда. - Для целочисленного значения
n
, гдеn
является целочисленным типом или нетипизированной целочисленной константой, значения итерации от0
доn-1
генерируются в порядке возрастания. Еслиn
является целочисленным типом, значения итерации имеют тот же тип. В противном случае типn
определяется так, как если бы он был присвоен переменной итерации. В частности: если переменная итерации уже существует, тип значений итерации является типом переменной итерации, который должен быть целочисленным типом. В противном случае, если переменная итерации объявлена с помощью клаузулы«range»
или отсутствует, тип значений итерации является типом по умолчанию дляn
. Еслиn <= 0
, цикл не выполняет никаких итераций. - Для функции
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»
происходит в несколько этапов:
- Для всех случаев в операторе канал-операнды операций приема и канал и выражения правой части операторов отправки оцениваются ровно один раз, в порядке источника, при входе в оператор
«select»
. Результатом является набор каналов для приема или отправки и соответствующие значения для отправки. Любые побочные эффекты в этой оценке будут происходить независимо от того, какая (если таковая имеется) операция связи выбрана для выполнения. Выражения в левой частиRecvStmt
с коротким объявлением переменной или присваиванием еще не оцениваются. - Если одна или несколько коммуникаций могут быть выполнены, выбирается одна из них с помощью единого псевдослучайного выбора. В противном случае, если есть случай по умолчанию, выбирается этот случай. Если случая по умолчанию нет, оператор
«select»
блокируется до тех пор, пока хотя бы одна из коммуникаций не сможет быть выполнена. - Если выбранный случай не является случаем по умолчанию, выполняется соответствующая операция связи.
- Если выбранный случай является
RecvStmt
с коротким объявлением переменной или присваиванием, выражения в левой части оцениваются, и полученное значение (или значения) присваиваются. - Выполняется список операторов выбранного случая.
Поскольку связь по нулевым каналам никогда не может быть выполнена, оператор 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
}
Существует три способа возврата значений из функции с типом результата:
- Возвращаемое значение или значения могут быть явно перечислены в операторе
"return"
. Каждое выражение должно быть однозначным и присваиваться соответствующему элементу типа результата функции.
func simpleF() int {
return 2
}
func complexF1() (re float64, im float64) {
return -7.0, -4.0
}
- Список выражений в операторе
"return"
может быть одним вызовом многозначной функции. Эффект будет таким, как если бы каждое значение, возвращаемое функцией, присваивалось временной переменной с типом соответствующего значения, а затем следовал оператор"return"
, перечисляющий эти переменные, и в этот момент применялись бы правила предыдущего случая.
func complexF2() (re float64, im float64) {
return complexF1()
}
- Список выражений может быть пустым, если тип результата функции задает имена для параметров результата. Параметры результата действуют как обычные локальные переменные, и функция может присваивать им значения по мере необходимости. Оператор
"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
Гарантируются следующие минимальные свойства выравнивания:
- Для переменной
x
любого типа:unsafe.Alignof(x)
не меньше1
. - Для переменной
x
типаstruct:
unsafe.Alignof(x)
- наибольшее из всех значенийunsafe.Alignof(x.f)
для каждого поляf
изx
, но не менее1
. - Для переменной
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, соответствующие типы методов унифицируются точно, а набор методов одного из интерфейсов является подмножеством набора методов другого интерфейса.
- Только один тип является интерфейсом (но не параметром типа), соответствующие методы двух типов унифицируются в соответствии с режимом сопоставления элементов, а набор методов интерфейса является подмножеством набора методов другого типа.
- Оба типа имеют одинаковую структуру, и их типы элементов унифицируются в соответствии с режимом сопоставления элементов.