Go 编程语言规范

语言版本 go1.24 (2024年12月30日)

引言

这是 Go 编程语言的参考手册。Go1.18 之前的版本(不包含泛型)可以在这里找到。更多信息和其他文档请参见 go.dev

Go 是一种通用编程语言,设计时考虑了系统编程。它是强类型、支持垃圾回收的,并明确支持并发编程。程序由包(package)构建,其特性允许高效地管理依赖。

语法紧凑且易于解析,便于集成开发环境等自动化工具进行分析。

符号表示

语法使用扩展巴科斯-诺尔范式 (EBNF) 的一种变体来指定

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

产生式是由项和以下运算符构成的表达式,按优先级递增排序

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

小写产生式名称用于标识词法(终结符)标记。非终结符使用驼峰命名法(CamelCase)。词法标记用双引号 "" 或反引号 `` 括起来。

形式 a … b 表示从 ab 的字符集合作为备选。水平省略号 在规范的其他地方也用于非正式地表示各种未进一步指定的枚举或代码片段。字符 (不同于三个字符的 ...)不是 Go 语言的标记。

形式为 [Go 1.xx] 的链接表示所描述的语言特性(或其某些方面)是在语言版本 1.xx 中更改或添加的,因此至少需要该语言版本才能构建。详细信息请参阅附录中的链接章节

源代码表示

源代码是使用 UTF-8 编码的 Unicode 文本。文本未进行规范化,因此一个单独的带重音的码位与由重音和字母组合构成的同一字符是不同的;后者被视为两个码位。为简单起见,本文档将使用非限定术语字符来指代源代码中的 Unicode 码位。

每个码位都是独特的;例如,大写和小写字母是不同的字符。

实现限制:为了与其他工具兼容,编译器可能会禁止源代码中的 NUL 字符 (U+0000)。

实现限制:为了与其他工具兼容,如果 UTF-8 编码的字节顺序标记 (U+FEFF) 是源代码中的第一个 Unicode 码位,编译器可能会忽略它。在源代码的任何其他位置,字节顺序标记可能被禁止。

字符

以下术语用于表示特定的 Unicode 字符类别

newline        = /* the Unicode code point U+000A */ .
unicode_char   = /* an arbitrary Unicode code point except newline */ .
unicode_letter = /* a Unicode code point categorized as "Letter" */ .
unicode_digit  = /* a Unicode code point categorized as "Number, decimal digit" */ .

Unicode Standard 8.0 的第 4.5 节“通用类别”中定义了一组字符类别。Go 将任意 Letter 类别(Lu, Ll, Lt, Lm, Lo)中的所有字符视为 Unicode 字母,将 Number 类别(Nd)中的所有字符视为 Unicode 数字。

字母和数字

下划线字符 _ (U+005F) 被视为小写字母。

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

词法元素

注释

注释用于程序文档。有两种形式

  1. 行注释以字符序列 // 开始,并持续到行尾。
  2. 通用注释以字符序列 /* 开始,并在遇到的第一个字符序列 */ 处停止。

注释不能开始于rune字符串字面值内部,也不能开始于注释内部。不包含换行符的通用注释作用如同空格。任何其他注释作用如同换行符。

标记 (Tokens)

标记构成了 Go 语言的词汇表。有四种类别:标识符关键字运算符和标点以及字面值。由空格 (U+0020)、水平制表符 (U+0009)、回车符 (U+000D) 和换行符 (U+000A) 组成的空白符会被忽略,除非它们用于分隔否则会合并成单个标记的标记。此外,换行符或文件末尾可能会触发分号的插入。在将输入分解为标记时,下一个标记是构成有效标记的最长字符序列。

分号

形式语法在多个产生式中使用分号 ";" 作为终止符。Go 程序可以使用以下两条规则省略大多数分号

  1. 当输入分解为标记时,如果在行的最后一个标记后立即遇到以下情况之一,则会自动在标记流中插入一个分号
  2. 为了允许复杂的语句占据单行,可以在闭括号 ")""}" 之前省略分号。

为了反映惯用法,本文档中的代码示例根据这些规则省略了分号。

标识符

标识符命名程序实体,例如变量和类型。标识符是由一个或多个字母和数字组成的序列。标识符的第一个字符必须是字母。

identifier = letter { letter | unicode_digit } .
a
_x9
ThisVariableIsExported
αβ

某些标识符是预声明的

关键字

以下关键字是保留的,不能用作标识符。

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

运算符和标点

以下字符序列代表运算符(包括赋值运算符)和标点 [Go 1.18]

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

整数字面值

整数字面值是表示整型常量的一系列数字。可选的前缀设置非十进制基数:0b0B 表示二进制,00o0O 表示八进制,以及 0x0X 表示十六进制 [Go 1.13]。单个 0 被视为十进制零。在十六进制字面值中,字母 af 以及 AF 分别代表值 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

浮点数字面值

浮点数字面值是浮点常量的十进制或十六进制表示。

十进制浮点数字面值由整数部分(十进制数字)、小数点、小数部分(十进制数字)和指数部分(eE 后跟可选的符号和十进制数字)组成。整数部分或小数部分之一可以省略;小数点或指数部分之一也可以省略。指数值 exp 将尾数(整数和小数部分)按 10exp 进行缩放。

十六进制浮点数字面值由 0x0X 前缀、整数部分(十六进制数字)、基数点、小数部分(十六进制数字)和指数部分(pP 后跟可选的符号和十进制数字)组成。整数部分或小数部分之一可以省略;基数点也可以省略,但指数部分是必需的。(此语法与 IEEE 754-2008 §5.12.3 中给出的语法一致。)指数值 exp 将尾数(整数和小数部分)按 2exp 进行缩放 [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

虚数字面值

虚数字面值表示复数常量的虚部。它由一个整数浮点数字面值后跟小写字母 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

Rune 字面值

rune 字面值表示rune 常量,一个标识 Unicode 码位的整数值。rune 字面值表示为单引号中括起来的一个或多个字符,例如 'x''\n'。在引号内,除了换行符和未转义的单引号之外,任何字符都可以出现。单个引起来的字符表示该字符本身的 Unicode 值,而以反斜杠开头的多字符序列则以各种格式编码值。

最简单的形式表示引号内的单个字符;由于 Go 源代码是 UTF-8 编码的 Unicode 字符,因此多个 UTF-8 编码的字节可能表示一个整数值。例如,字面值 'a' 包含一个字节,表示字面值 a,Unicode U+0061,值为 0x61,而 'ä' 包含两个字节(0xc3 0xa4),表示带分音符的字母 a,U+00E4,值为 0xe4

几种反斜杠转义允许将任意值编码为 ASCII 文本。有四种方式将整数值表示为数字常量:\x 后跟两个十六进制数字;\u 后跟四个十六进制数字;\U 后跟八个十六进制数字;以及普通反斜杠 \ 后跟三个八进制数字。在每种情况下,字面值的值都是由相应基数中的数字表示的值。

尽管这些表示都产生整数,但它们具有不同的有效范围。八进制转义必须表示一个介于 0 到 255 之间的值(包含)。十六进制转义通过构造满足此条件。转义序列 \u\U 表示 Unicode 码位,因此其中某些值是非法的,特别是高于 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 字面值中反斜杠后跟的未识别字符是非法的。

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"。在引号内,除了换行符和未转义的双引号之外,任何字符都可以出现。引号之间的文本构成了字面值的值,反斜杠转义像在rune 字面值中一样被解释(除了 \' 是非法的而 \" 是合法的),并遵循相同的限制。三位八进制 (\nnn) 和两位十六进制 (\xnn) 转义表示结果字符串中的单个字节;所有其他转义表示单个字符的(可能是多字节)UTF-8 编码。因此,在字符串字面值中,\377\xFF 表示值为 0xFF=255 的单个字节,而 ÿ\u00FF\U000000FF\xc3\xbf 表示字符 U+00FF 的 UTF-8 编码的两个字节 0xc3 0xbf

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

如果源代码将一个字符表示为两个码位,例如涉及重音和字母的组合形式,则将其放在 rune 字面值中会出错(它不是单个码位),而如果放在字符串字面值中,它将显示为两个码位。

常量

布尔常量rune 常量整数常量浮点常量复数常量字符串常量。rune、整数、浮点数和复数常量统称为数值常量

常量值由rune整数浮点数虚数字符串字面值、表示常量的标识符、常量表达式、结果为常量的转换,或应用于常量参数的某些内置函数(如 minmax)、应用于某些值unsafe.Sizeof、应用于某些表达式caplen、应用于复数常量的 realimag 以及应用于数值常量的 complex 的结果值表示。布尔真值由预声明常量 truefalse 表示。预声明标识符 iota 表示一个整数常量。

一般来说,复数常量是常量表达式的一种形式,并在该章节中讨论。

数值常量表示任意精度的精确值,并且不会溢出。因此,没有常量表示 IEEE 754 的负零、无穷大和非数字值。

常量可以是有类型的无类型的。字面值常量、truefalseiota 以及仅包含无类型常量操作数的某些常量表达式是无类型的。

常量可以通过常量声明转换显式地赋予类型,或在变量声明赋值语句中使用或作为表达式中的操作数时隐式地赋予类型。如果常量值不能表示为相应类型的值,则会出错。如果类型是类型参数,则常量会被转换为类型参数的非常量值。

无类型常量具有一个默认类型,这是在需要有类型值的上下文(例如,在没有显式类型的短变量声明 i := 0 中)常量隐式转换为的类型。无类型常量的默认类型分别是 boolruneintfloat64complex128string,具体取决于它是布尔、rune、整数、浮点、复数还是字符串常量。

实现限制:尽管语言中的数值常量具有任意精度,编译器可以使用有限精度的内部表示来实现它们。尽管如此,每个实现都必须

  • 表示至少 256 位的整数常量。
  • 表示浮点常量(包括复数常量的各部分),其尾数至少为 256 位,带符号二进制指数至少为 16 位。
  • 如果无法精确表示整数常量,则给出错误。
  • 如果由于溢出而无法表示浮点或复数常量,则给出错误。
  • 如果由于精度限制而无法表示浮点或复数常量,则四舍五入到最接近的可表示常量。

这些要求既适用于字面值常量,也适用于评估常量表达式的结果。

变量

变量是用于存储的存储位置。允许值的集合由变量的类型决定。

变量声明或函数参数和结果的函数声明函数字面值的签名会为命名变量保留存储空间。调用内置函数 new 或获取复合字面值的地址会在运行时为变量分配存储空间。此类匿名变量通过(可能是隐式的)指针间接寻址来引用。

数组切片结构体类型的结构化变量具有可以单独寻址的元素和字段。每个此类元素都像一个变量。

变量的静态类型(或简称类型)是在其声明中给出的类型,在 new 调用或复合字面值中提供的类型,或结构化变量元素的类型。接口类型的变量还有一个不同的动态类型,这是在运行时赋给变量的值的(非接口)类型(除非该值是预声明标识符 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 .

该语言预声明了某些类型名称。其他类型则通过类型声明类型参数列表引入。复合类型——数组、结构体、指针、函数、接口、切片、映射(map)和通道类型——可以使用类型字面值构造。

预声明类型、定义类型和类型参数被称为命名类型。如果别名声明中给定的类型是命名类型,则别名表示命名类型。

布尔类型

布尔类型表示由预声明常量 truefalse 表示的布尔真值集合。预声明的布尔类型是 bool;它是一个定义类型

数值类型

整数浮点复数类型分别表示整数、浮点数或复数值的集合。它们统称为数值类型。预声明的架构无关的数值类型是

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     either 32 or 64 bits
int      same size as uint
uintptr  an unsigned integer large enough to store the uninterpreted bits of a pointer value

为了避免可移植性问题,所有数值类型都是定义类型,因此是不同的,除了 byteuint8别名,以及 runeint32 的别名。当在表达式或赋值中混合使用不同的数值类型时,需要显式转换。例如,int32int 不是同一类型,即使它们在特定架构上可能具有相同的大小。

字符串类型

字符串类型表示字符串值的集合。字符串值是(可能为空的)字节序列。字节数称为字符串的长度,且永不为负。字符串是不可变的:一旦创建,就无法更改字符串的内容。预声明的字符串类型是 string;它是一个定义类型

字符串 s 的长度可以使用内置函数 len 获取。如果字符串是常量,则长度是编译时常量。可以通过整数索引 0 到 len(s)-1 访问字符串的字节。获取此类元素的地址是非法的;如果 s[i] 是字符串的第 i 个字节,则 &s[i] 无效。

数组类型

数组是由单一类型元素组成的带编号序列,该类型称为元素类型。元素数量称为数组的长度,且永不为负。

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 的元素,或包含 T 作为其组件的类型。

// invalid array types
type (
	T1 [10]T1                 // element type of T1 is T1
	T2 [10]struct{ f T2 }     // T2 contains T2 as component of a struct
	T3 [10]T4                 // T3 contains T3 as component of a struct in T4
	T4 struct{ f T3 }         // T4 contains T4 as component of array T3 in a struct
)

// valid array types
type (
	T5 [10]*T5                // T5 contains T5 as component of a pointer
	T6 [10]func() T6          // T6 contains T6 as component of a function type
	T7 [10]struct{ f []T7 }   // T7 contains T7 as component of a slice in a struct
)

切片类型

切片是对底层数组连续段的描述符,并提供对该数组中带编号元素序列的访问。切片类型表示其元素类型的数组的所有切片集合。元素数量称为切片的长度,且永不为负。未初始化的切片值为 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]

与数组类似,切片总是一维的,但可以组合构造更高维度的对象。对于数组的数组,内部数组按照构造总是长度相同;然而对于切片的切片(或数组的切片),内部长度可能会动态变化。此外,内部切片必须单独初始化。

结构体类型

结构体是由命名元素(称为字段)组成的序列,每个字段都有名称和类型。字段名称可以显式指定 (IdentifierList),也可以隐式指定 (EmbeddedField)。在结构体中,非空白字段名称必须唯一

StructType    = "struct" "{" { FieldDecl ";" } "}" .
FieldDecl     = (IdentifierList Type | EmbeddedField) [ Tag ] .
EmbeddedField = [ "*" ] TypeName [ TypeArgs ] .
Tag           = string_lit .
// An empty struct.
struct {}

// A struct with 6 fields.
struct {
	x, y int
	u float32
	_ float32  // padding
	A *[]int
	F func()
}

使用类型声明但没有显式字段名称的字段称为嵌入字段。嵌入字段必须指定为类型名称 T 或指向非接口类型名称 *T 的指针,并且 T 本身不能是指针类型或类型参数。非限定类型名称充当字段名称。

// A struct with four embedded fields of types T1, *T2, P.T3 and *P.T4
struct {
	T1        // field name is T1
	*T2       // field name is T2
	P.T3      // field name is T3
	*P.T4     // field name is T4
	x, y int  // field names are x and y
}

以下声明是非法的,因为在结构体类型中字段名称必须唯一

struct {
	T     // conflicts with embedded field *T and *P.T
	*T    // conflicts with embedded field T and *P.T
	*P.T  // conflicts with embedded field T and *T
}

结构体 x 中嵌入字段的字段或方法 f,如果 x.f 是一个表示该字段或方法 f 的合法选择器,则称其为被提升的(promoted)

被提升的字段作用如同结构体的普通字段,只是它们不能用作结构体复合字面值中的字段名称。

给定结构体类型 S 和类型名称 T,被提升的方法按如下方式包含在结构体的方法集中

  • 如果 S 包含嵌入字段 T,则 S*S方法集都包含接收者为 T 的被提升的方法。*S 的方法集还包含接收者为 *T 的被提升的方法。
  • 如果 S 包含嵌入字段 *T,则 S*S 的方法集都包含接收者为 T*T 的被提升的方法。

字段声明可以跟随一个可选的字符串字面值 tag(标签),它会成为相应字段声明中所有字段的属性。空的标签字符串等同于没有标签。标签通过 反射接口 可见,并参与结构体的 类型标识,否则会被忽略。

struct {
	x, y float64 ""  // an empty tag string is like an absent tag
	name string  "any string is permitted as a tag"
	_    [4]byte "ceci n'est pas un champ de structure"
}

// A struct corresponding to a TimeStamp protocol buffer.
// The tag strings define the protocol buffer field numbers;
// they follow the convention outlined by the reflect package.
struct {
	microsec  uint64 `protobuf:"1"`
	serverIP6 uint64 `protobuf:"2"`
}

结构体类型 T 不能包含类型为 T 的字段,也不能直接或间接包含包含 T 作为组件的类型,如果这些包含类型仅为数组或结构体类型。

// invalid struct types
type (
	T1 struct{ T1 }            // T1 contains a field of T1
	T2 struct{ f [10]T2 }      // T2 contains T2 as component of an array
	T3 struct{ T4 }            // T3 contains T3 as component of an array in struct T4
	T4 struct{ f [10]T3 }      // T4 contains T4 as component of struct T3 in an array
)

// valid struct types
type (
	T5 struct{ f *T5 }         // T5 contains T5 as component of a pointer
	T6 struct{ f func() T6 }   // T6 contains T6 as component of a function type
	T7 struct{ f [10][]T7 }    // T7 contains T7 as component of a slice in an array
)

指针类型

指针类型表示所有指向给定类型的 变量 的指针集合,该给定类型称为指针的 基类型。未初始化指针的 nil

PointerType = "*" BaseType .
BaseType    = Type .
*Point
*[4]int

函数类型

函数类型表示具有相同参数和结果类型的所有函数的集合。未初始化函数类型变量的 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)

接口类型

接口类型定义一个 类型集合。接口类型的变量可以存储接口类型集合中的任何类型的值。此类类型称为 实现该接口。未初始化接口类型变量的 nil

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

接口类型由一个 接口元素 列表指定。接口元素可以是 方法类型元素,其中类型元素是一个或多个 类型项 的联合。类型项可以是单个类型或单个底层类型。

基本接口

在最基本的形式中,接口指定一个(可能为空的)方法列表。由此类接口定义的类型集合是实现所有这些方法的类型集合,并且相应的 方法集 正好包含接口指定的方法。其类型集合可以完全由方法列表定义的接口称为 基本接口

// A simple File interface.
interface {
	Read([]byte) (int, error)
	Write([]byte) (int, error)
	Close() error
}

每个显式指定的方法名称必须是 唯一 且非 空白 的。

interface {
	String() string
	String() string  // illegal: String not unique
	_(x int)         // illegal: method must have non-blank name
}

多个类型可以实现一个接口。例如,如果两个类型 S1S2 具有方法集

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 代表 S1S2)那么 File 接口由 S1S2 都实现,而无论 S1S2 可能拥有或共享哪些其他方法。

属于接口类型集合成员的每个类型都实现了该接口。任何给定类型都可以实现几个不同的接口。例如,所有类型都实现了 空接口,它代表所有(非接口)类型的集合

interface{}

为了方便,预声明类型 any 是空接口的别名。 [Go 1.18]

类似地,考虑以下接口规范,它出现在 类型声明 中以定义一个名为 Locker 的接口

type Locker interface {
	Lock()
	Unlock()
}

如果 S1S2 也实现

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

它们就既实现了 Locker 接口,也实现了 File 接口。

嵌入接口

在稍微更通用的形式中,接口 T 可以使用一个(可能带限定符的)接口类型名称 E 作为接口元素。这称为在 T嵌入 接口 E [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's methods are Read, Write, and 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()  // illegal: signatures of Reader.Close and Close are different
}

通用接口

在最通用的形式中,接口元素也可以是任意类型项 T,或者是指定底层类型 T~T 形式的项,或者是形如 t1|t2|…|tn 的项的联合 [Go 1.18]。这些元素与方法规范一起,可以如下精确地定义接口的类型集合:

  • 空接口的类型集合是所有非接口类型的集合。
  • 非空接口的类型集合是其接口元素的类型集合的交集。
  • 方法规范的类型集合是所有非接口类型的集合,这些类型的方法集包含该方法。
  • 非接口类型项的类型集合是仅包含该类型的集合。
  • ~T 形式项的类型集合是所有底层类型为 T 的类型集合。
  • t1|t2|…|tn 形式的 联合 的类型集合是这些项的类型集合的联合。

“所有非接口类型的集合”这一量化并非仅指当前程序中声明的所有(非接口)类型,而是指所有可能程序中的所有可能类型,因此是无限的。类似地,给定实现特定方法的所有非接口类型的集合,这些类型的方法集的交集将恰好包含该方法,即使当前程序中的所有类型总是将该方法与另一个方法配对。

通过构造,接口的类型集合永远不会包含接口类型。

// An interface representing only the type int.
interface {
	int
}

// An interface representing all types with underlying type int.
interface {
	~int
}

// An interface representing all types with underlying type int that implement the String method.
interface {
	~int
	String() string
}

// An interface representing an empty type set: there is no type that is both an int and a string.
interface {
	int
	string
}

~T 形式的项中,T 的底层类型必须是它本身,并且 T 不能是接口。

type MyInt int

interface {
	~[]byte  // the underlying type of []byte is itself
	~MyInt   // illegal: the underlying type of MyInt is not MyInt
	~error   // illegal: error is an interface
}

联合元素表示类型集合的联合

// The Float interface represents all floating-point types
// (including any named types whose underlying types are
// either float32 or float64).
type Float interface {
	~float32 | ~float64
}

T~T 形式的项中的类型 T 不能是 类型参数,并且所有非接口项的类型集合必须是成对不相交的(类型集合的成对交集必须为空)。给定类型参数 P

interface {
	P                // illegal: P is a type parameter
	int | ~P         // illegal: P is a type parameter
	~int | MyInt     // illegal: the type sets for ~int and MyInt are not disjoint (~int includes MyInt)
	float32 | Float  // overlapping type sets but Float is an interface
}

实现限制:联合(包含多个项)不能包含 预声明标识符 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、包含 T 或嵌入 T 的类型元素。

// illegal: Bad may not embed itself
type Bad interface {
	Bad
}

// illegal: Bad1 may not embed itself using Bad2
type Bad1 interface {
	Bad2
}
type Bad2 interface {
	Bad1
}

// illegal: Bad3 may not embed a union containing Bad3
type Bad3 interface {
	~int | ~string | Bad3
}

// illegal: Bad4 may not embed an array containing Bad4 as element type
type Bad4 interface {
	[10]Bad4
}

实现接口

类型 T 实现接口 I 如果满足以下任一条件:

  • T 不是接口,并且是 I 的类型集合中的一个元素;或者
  • T 是接口,并且 T 的类型集合是 I 的类型集合的子集。

类型为 T 的值如果 T 实现该接口,则该值实现该接口。

Map 类型

Map 是一种无序的元素集合,元素类型相同,由另一类型的唯一 索引,该类型称为键类型。未初始化 map 的 nil

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

键类型的操作数必须完全定义 比较运算符 ==!=;因此键类型不能是函数、map 或切片。如果键类型是接口类型,这些比较运算符必须定义在动态键值上;否则会导致 运行时 panic

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

map 元素的数量称为其长度。对于 map m,可以使用内建函数 len 获取其长度,并且在执行期间可能会改变。可以通过 赋值 在执行期间添加元素,并通过 索引表达式 检索元素;可以使用内建函数 deleteclear 删除元素。

新的空 map 值使用内建函数 make 创建,该函数接受 map 类型和可选的容量提示作为参数

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

初始容量不限制其大小:map 会增长以容纳存储的项目数量,但 nil map 除外。nil map 等效于空 map,只是不能添加任何元素。

Channel 类型

Channel 提供了一种机制,供 并发执行的函数 通过 发送接收 指定元素类型的值进行通信。未初始化 channel 的 nil

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

可选的 <- 运算符指定 channel 的 方向发送接收。如果指定了方向,则 channel 是 定向的,否则是 双向的。Channel 可以通过 赋值 或显式 转换 限制为仅发送或仅接收。

chan T          // can be used to send and receive values of type T
chan<- float64  // can only be used to send float64s
<-chan int      // can only be used to receive ints

<- 运算符与最左侧的 chan 关联

chan<- chan int    // same as chan<- (chan int)
chan<- <-chan int  // same as chan<- (<-chan int)
<-chan <-chan int  // same as <-chan (<-chan int)
chan (<-chan int)

新的、初始化的 channel 值可以使用内建函数 make 创建,该函数接受 channel 类型和可选的 容量 作为参数

make(chan int, 100)

容量(以元素数量计)设置了 channel 中缓冲区的大小。如果容量为零或省略,则 channel 是无缓冲的,通信只有在发送方和接收方都准备好时才能成功。否则,channel 是有缓冲的,如果缓冲区未满(发送)或非空(接收),通信即可成功而不阻塞。nil channel 从未准备好通信。

Channel 可以通过内建函数 close 关闭。接收运算符 的多值赋值形式报告接收到的值是否在 channel 关闭之前发送。

任意数量的 goroutine 可以在不进行额外同步的情况下使用单个 channel 进行 发送操作接收操作 以及调用内建函数 caplen。Channel 作为先进先出队列运行。例如,如果一个 goroutine 在 channel 上发送值,而另一个 goroutine 接收它们,则值按发送顺序接收。

类型和值的属性

值的表示

预声明类型(参见下面的接口 anyerror)、数组和结构体的值是自包含的:每个这样的值包含其所有数据的完整副本,并且这些类型的 变量 存储整个值。例如,数组变量提供了数组所有元素的存储空间(变量)。相应的 零值 是值类型的特定零值;它们永远不是 nil

nil 指针、函数、切片、map 和 channel 值包含对底层数据的引用,这些底层数据可能由多个值共享

  • 指针值是对持有指针基类型值的变量的引用。
  • 函数值包含对(可能是 匿名的)函数和封闭变量的引用。
  • 切片值包含切片长度、容量以及对其 底层数组 的引用。
  • map 或 channel 值是对 map 或 channel 的具体实现的数据结构的引用。

接口值可能是自包含的,也可能包含对底层数据的引用,这取决于接口的 动态类型。预声明标识符 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) { … }

stringA1A2B1B2 的底层类型都是 string[]B1B3B4 的底层类型是 []B1P 的底层类型是 interface{}

核心类型

每个非接口类型 T 都有一个 核心类型,其与 T底层类型 相同。

接口 T 具有核心类型,如果满足以下任一条件:

  1. 存在一个单一类型 U,它是 T类型集合 中所有类型的 底层类型;或者
  2. T 的类型集合仅包含元素类型 E 相同的 channel 类型,并且所有定向 channel 具有相同的方向。

没有其他接口具有核心类型。

接口的核心类型,根据满足的条件,可以是

  1. 类型 U;或者
  2. 如果 T 仅包含双向 channel,则核心类型是 chan E;如果包含定向 channel,则根据定向 channel 的方向是 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{}                               // no single underlying type
interface{ Celsius|float64 }              // no single underlying type
interface{ chan int | chan<- string }     // channels have different element types
interface{ <-chan int | chan<- int }      // directional channels have different directions

某些操作(切片表达式appendcopy)依赖于一种稍微更宽松的核心类型形式,它接受字节切片和字符串。具体来说,如果接口 T 的类型集合中所有类型的底层类型恰好只有两种:[]bytestring,则 T 的核心类型称为 bytestring

具有 bytestring 核心类型的接口示例

interface{ int }                          // int (same as ordinary core type)
interface{ []byte | string }              // bytestring
interface{ ~[]byte | myString }           // bytestring

注意 bytestring 不是一个真实的类型;它不能用于声明变量或构成其他类型。它仅用于描述某些操作的行为,这些操作从字节序列(可能是字节切片或字符串)读取数据。

类型标识

两个类型要么是 相同 的,要么是 不同 的。

命名类型 总是与任何其他类型不同。否则,如果两个类型其 底层 类型字面值在结构上等价,则它们是相同的;也就是说,它们具有相同的字面值结构,并且对应的组件具有相同的类型。详细来说:

  • 两个数组类型如果具有相同的元素类型和相同的数组长度,则它们是相同的。
  • 两个切片类型如果具有相同的元素类型,则它们是相同的。
  • 两个结构体类型如果具有相同的字段序列,并且对应的字段对具有相同的名称、相同的类型和相同的标签,并且要么都嵌入,要么都不嵌入,则它们是相同的。非导出 的字段名称来自不同包时总是不同的。
  • 两个指针类型如果具有相同的基类型,则它们是相同的。
  • 两个函数类型如果具有相同数量的参数和结果值,对应的参数和结果类型相同,并且要么两个函数都是可变参数函数,要么都不是,则它们是相同的。参数和结果名称无需匹配。
  • 两个接口类型如果定义相同的类型集合,则它们是相同的。
  • 两个 map 类型如果具有相同的键和元素类型,则它们是相同的。
  • 两个 channel 类型如果具有相同的元素类型和相同的方向,则它们是相同的。
  • 两个 实例化 类型如果它们的定义类型和所有类型实参相同,则它们是相同的。

给定声明

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

B0B1 是不同的,因为它们是由不同的 类型定义 创建的新类型;func(int, float64) *B0func(x int, y float64) *[]string 是不同的,因为 B0[]string 不同;P1P2 是不同的,因为它们是不同的类型参数。D0[int, string]struct{ x int; y string } 是不同的,因为前者是一个 实例化 的定义类型,而后者是一个类型字面值(但它们仍然 可赋值)。

可赋值性

类型为 V 的值 x 是可赋值给类型为 T变量(“x 可赋值给 T”)如果满足以下任一条件:

  • VT 相同。
  • VT 具有相同的 底层类型,但不是类型参数,并且 VT 中至少有一个不是 命名类型
  • VT 是具有相同元素类型的 channel 类型,V 是一个双向 channel,并且 VT 中至少有一个不是 命名类型
  • T 是接口类型,但不是类型参数,并且 x 实现 T
  • x 是预声明标识符 nil,并且 T 是指针、函数、切片、map、channel 或接口类型,但不是类型参数。
  • x 是一个无类型的 常量,它 可表示T 类型的值。

此外,如果 x 的类型 VT 是类型参数,则 x 可赋值给类型为 T 的变量,如果满足以下任一条件:

  • x 是预声明标识符 nilT 是类型参数,并且 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 的分量类型(float32float64)的值。

如果 T 是类型参数,则 x 可表示为类型为 T 的值,如果 x 可表示为 T 的类型集合中的每种类型的值。

x                   T           x is representable by a value of T because

'a'                 byte        97 is in the set of byte values
97                  rune        rune is an alias for int32, and 97 is in the set of 32-bit integers
"foo"               string      "foo" is in the set of string values
1024                int16       1024 is in the set of 16-bit integers
42.0                byte        42 is in the set of unsigned 8-bit integers
1e10                uint64      10000000000 is in the set of unsigned 64-bit integers
2.718281828459045   float32     2.718281828459045 rounds to 2.7182817 which is in the set of float32 values
-1e-1000            float64     -1e-1000 rounds to IEEE -0.0 which is further simplified to 0.0
0i                  int         0 is an integer value
(42 + 0i)           float32     42.0 (with zero imaginary part) is in the set of float32 values
x                   T           x is not representable by a value of T because

0                   bool        0 is not in the set of boolean values
'a'                 string      'a' is a rune, it is not in the set of string values
1024                byte        1024 is not in the set of unsigned 8-bit integers
-1                  uint16      -1 is not in the set of unsigned 16-bit integers
1.1                 int         1.1 is not an integer value
42i                 float32     (0 + 42i) is not in the set of float32 values
1e1000              float64     1e1000 overflows to IEEE +Inf after rounding

方法集

类型的 方法集 决定了可以对该类型的 操作数 调用 的方法。每个类型都有一个(可能为空的)与之关联的方法集:

  • 定义类型 T 的方法集包含所有使用接收者类型 T 声明的 方法
  • 指向定义类型 T 的指针(其中 T 既不是指针也不是接口)的方法集是所有使用接收者 *TT 声明的方法的集合。
  • 接口类型 的方法集是接口 类型集合 中每种类型的方法集的交集(最终的方法集通常就是接口中声明的方法集合)。

含有嵌入字段的结构体(及指向结构体的指针)还有进一步的规则,如 结构体类型 部分所述。任何其他类型都有一个空的方法集。

在一个方法集中,每个方法都必须有一个 唯一 的非 空白 方法名称

是匹配花括号内可能为空的声明和语句序列。

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

除了源代码中的显式块之外,还有隐式块:

  1. 全局块 包含所有 Go 源代码文本。
  2. 每个 都有一个 包块,包含该包的所有 Go 源代码文本。
  3. 每个文件都有一个 文件块,包含该文件中的所有 Go 源代码文本。
  4. 每个 "if""for""switch" 语句都被视为位于其自身的隐式块中。
  5. "switch""select" 语句中的每个子句都充当一个隐式块。

块嵌套并影响 作用域

声明和作用域

声明 将非 空白 标识符绑定到 常量类型类型参数变量函数标签。程序中的每个标识符都必须声明。在同一个块中不能两次声明同一个标识符,也不能在文件块和包块中同时声明同一个标识符。

空白标识符 可以像声明中的任何其他标识符一样使用,但它不引入绑定,因此未声明。在包块中,标识符 init 只能用于 init 函数 声明,并且与空白标识符一样,它不引入新的绑定。

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

已声明标识符的 作用域 是源代码文本中标识符表示指定常量、类型、变量、函数、标签或包的范围。

Go 使用 进行词法作用域划分:

  1. 预声明标识符 的作用域是全局块。
  2. 在顶层(任何函数之外)声明的表示常量、类型、变量或函数(但不是方法)的标识符的作用域是包块。
  3. 导入包的包名的作用域是包含 import 声明的文件的文件块。
  4. 表示方法接收者、函数参数或结果变量的标识符的作用域是函数体。
  5. 表示函数类型参数或由方法接收者声明的类型参数的标识符的作用域在函数名之后开始,并在函数体结束时结束。
  6. 表示类型类型参数的标识符的作用域在类型名之后开始,并在 TypeSpec 结束时结束。
  7. 在函数内部声明的常量或变量标识符的作用域在 ConstSpec 或 VarSpec(对于短变量声明是 ShortVarDecl)结束时开始,并在最内层包含块结束时结束。
  8. 在函数内部声明的类型标识符的作用域在 TypeSpec 中的标识符处开始,并在最内层包含块结束时结束。

在块中声明的标识符可以在内部块中重新声明。当内部声明的标识符在作用域内时,它表示由内部声明定义的实体。

包子句 不是声明;包名不出现在任何作用域中。其目的是标识属于同一 的文件,并为 import 声明指定默认包名。

标签作用域

标签由 标签语句 声明,并用于 "break""continue""goto" 语句中。声明一个从未使用过的标签是非法的。与其它标识符不同,标签不具有块作用域,也不与非标签标识符冲突。标签的作用域是声明它的函数体,不包括任何嵌套函数的函数体。

空白标识符

空白标识符 由下划线字符 _ 表示。它用作匿名占位符,替代常规(非空白)标识符,并在 声明、作为 操作数 以及在 赋值语句 中具有特殊含义。

预声明标识符

以下标识符在 全局块 中隐式声明 [Go 1.18] [Go 1.21]

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

Constants:
	true false iota

Zero value:
	nil

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

导出标识符

标识符可以被 导出,以便允许从另一个包访问它。标识符被导出,如果同时满足:

  1. 标识符名称的第一个字符是 Unicode 大写字母(Unicode 字符类别 Lu);并且
  2. 标识符在 包块 中声明,或者它是 字段名方法名

所有其他标识符都不导出。

标识符的唯一性

给定一组标识符,如果一个标识符与该集合中所有其他标识符都 不同,则称该标识符是 唯一 的。如果两个标识符拼写不同,或者它们出现在不同的 中且未 导出,则它们是不同的。否则,它们是相同的。

常量声明

常量声明将一个标识符列表(常量的名称)绑定到 常量表达式 列表的值。标识符的数量必须等于表达式的数量,左边的第 n 个标识符绑定到右边第 n 个表达式的值。

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

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

如果类型存在,所有常量都采用指定的类型,并且表达式必须 可赋值 给该类型,该类型不能是类型参数。如果省略类型,常量采用相应表达式的单独类型。如果表达式的值是无类型的 常量,声明的常量保持无类型,常量标识符表示常量值。例如,如果表达式是浮点字面值,即使字面值的小数部分为零,常量标识符也表示一个浮点常量。

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

在括号括起来的 const 声明列表中,除了第一个 ConstSpec 外,表达式列表可以省略。这样的空列表等效于文本替换前面的第一个非空表达式列表及其类型(如果有)。因此,省略表达式列表等同于重复上一个列表。标识符的数量必须等于上一个列表中的表达式数量。与 iota 常量生成器 一起,此机制允许轻量级声明序列值

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

Iota

常量声明 中,预声明标识符 iota 代表连续的无类型整型 常量。它的值是该常量声明中对应 ConstSpec 的索引,从零开始。它可用于构造一组相关常量

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

根据定义,同一 ConstSpec 中多次使用 iota 都具有相同的值

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 and []*Node are identical types
	Polar    = polar    // Polar and polar denote identical types
)

如果别名声明指定了 类型参数 [Go 1.24],则类型名称表示一个 泛型别名。泛型别名在使用时必须 实例化

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

在别名声明中,给定的类型不能是类型参数。

type A[P any] = P    // illegal: P is a type parameter

类型定义

类型定义创建一个新的、不同的类型,它与给定类型具有相同的 底层类型 和操作,并将一个标识符(类型名)绑定到它。

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)
}

定义类型可以有与之关联的 方法。它不继承绑定到给定类型的任何方法,但接口类型或复合类型元素的方法集保持不变

// A Mutex is a data type with two methods, Lock and Unlock.
type Mutex struct         { /* Mutex fields */ }
func (m *Mutex) Lock()    { /* Lock implementation */ }
func (m *Mutex) Unlock()  { /* Unlock implementation */ }

// NewMutex has the same composition as Mutex but its method set is empty.
type NewMutex Mutex

// The method set of PtrMutex's underlying type *Mutex remains unchanged,
// but the method set of PtrMutex is empty.
type PtrMutex *Mutex

// The method set of *PrintableMutex contains the methods
// Lock and Unlock bound to its embedded field Mutex.
type PrintableMutex struct {
	Mutex
}

// MyBlock is an interface type that has the same method set as 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    // illegal: P is a type parameter

func f[T any]() {
	type L T   // illegal: T is a type parameter declared by the enclosing function
}

泛型类型也可以有与之关联的 方法。在这种情况下,方法接收者必须声明与泛型类型定义中存在的相同数量的类型参数。

// The method Len returns the number of elements in the linked list 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]

就像每个普通函数参数都有参数类型一样,每个类型参数都有一个对应的(元)类型,称为其类型约束

当泛型类型的类型参数列表声明一个带有约束 C 的单个类型参数 P,并且文本 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 refers to itself
type T2[P interface{ T2[int] }] …     // illegal: T2 refers to itself
type T3[P interface{ m(T3[int])}] …   // illegal: T3 refers to itself
type T4[P T5[P]] …                    // illegal: T4 refers to T5 and
type T5[P T4[P]] …                    //          T5 refers to T4

type T6[P int] struct{ f *T6[P] }     // ok: reference to T6 is not in type parameter list

类型约束

类型约束是一个接口,它定义了相应类型参数允许使用的类型实参集合,并控制该类型参数值支持的操作 [Go 1.18]。

TypeConstraint = TypeElem .

如果约束是一个形式为 interface{E} 的接口字面量,其中 E 是一个嵌入的类型元素(不是方法),那么在类型参数列表中,出于方便可以省略外围的 interface{ … }

[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                          // implements comparable (int is strictly comparable)
[]byte                       // does not implement comparable (slices cannot be compared)
interface{}                  // does not implement comparable (see above)
interface{ ~int | ~string }  // type parameter only: implements comparable (int, string types are strictly comparable)
interface{ comparable }      // type parameter only: implements comparable (comparable implements itself)
interface{ ~int | ~[]byte }  // type parameter only: does not implement comparable (slices are not comparable)
interface{ ~struct{ any } }  // type parameter only: does not implement comparable (field any is not strictly comparable)

comparable 接口以及(直接或间接)嵌入 comparable 的接口只能用作类型约束。它们不能作为值或变量的类型,也不能作为其他非接口类型的组成部分。

满足类型约束

如果类型实参 T 是由 C 定义的类型集合中的一个元素,即 T 实现 C,则类型实参 T 满足类型约束 C。作为例外,严格可比较的类型约束也可以被可比较的(不一定是严格可比较的)类型实参满足 [Go 1.20]。更精确地说

如果类型 T 满足约束 C,则:

type argument      type constraint                // constraint satisfaction

int                interface{ ~int }              // satisfied: int implements interface{ ~int }
string             comparable                     // satisfied: string implements comparable (string is strictly comparable)
[]byte             comparable                     // not satisfied: slices are not comparable
any                interface{ comparable; int }   // not satisfied: any does not implement interface{ int }
any                comparable                     // satisfied: any is comparable and implements the basic interface any
struct{f any}      comparable                     // satisfied: struct{f any} is comparable and implements the basic interface any
any                interface{ comparable; m() }   // not satisfied: any does not implement the basic interface interface{ m() }
interface{ m() }   interface{ comparable; m() }   // satisfied: interface{ m() } is comparable and implements the basic interface interface{ m() }

由于约束满足规则中的例外情况,比较类型参数类型的操作数可能会在运行时发生 panic(即使可比较的类型参数总是严格可比较的)。

变量声明

变量声明创建一个或多个变量,将相应的标识符绑定到它们,并为每个变量赋予类型和初始值。

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() returns a connected pair of Files and an error, if any
_, y, _ := coord(p)   // coord() returns three values; only interested in y coordinate

与常规变量声明不同,短变量声明可以重新声明变量,前提是这些变量最初是在同一块(如果该块是函数体,则在其参数列表)中以相同类型先前声明过的,并且至少有一个非变量是新的。因此,重新声明只能出现在多变量的短变量声明中。重新声明不会引入新变量;它只是将新值赋给原有变量。:= 左侧的非空变量名必须是唯一的。

field1, offset := nextField(str, 0)
field2, offset := nextField(str, offset)  // redeclares offset
x, y, x := 1, 2, 3                        // illegal: x repeated on left side of :=

短变量声明只能出现在函数内部。在某些上下文,例如"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
		}
	}
	// invalid: missing return statement
}

如果函数声明指定了类型参数,则函数名表示一个泛型函数。泛型函数必须先实例化才能被调用或用作值。

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

不带类型参数的函数声明可以省略函数体。这种声明提供了在 Go 外部实现的函数(例如汇编例程)的签名。

func flushICache(begin, end uintptr)  // implemented externally

方法声明

方法是一个带有接收者函数。方法声明将一个标识符(方法名称)绑定到一个方法,并将该方法与接收者的基类型关联起来。

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

接收者通过方法名称前的一个额外参数部分来指定。该参数部分必须声明一个非变长参数,即接收者。其类型必须是已定义类型 T 或指向已定义类型 T 的指针,其后可选地跟着一个用方括号 [] 括起来的类型参数名称列表 [P1, P2, …]T 称为接收者的基类型。接收者基类型不能是指针或接口类型,并且必须与方法定义在同一个包中。该方法被称为绑定到其接收者基类型,并且该方法名仅在类型 T*T选择器内可见。

接收者标识符在方法签名中必须是唯一的。如果接收者的值在方法体内部没有被引用,可以在声明中省略其标识符。通常,函数和方法的参数也适用相同规则。

对于一个基类型,绑定到它的方法的非空名称必须唯一。如果基类型是结构体类型,则非空的方法名和字段名必须不同。

假设已定义类型 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
}

将接收者类型为 *Point 的方法 LengthScale 绑定到基类型 Point

如果接收者基类型是泛型类型,则接收者规范必须声明方法使用的相应类型参数。这使得接收者类型参数可用于该方法。在语法上,这种类型参数声明看起来像接收者基类型的实例化:类型实参必须是表示正在声明的类型参数的标识符,接收者基类型的每个类型参数对应一个。类型参数名称不需要与其在接收者基类型定义中的相应参数名称匹配,并且所有非空参数名称在接收者参数部分和方法签名中必须唯一。接收者类型参数的约束是由接收者基类型定义隐含的:相应的类型参数具有相应的约束。

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

func (p Pair[A, B]) Swap() Pair[B, A]  { … }  // receiver declares A, B
func (p Pair[First, _]) First() First  { … }  // receiver declares First, corresponds to A in Pair

如果接收者类型由(指向)别名表示,则该别名不能是泛型的,也不能直接或间接通过另一个别名表示实例化泛型类型,无论是否存在指针间接引用。

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

func (*GPoint[P]) Draw(P)   { … }  // illegal: alias must not be generic
func (HPoint) Draw(P)       { … }  // illegal: alias must not denote instantiated type GPoint[int]
func (*IPair) Second() int  { … }  // illegal: alias must not denote instantiated type 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 // denotes the Sin function in package 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 必须是结构体、数组、切片或映射类型(语法强制执行此约束,除非类型以 TypeName 形式给出)。元素和键的类型必须可赋值给类型 T 的相应字段、元素和键类型;没有额外的转换。键在结构体字面量中解释为字段名,在数组和切片字面量中解释为索引,在映射字面量中解释为键。对于映射字面量,所有元素必须有键。指定多个具有相同字段名或常量键值的元素是错误的。对于非常量映射键,请参阅关于求值顺序的部分。

对于结构体字面量,适用以下规则:

  • 键必须是在结构体类型中声明的字段名。
  • 不包含任何键的元素列表必须按字段声明的顺序为每个结构体字段列出一个元素。
  • 如果任一元素有键,则每个元素都必须有键。
  • 包含键的元素列表不需要为每个结构体字段都提供一个元素。省略的字段获取该字段的零值。
  • 字面量可以省略元素列表;这样的字面量求值为其类型的零值。
  • 为属于不同包的结构体的非导出字段指定元素是错误的。

给定声明

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}

注意,切片或映射类型的零值与相同类型的已初始化但为空的值不同。因此,对空切片或空映射复合字面量取地址与使用new分配新的切片或映射值效果不同。

p1 := &[]int{}    // p1 points to an initialized, empty slice with value []int{} and length 0
p2 := new([]int)  // p2 points to an uninitialized slice with value nil and length 0

数组字面量的长度是字面量类型中指定的长度。如果字面量中提供的元素少于指定长度,则缺失的元素被设置为数组元素类型的零值。提供超出数组索引范围的索引值元素是错误的。... 符号指定数组长度等于最大元素索引加一。

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

切片字面量描述了整个底层数组字面量。因此,切片字面量的长度和容量是最大元素索引加一。切片字面量形式为:

[]T{x1, x2, … xn}

并且是应用于数组的切片操作的简写形式。

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

在数组、切片或映射类型 T 的复合字面量中,如果作为元素或映射键的复合字面量本身的类型与 T 的元素或键类型相同,则可以省略该复合字面量的类型。类似地,如果元素或键类型是 *T,则作为复合字面量地址的元素或键可以省略 &T

[...]Point{{1.5, -3.5}, {0, 0}}     // same as [...]Point{Point{1.5, -3.5}, Point{0, 0}}
[][]int{{1, 2, 3}, {4, 5}}          // same as [][]int{[]int{1, 2, 3}, []int{4, 5}}
[][]Point{{{0, 1}, {1, 2}}}         // same as [][]Point{[]Point{Point{0, 1}, Point{1, 2}}}
map[string]Point{"orig": {0, 0}}    // same as map[string]Point{"orig": Point{0, 0}}
map[Point]string{{0, 0}: "orig"}    // same as map[Point]string{Point{0, 0}: "orig"}

type PPoint *Point
[2]*Point{{1.5, -3.5}, {}}          // same as [2]*Point{&Point{1.5, -3.5}, &Point{}}
[2]PPoint{{1.5, -3.5}, {}}          // same as [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]) { … }

有效数组、切片和映射字面量的示例:

// list of prime numbers
primes := []int{2, 3, 5, 7, 9, 2147483647}

// vowels[ch] is true if ch is a vowel
vowels := [128]bool{'a': true, 'e': true, 'i': true, 'o': true, 'u': true, 'y': true}

// the array [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}

// frequencies in Hz for equal-tempered scale (A4 = 440Hz)
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()

选择器

对于非包名基本表达式 x选择器表达式

x.f

表示值 x 的字段或方法 f(有时是 *x;详见下文)。标识符 f 称为(字段或方法)选择器;它不能是空标识符。选择器表达式的类型是 f 的类型。如果 x 是包名,请参阅关于限定标识符的部分。

选择器 f 可以表示类型 T 的字段或方法 f,也可以引用 T 的嵌套嵌入字段的字段或方法 f。遍历以到达 f 的嵌入字段数量称为它在 T 中的深度。在 T 中声明的字段或方法 f 的深度为零。在 T 的嵌入字段 A 中声明的字段或方法 f 的深度是 fA 中的深度加一。

选择器适用以下规则:

  1. 对于类型为 T*T(其中 T 不是指针或接口类型)的值 xx.f 表示在 T 中存在 f 的最浅深度的字段或方法。如果最浅深度上不存在刚好一个 f,则选择器表达式非法。
  2. 对于类型为 I(其中 I接口类型)的值 xx.f 表示 x 的动态值的实际方法名 f。如果 I方法集中没有名为 f 的方法,则选择器表达式非法。
  3. 作为例外,如果 x 的类型是已定义的指针类型,并且 (*x).f 是一个有效的表示字段(但不是方法)的选择器表达式,则 x.f(*x).f 的简写。
  4. 在所有其他情况下,x.f 非法。
  5. 如果 x 是指针类型且值为 nil,并且 x.f 表示结构体字段,则赋值或求值 x.f 会导致运行时 panic
  6. 如果 x 是接口类型且值为 nil,则调用求值方法 x.f 会导致运行时 panic

例如,给定声明:

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 is a valid field selector

p.M0()       // ((*p).T0).M0()      M0 expects *T0 receiver
p.M1()       // ((*p).T1).M1()      M1 expects T1 receiver
p.M2()       // p.M2()              M2 expects *T2 receiver
t.M2()       // (&t).M2()           M2 expects *T2 receiver, see section on Calls

但以下是无效的:

q.M0()       // (*q).M0 is valid but not a field selector

方法表达式

如果 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 }  // value receiver
func (tp *T) Mp(f float32) float32 { return 1 }  // pointer receiver

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 的静态类型是 TM 在类型 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                    // receiver *t is evaluated and stored in f
g := s.M                    // receiver *(s.T) is evaluated and stored in g
*t = 42                     // does not affect stored receivers in f and g

类型 T 可以是接口类型或非接口类型。

如上文关于方法表达式的讨论所述,考虑一个结构体类型 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]

表示由 x 索引的数组、指向数组的指针、切片、字符串或映射 a 中的元素。值 x 分别称为索引映射键。适用以下规则:

如果 a 既不是映射也不是类型参数

  • 索引 x 必须是无类型常量,或者其核心类型必须是整型
  • 常量索引必须是非负的,并且可由 int 类型值表示
  • 无类型的常量索引被赋予 int 类型
  • 0 <= x < len(a) 时,索引 x 在范围in range),否则超出范围(out of range

对于数组类型 Aa

  • 常量索引必须在范围内
  • 如果 x 在运行时超出范围,则会发生运行时 panic
  • a[x] 是索引 x 处的数组元素,a[x] 的类型是 A 的元素类型

对于指向数组的指针类型 a

  • a[x](*a)[x] 的简写

对于切片类型 Sa

  • 如果 x 在运行时超出范围,则会发生运行时 panic
  • a[x] 是索引 x 处的切片元素,a[x] 的类型是 S 的元素类型

对于字符串类型a

  • 如果字符串 a 也是常量,常量索引必须在范围内
  • 如果 x 在运行时超出范围,则会发生运行时 panic
  • a[x] 是索引 x 处的非常量字节值,a[x] 的类型是 byte
  • a[x] 不能被赋值

对于映射类型 Ma

  • x 的类型必须可赋值M 的键类型
  • 如果映射包含键 x 的条目,则 a[x] 是键为 x 的映射元素,a[x] 的类型是 M 的元素类型
  • 如果映射是 nil 或不包含此类条目,则 a[x]M 的元素类型的零值

对于类型参数类型 Pa

  • 索引表达式 a[x] 对于 P 的类型集合中的所有类型的值都必须有效。
  • P 的类型集合中所有类型的元素类型必须相同。在此上下文中,字符串类型的元素类型是 byte
  • 如果 P 的类型集合中存在映射类型,则该类型集合中的所有类型都必须是映射类型,并且相应的键类型必须全部相同。
  • a[x] 是索引 x 处的数组、切片或字符串元素,或 P 实例化所使用的类型实参中键为 x 的映射元素,a[x] 的类型是(相同的)元素类型。
  • 如果 P 的类型集合包含字符串类型,则不能对 a[x] 赋值。

否则,a[x] 非法。

对类型为 map[K]V 的映射 a 使用的索引表达式,在赋值语句或以下特殊形式的初始化中使用时:

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

会产生一个额外的无类型布尔值。如果映射中存在键 x,则 ok 的值为 true,否则为 false

nil 映射的元素赋值会导致运行时 panic

切片表达式

切片表达式从字符串、数组、指向数组的指针或切片构造子字符串或切片。有两种变体:简单形式指定低位和高位边界,完整形式还指定容量边界。

简单切片表达式

基本表达式

a[low : high]

构造一个子字符串或切片。a核心类型必须是字符串、数组、指向数组的指针、切片或bytestring索引 lowhigh 选择操作数 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

出于方便,任何索引都可以省略。缺失的 low 索引默认为零;缺失的 high 索引默认为被切片操作数的长度:

a[2:]  // same as a[2 : len(a)]
a[:3]  // same as a[0 : 3]
a[:]   // same as a[0 : len(a)]

如果 a 是指向数组的指针,则 a[low : high](*a)[low : high] 的简写。

对于数组或字符串,当 0 <= low <= high <= len(a) 时,索引在范围in range),否则超出范围(out of range)。对于切片,上界索引是切片容量 cap(a) 而不是长度。常量索引必须是非负的,并且可由 int 类型值表示;对于数组或常量字符串,常量索引也必须在范围内。如果两个索引都是常量,它们必须满足 low <= high。如果索引在运行时超出范围,则会发生运行时 panic

除了无类型字符串,如果被切片操作数是字符串或切片,切片操作的结果是与操作数相同类型的非常量值。对于无类型字符串操作数,结果是 string 类型的非常量值。如果被切片操作数是数组,它必须是可寻址的,并且切片操作的结果是一个与该数组元素类型相同的切片。

如果有效切片表达式的被切片操作数是 nil 切片,则结果是 nil 切片。否则,如果结果是一个切片,它与操作数共享其底层数组。

var a [10]int
s1 := a[3:7]   // underlying array of s1 is array a; &s1[2] == &a[5]
s2 := s1[1:4]  // underlying array of s2 is underlying array of s1 which is array a; &s2[1] == &a[5]
s2[1] = 42     // s2[1] == s1[2] == a[5] == 42; they all refer to the same underlying array element

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) 时,索引在范围in range),否则超出范围(out of range)。常量索引必须是非负的,并且可由 int 类型值表示;对于数组,常量索引也必须在范围内。如果多个索引是常量,则存在的常量必须彼此相对在范围内。如果索引在运行时超出范围,则会发生运行时 panic

类型断言

对于接口类型(但不是类型参数)的表达式 x 和类型 T,该基本表达式

x.(T)

断言 xnil 且存储在 x 中的值是类型 T 的。符号 x.(T) 称为类型断言

更精确地说,如果 T 不是接口类型,x.(T) 断言 x 的动态类型与类型 T 相同。在这种情况下,T 必须实现 x 的(接口)类型;否则类型断言无效,因为 x 不可能存储类型 T 的值。如果 T 是接口类型,x.(T) 断言 x 的动态类型实现接口 T

如果类型断言成立,表达式的值就是存储在 x 中的值,其类型是 T。如果类型断言失败,就会发生 运行时 panic。换句话说,尽管 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) // dynamic types of v and ok are T and bool

产生一个额外的无类型布尔值。如果断言成立,ok 的值为 true。否则其值为 false,且 v 的值是类型 T零值。在这种情况下不会发生 运行时 panic

调用

给定一个表达式 f,其 核心类型 F函数类型

f(a1, a2, … an)

调用 f,参数为 a1, a2, … an。除一个特殊情况外,参数必须是单值表达式,可以 赋值F 的参数类型,并在函数调用前求值。表达式的类型是 F 的结果类型。方法调用与此类似,但方法本身是作为其接收者类型值的选择器来指定的。

math.Atan2(x, y)  // function call
var pt *Point
pt.Scale(3.5)     // method call with receiver pt

如果 f 表示一个泛型函数,它必须在被调用或用作函数值之前被 实例化

在函数调用中,函数值和参数按照 通常顺序 求值。求值后,为函数的 变量(包括其参数和结果)分配新存储空间。然后,调用参数被 传递 给函数,这意味着它们被 赋值 给其对应的函数参数,被调用的函数开始执行。函数返回时,其返回参数被传递回调用者。

调用一个 nil 函数值会导致 运行时 panic

作为一种特殊情况,如果函数或方法 g 的返回值数量上相等,并且可以逐个赋值给另一个函数或方法 f 的参数,则调用 f(g(parameters_of_g)) 将在将 g 的返回值按顺序传递给 f 的参数后调用 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 提供实参,传递给 pnil。否则,传递的值是一个类型为 []T 的新切片,其底层数组是新的,其后续元素是实参,所有实参都必须可以 赋值T。因此,切片的长度和容量是绑定到 p 的实参数量,并且对于每个调用点可能不同。

考虑函数和调用

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

Greeting 函数内部,在第一次调用中,who 的值为 nil,在第二次调用中,其值为 []string{"Joe", "Anna", "Eileen"}

如果最后一个实参可以赋值给切片类型 []T 并且后跟 ...,它被原样传递作为 ...T 参数的值。在这种情况下不会创建新切片。

考虑切片 s 和调用

s := []string{"James", "Jasmine"}
Greeting("goodbye:", s...)

Greeting 函数内部,who 将具有与 s 相同的值,使用相同的底层数组。

实例化

泛型函数或类型通过用 类型实参 替换类型形参来被 实例化 [Go 1.18]。实例化分两步进行:

  1. 将每个类型实参替换其在泛型声明中对应的类型形参。此替换发生在整个函数或类型声明中,包括类型形参列表本身以及该列表中的任何类型。
  2. 替换后,每个类型实参必须 满足 其对应的类型形参的 约束(如果需要,将被实例化)。否则实例化失败。

实例化类型会产生一个新的非泛型 命名类型;实例化函数会产生一个新的非泛型函数。

type parameter list    type arguments    after substitution

[P any]                int               int satisfies any
[S ~[]E, E any]        []int, int        []int satisfies ~[]int, int satisfies any
[P io.Writer]          string            illegal: string doesn't satisfy io.Writer
[P comparable]         any               any satisfies (but does not implement) comparable

使用泛型函数时,可以显式提供类型实参,或者可以从函数使用的上下文中部分或完全 推断 出来。如果可以推断出来,在以下情况下可以完全省略类型实参列表:

在所有其他情况下,必须存在一个(可能部分的)类型实参列表。如果类型实参列表不存在或不完整,所有缺失的类型实参必须能够从函数使用的上下文中推断出来。

// sum returns the sum (concatenation, for strings) of its arguments.
func sum[T ~int | ~float64 | ~string](x... T) T { … }

x := sum                       // illegal: the type of x is unknown
intSum := sum[int]             // intSum has type func(x... int) int
a := intSum(2, 3)              // a has value 5 of type int
b := sum[float64](2.0, 3)      // b has value 5.0 of type float64
c := sum(b, -1)                // c has value 4.0 of type float64

type sumFunc func(x... string) string
var f sumFunc = sum            // same as var f sumFunc = sum[string]
f = sum                        // same as f = sum[string]

部分类型实参列表不能是空的;至少第一个实参必须存在。该列表是完整的类型实参列表的前缀,剩余的实参留待推断。粗略地说,类型实参可以从“右到左”省略。

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

f0 := apply[]                  // illegal: type argument list cannot be empty
f1 := apply[[]int]             // type argument for S explicitly provided, type argument for E inferred
f2 := apply[[]string, string]  // both type arguments explicitly provided

var bytes []byte
r := apply(bytes, func(byte) byte { … })  // both type arguments inferred from the function arguments

对于泛型类型,总是必须显式提供所有类型实参。

类型推断

泛型函数的使用可以省略部分或所有类型实参,如果它们可以从函数使用的上下文中被 推断 出来,包括函数类型形参的约束。如果能够推断出缺失的类型实参并且使用推断出的类型实参进行 实例化 也成功,则类型推断成功。否则,类型推断失败,程序无效。

类型推断使用成对类型之间的类型关系进行推断:例如,函数实参必须可以 赋值 给其对应的函数形参;这建立了实参类型和形参类型之间的关系。如果这两种类型中任一种包含类型形参,类型推断会寻找用来替换类型形参的类型实参,使得可赋值性关系得到满足。类似地,类型推断使用类型实参必须 满足 其对应的类型形参的约束这一事实。

每一对这样的匹配类型都对应于一个 类型方程,该方程包含来自一个或可能多个泛型函数的一个或多个类型形参。推断缺失的类型实参意味着求解由此产生的一组类型方程,以得出各自的类型形参。

例如,给定

// dedup returns a copy of the argument slice with any duplicate entries removed.
func dedup[S ~[]E, E comparable](S) S { … }

type Slice []int
var s Slice
s = dedup(s)   // same as s = dedup[Slice, int](s)

为了使程序有效,类型为 Slice 的变量 s 必须可以赋值给函数形参类型 S。为了降低复杂性,类型推断忽略赋值的方向性,因此 SliceS 之间的类型关系可以通过(对称的)类型方程 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)

现在可以求解出类型形参 SE。从 (1) 中,编译器可以推断出 S 的类型实参是 Slice。类似地,因为 Slice 的底层类型是 []int 并且 []int 必须匹配约束中的 []E,编译器可以推断出 E 必须是 int。因此,对于这两个方程,类型推断推断出:

	S ➞ Slice
	E ➞ int

给定一组类型方程,需要求解的类型形参是需要被实例化且没有显式提供类型实参的函数的类型形参。这些类型形参被称为 绑定 类型形参。例如,在上面的 dedup 例子中,类型形参 SE 绑定到 dedup。泛型函数调用的一个实参本身可能是一个泛型函数。该函数的类型形参被包含在绑定类型形参集合中。函数实参的类型可能包含来自其他函数(例如包含函数调用的泛型函数)的类型形参。这些类型形参也可能出现在类型方程中,但在那种上下文中它们不是绑定的。类型方程总是只针对绑定类型形参进行求解。

类型推断支持泛型函数的调用以及将泛型函数赋值给(显式为函数类型的)变量。这包括将泛型函数作为实参传递给其他(可能也是泛型)函数,以及将泛型函数作为结果返回。类型推断针对每种情况,在一组特定的方程上进行操作。这些方程如下(为清晰起见,省略了类型实参列表):

  • 对于函数调用 f(a0, a1, …),其中 f 或函数实参 ai 是一个泛型函数:
    每一对对应的函数实参和形参 (ai, pi)(其中 ai 不是一个 无类型常量)产生一个方程 typeof(pi) ≡A typeof(ai)
    如果 ai 是一个无类型常量 cj,并且 typeof(pi) 是一个绑定类型形参 Pk,那么这对 (cj, Pk) 会被从类型方程中单独收集。

  • 对于将泛型函数 f 赋值给(非泛型)函数类型的变量 v
    typeof(v) ≡A typeof(f).

  • 对于返回语句 return …, f, … ,其中 f 是一个泛型函数,作为结果返回给一个(非泛型)函数类型的结果变量 r
    typeof(r) ≡A typeof(f).

此外,每个类型形参 Pk 及其对应的类型约束 Ck 产生类型方程 PkC Ck

在考虑无类型常量之前,类型推断优先考虑从有类型操作数获得的类型信息。因此,推断分两个阶段进行:

  1. 使用 类型统一 求解针对绑定类型形参的类型方程。如果统一失败,类型推断失败。

  2. 对于每个尚未推断出类型实参且收集了一个或多个包含该相同类型形参的对 (cj, Pk) 的绑定类型形参 Pk,确定所有这些对中常量 cj常量种类,方式与针对 常量表达式 相同。Pk 的类型实参是确定的常量种类的 默认类型。如果由于常量种类冲突而无法确定常量种类,类型推断失败。

如果在这两个阶段之后未能找到所有类型实参,类型推断失败。

如果这两个阶段成功,类型推断将为每个绑定类型形参确定一个类型实参。

	Pk ➞ Ak

类型实参 Ak 可能是一个复合类型,包含其他绑定类型形参 Pk 作为元素类型(或者甚至只是另一个绑定类型形参)。在一个重复简化的过程中,每个类型实参中的绑定类型形参被替换为那些类型形参对应的类型实参,直到每个类型实参不再包含绑定类型形参。

如果类型实参通过绑定类型形参包含对自身的循环引用,简化失败,进而导致类型推断失败。否则,类型推断成功。

类型统一

类型推断通过 类型统一 求解类型方程。类型统一递归地比较方程的 LHS 和 RHS 类型,其中一个或两个类型可能是或包含绑定类型形参,并寻找那些类型形参的类型实参,使得 LHS 和 RHS 匹配(根据上下文变得完全相同或赋值兼容)。为此,类型推断维护一个从绑定类型形参到推断类型实参的映射;在类型统一过程中查询和更新此映射。最初,绑定类型形参是已知的,但映射是空的。在类型统一过程中,如果推断出一个新的类型实参 A,则将从类型形参到实参的对应映射 P ➞ A 添加到映射中。相反,比较类型时,一个已知类型实参(映射中已存在条目的类型实参)代替其对应的类型形参。随着类型推断的进行,映射逐渐被填充,直到所有方程都被考虑,或直到统一失败。如果没有统一步骤失败,并且映射中对于每个类型形参都有一个条目,则类型推断成功。

例如,给定包含绑定类型形参 P 的类型方程:

	[10]struct{ elem P, list []P } ≡A [10]struct{ elem string; list []string }

类型推断从一个空映射开始。统一首先比较 LHS 和 RHS 类型的顶层结构。两者都是相同长度的数组;如果元素类型统一,它们就统一。两个元素类型都是结构体;如果它们具有相同数量的同名字段并且字段类型统一,它们就统一。P 的类型实参尚不已知(映射中没有条目),因此将 Pstring 统一会将映射 P ➞ string 添加到映射中。统一 list 字段的类型需要统一 []P[]string,因此也需要统一 Pstring。由于此时 P 的类型实参是已知的(映射中存在 P 的条目),其类型实参 string 代替 P。并且由于 stringstring 完全相同,此统一步骤也成功。方程的 LHS 和 RHS 的统一现已完成。类型推断成功是因为只有一个类型方程,没有统一步骤失败,并且映射已完全填充。

统一使用 精确宽松 统一的组合,取决于两个类型是否必须 完全相同赋值兼容,或仅在结构上相等。相应的 类型统一规则附录 中有详细说明。

对于形式为 X ≡A Y 的方程,其中 XY 是涉及赋值的类型(包括参数传递和返回语句),顶层类型结构可以宽松统一,但元素类型必须精确统一,匹配赋值规则。

对于形式为 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   = "+" | "-" | "!" | "^" | "*" | "&" | "<-" .

比较运算符在 其他地方 讨论。对于其他二元运算符,操作数类型必须 完全相同,除非操作涉及移位或无类型 常量。对于仅涉及常量的操作,参见关于 常量表达式 的章节。

除了移位操作,如果一个操作数是一个无类型 常量 且另一个操作数不是,该常量会被隐式 转换 为另一个操作数的类型。

移位表达式中的右操作数必须具有 整数类型 [Go 1.13],或是一个可以用 uint 类型的值来 表示 的无类型常量。如果非常量移位表达式的左操作数是一个无类型常量,它会首先被隐式转换为如果移位表达式被其左操作数本身替换的话,它将采用的类型。

var a [1024]byte
var s uint = 33

// The results of the following examples are given for 64-bit ints.
var i = 1<<s                   // 1 has type int
var j int32 = 1<<s             // 1 has type int32; j == 0
var k = uint64(1<<s)           // 1 has type uint64; k == 1<<33
var m int = 1.0<<s             // 1.0 has type int; m == 1<<33
var n = 1.0<<s == j            // 1.0 has type int32; n == true
var o = 1<<s == 2<<s           // 1 and 2 have type int; o == false
var p = 1<<s == 1<<33          // 1 has type int; p == true
var u = 1.0<<s                 // illegal: 1.0 has type float64, cannot shift
var u1 = 1.0<<s != 0           // illegal: 1.0 has type float64, cannot shift
var u2 = 1<<s != 1.0           // illegal: 1 has type float64, cannot shift
var v1 float32 = 1<<s          // illegal: 1 has type float32, cannot shift
var v2 = string(1<<s)          // illegal: 1 is converted to a string, cannot shift
var w int64 = 1.0<<33          // 1.0<<33 is a constant shift expression; w == 1<<33
var x = a[1.0<<s]              // panics: 1.0 has type int, but 1<<33 overflows array bounds
var b = make([]byte, 1.0<<s)   // 1.0 has type int; len(b) == 1<<33

// The results of the following examples are given for 32-bit ints,
// which means the shifts will overflow.
var mm int = 1.0<<s            // 1.0 has type int; mm == 0
var oo = 1<<s == 2<<s          // 1 and 2 have type int; oo == true
var pp = 1<<s == 1<<33         // illegal: 1 has type int, but 1<<33 overflows int
var xx = a[1.0<<s]             // 1.0 has type int; xx == a[0]
var bb = make([]byte, 1.0<<s)  // 1.0 has type int; len(bb) == 0

运算符优先级

一元运算符优先级最高。由于 ++-- 运算符构成语句而非表达式,它们不在运算符层次结构之内。因此,语句 *p++ 等同于 (*p)++

二元运算符有五个优先级级别。乘法运算符结合最紧密,其次是加法运算符、比较运算符、&& (逻辑与),最后是 || (逻辑或):

Precedence    Operator
    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 分别以 float32float64 的精度计算,取决于 F 的类型实参。

整数运算符

对于两个整数值 xy,整数商 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

此规则的一个例外是,如果被除数 xx 的整数类型的最负值,则由于二进制补码 整数溢出,商 q = x / -1 等于 x(且 r = 0)。

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

如果除数是一个 常量,它不能是零。如果除数在运行时为零,会发生 运行时 panic。如果被除数是非负的并且除数是一个常量的 2 的幂,除法可以用右移代替,计算余数可以用按位与操作代替。

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

移位运算符通过右操作数指定的移位计数移动左操作数,该计数必须是非负的。如果移位计数在运行时为负,会发生 运行时 panic。如果左操作数是有符号整数,移位运算符实现算术移位;如果是无符号整数,则实现逻辑移位。移位计数没有上限。移位表现得好像对于移位计数为 n,左操作数被 1 移位了 n 次。因此,x << 1 等同于 x*2,而 x >> 1 等同于 x/2 但向负无穷截断。

对于整数操作数,一元运算符 +-^ 定义如下:

+x                          is 0 + x
-x    negation              is 0 - x
^x    bitwise complement    is m ^ x  with m = "all bits set to 1" for unsigned x
                                      and  m = -1 for signed x

整数溢出

对于 无符号整数 值,运算 +-*<< 按照模 2n 计算,其中 n 是无符号整数类型的位宽。粗略地说,这些无符号整数运算在溢出时丢弃高位,程序可以依赖“回绕”行为。

对于有符号整数,运算 +-*/<< 可能合法溢出,并且结果值存在,其确定性由有符号整数表示、运算及其操作数定义。溢出不会导致 运行时 panic。编译器不能在假设溢出不会发生的情况下优化代码。例如,它不能假设 x < x + 1 总是为真。

浮点运算符

对于浮点数和复数,+x 等同于 x,而 -xx 的取反。浮点或复数除以零的结果在 IEEE 754 标准之外未指定;是否发生 运行时 panic 取决于具体实现。

具体实现可能将多个浮点运算结合成一个单一的融合操作,可能跨越语句,并产生与单独执行和舍入指令得到的值不同的结果。显式 浮点类型 转换 会舍入到目标类型的精度,从而阻止了会丢弃该舍入的融合。

例如,某些架构提供“乘加融合” (FMA) 指令,计算 x*y + z 而不对中间结果 x*y 进行舍入。这些例子展示了 Go 实现何时可以使用该指令:

// FMA allowed for computing r, because x*y is not explicitly rounded:
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 disallowed for computing r, because it would omit rounding of 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

在任何比较中,第一个操作数必须可以 赋值 给第二个操作数的类型,反之亦然。

相等运算符 ==!= 应用于 可比较 类型的操作数。顺序运算符 <<=>>= 应用于 有序 类型的操作数。这些术语和比较的结果定义如下:

  • 布尔类型是可比较的。两个布尔值相等,如果它们都为 true 或都为 false
  • 整数类型是可比较且有序的。两个整数值按照通常方式比较。
  • 浮点类型是可比较且有序的。两个浮点值按照 IEEE 754 标准定义的方式比较。
  • 复数类型是可比较的。两个复数值 uv 相等,如果 real(u) == real(v)imag(u) == imag(v) 都成立。
  • 字符串类型是可比较且有序的。两个字符串值按照字节逐个地进行词法比较。
  • 指针类型是可比较的。两个指针值相等,如果它们指向同一个变量,或者如果两者都为 nil 值。指向不同的 零大小 变量的指针可能相等也可能不相等。
  • 通道类型是可比较的。两个通道值相等,如果它们是通过同一个 make 调用创建的,或者如果两者都为 nil 值。
  • 非类型形参的接口类型是可比较的。两个接口值相等,如果它们具有 完全相同 的动态类型并且具有相等的动态值,或者如果两者都为 nil 值。
  • 如果类型 X 是可比较的并且 X 实现了 T,则类型为非接口类型 X 的值 x 和类型为接口类型 T 的值 t 可以比较。它们相等,如果 t 的动态类型与 X 完全相同,并且 t 的动态值与 x 相等。
  • 结构体类型是可比较的,如果所有其字段类型都是可比较的。两个结构体值相等,如果它们对应的非 空白标识符 字段值相等。字段按照源代码顺序比较,并且一旦两个字段值不同,比较就会停止(或者所有字段都已比较完)。
  • 数组类型是可比较的,如果它们的数组元素类型是可比较的。两个数组值相等,如果它们对应的元素值相等。元素按照升序索引顺序比较,并且一旦两个元素值不同,比较就会停止(或者所有元素都已比较完)。
  • 类型形参是可比较的,如果它们是严格可比较的(见下文)。

如果该类型不可比较,两个具有完全相同动态类型的接口值的比较会导致 运行时 panic。这种行为不仅适用于直接的接口值比较,也适用于比较接口值数组或带有接口值字段的结构体。

切片、map 和函数类型是不可比较的。然而,作为一种特殊情况,切片、map 或函数值可以与预声明标识符 nil 进行比较。指针、通道和接口值与 nil 的比较也是允许的,并遵循上述一般规则。

const c = 3 < 4            // c is the untyped boolean constant true

type MyBool bool
var x, y int
var (
	// The result of a comparison is an untyped boolean.
	// The usual assignment rules apply.
	b3        = x == y // b3 has type bool
	b4 bool   = x == y // b4 has type bool
	b5 MyBool = x == y // b5 has type 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"

地址运算符

对于类型 T 的操作数 x,地址操作 &x 生成一个指向 x 的类型为 *T 的指针。操作数必须是 可寻址的,也就是说,要么是一个变量、指针解引用、切片索引操作,要么是一个可寻址结构体操作数的字段选择器,或者是一个可寻址数组的数组索引操作。作为对可寻址性要求的一个例外,x 也可以是一个(可能带括号的)复合字面量。如果 x 的求值会导致 运行时 panic,那么 &x 的求值也会。

对于指针类型 *T 的操作数 x,指针解引用 *x 表示由 x 指向的类型 T变量。如果 xnil,尝试对 *x 求值会导致 运行时 panic

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

var x *int = nil
*x   // causes a run-time panic
&*x  // causes a run-time panic

接收运算符

对于其 核心类型 是一个 通道 的操作数 ch,接收操作 <-ch 的值是从通道 ch 接收到的值。通道方向必须允许接收操作,并且接收操作的类型是通道的元素类型。该表达式会阻塞,直到有值可用。从 nil 通道接收会永远阻塞。对一个 已关闭 通道的接收操作总是可以立即进行,在任何先前发送的值都被接收之后,产生元素类型的 零值

v1 := <-ch
v2 = <-ch
f(<-ch)
<-strobe  // wait until clock pulse and discard received value

用于 赋值语句 或采用特殊形式初始化的接收表达式

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

产生一个额外的无类型布尔结果,报告通信是否成功。如果接收到的值是由成功的发送操作发送到通道的,则 ok 的值为 true;否则,如果它是因为通道已关闭且为空而产生的零值,则为 false

转换

转换改变表达式的 类型 为转换指定的类型。转换可能字面上出现在源代码中,或者可能由表达式出现的上下文所 隐含

一个 显式 转换是形式为 T(x) 的表达式,其中 T 是一个类型,并且 x 是可以转换为类型 T 的表达式。

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

如果类型以运算符 *<- 开头,或者如果类型以关键字 func 开头且没有结果列表,在必要时必须加括号以避免歧义:

*Point(p)        // same as *(Point(p))
(*Point)(p)      // p is converted to *Point
<-chan int(c)    // same as <-(chan int(c))
(<-chan int)(c)  // c is converted to <-chan int
func()(x)        // function signature func() x
(func())(x)      // x is converted to func()
(func() int)(x)  // x is converted to func() int
func() int(x)    // x is converted to func() int (unambiguous)

常量x 可以转换为类型 T,如果 x 可以由类型 T 的值来 表示。作为一种特殊情况,整数常量 x 可以使用与非常量 x 相同的 规则 显式转换为 字符串类型

将常量转换为非 类型形参 的类型会产生一个有类型常量。

uint(iota)               // iota value of type uint
float32(2.718281828)     // 2.718281828 of type float32
complex128(1)            // 1.0 + 0.0i of type complex128
float32(0.49999999)      // 0.5 of type float32
float64(-1e-1000)        // 0.0 of type float64
string('x')              // "x" of type string
string(0x266c)           // "♬" of type string
myString("foo" + "bar")  // "foobar" of type myString
string([]byte{'a'})      // not a constant: []byte{'a'} is not a constant
(*int)(nil)              // not a constant: nil is not a constant, *int is not a boolean, numeric, or string type
int(1.2)                 // illegal: 1.2 cannot be represented as an int
string(65.0)             // illegal: 65.0 is not an integer constant

将常量转换为类型形参会产生该类型的 非常量 值,该值表示为该类型形参被 实例化 所用的类型实参的值。例如,给定函数:

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

转换 P(1.1) 产生类型 P 的非常量值,并且值 1.1 表示为 float32float64,取决于 f 的类型实参。因此,如果 f 使用 float32 类型实例化,表达式 P(1.1) + 1.2 的数值将以与对应的非常量 float32 加法相同的精度计算。

在以下任一情况下,非常量值 x 可以转换为类型 T

  • x 可以 赋值T
  • 忽略结构体标签(见下文),x 的类型和 T 都不是 类型形参,但具有 完全相同底层类型
  • 忽略结构体标签(见下文),x 的类型和 T 都是不是 命名类型 的指针类型,并且它们的指针基类型不是类型形参但具有完全相同的底层类型。
  • x 的类型和 T 都是整数类型或浮点类型。
  • x 的类型和 T 都是复数类型。
  • x 是一个整数,或者是一个字节切片或 rune 切片,并且 T 是一个字符串类型。
  • x 是一个字符串,并且 T 是一个字节切片或 rune 切片。
  • x 是一个切片,T 是一个数组 [Go 1.20] 或一个指向数组的指针 [Go 1.17],并且切片和数组类型具有 完全相同 的元素类型。

此外,如果 Tx 的类型 V 是类型参数,则在满足以下任一条件时,x 也可以转换为类型 T

  • VT 都是类型参数,并且 V 的类型集中的每种类型的值都可以转换为 T 的类型集中的每种类型。
  • 只有 V 是类型参数,并且 V 的类型集中的每种类型的值都可以转换为 T
  • 只有 T 是类型参数,并且 x 可以转换为 T 的类型集中的每种类型。

在为转换目的比较结构体类型的同一性时,会忽略结构体标签

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)  // ignoring tags, the underlying types are identical

特定的规则适用于(非常量)数字类型之间的转换或与字符串类型之间的相互转换。这些转换可能会改变 x 的表示形式并产生运行时开销。所有其他转换只改变类型而不改变 x 的表示形式。

语言中没有在指针和整数之间进行转换的机制。包 unsafe 在受限情况下实现了此功能。

数字类型之间的转换

对于非常量数字值的转换,适用以下规则

  1. 整数类型之间进行转换时,如果该值是有符号整数,则将其符号位扩展到隐式无限精度;否则进行零扩展。然后将其截断以适应结果类型的大小。例如,如果 v := uint16(0x10F0),则 uint32(int8(v)) == 0xFFFFFFF0。转换总是产生一个有效值;没有溢出指示。
  2. 浮点数转换为整数时,小数部分将被丢弃(向零截断)。
  3. 将整数或浮点数转换为浮点类型,或将复数转换为另一个复数类型时,结果值会四舍五入到目标类型指定的精度。例如,类型为 float32 的变量 x 的值可能使用超出 IEEE 754 32 位数字的额外精度存储,但 float32(x) 表示将 x 的值四舍五入到 32 位精度的结果。类似地,x + 0.1 可能使用超过 32 位的精度,但 float32(x + 0.1) 不会。

在所有涉及浮点或复数值的非常量转换中,如果结果类型无法表示该值,则转换成功但结果值依赖于具体实现。

与字符串类型之间的相互转换

  1. 将字节切片转换为字符串类型会得到一个字符串,其连续字节是该切片的元素。
    string([]byte{'h', 'e', 'l', 'l', '\xc3', '\xb8'})   // "hellø"
    string([]byte{})                                     // ""
    string([]byte(nil))                                  // ""
    
    type bytes []byte
    string(bytes{'h', 'e', 'l', 'l', '\xc3', '\xb8'})    // "hellø"
    
    type myByte byte
    string([]myByte{'w', 'o', 'r', 'l', 'd', '!'})       // "world!"
    myString([]myByte{'\xf0', '\x9f', '\x8c', '\x8d'})   // "🌍"
    
  2. 将 rune 切片转换为字符串类型会得到一个字符串,该字符串是将单个 rune 值转换为字符串后连接起来的结果。
    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" == "🌎"
    
  3. 将字符串类型的值转换为字节切片类型会得到一个非零切片,其连续元素是字符串的字节。结果切片的容量依赖于具体实现,并且可能大于切片的长度。
    []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'}
    
  4. 将字符串类型的值转换为 rune 切片类型会得到一个包含字符串中单个 Unicode 码点的切片。结果切片的容量依赖于具体实现,并且可能大于切片的长度。
    []rune(myString("白鵬翔"))   // []rune{0x767d, 0x9d6c, 0x7fd4}
    []rune("")                  // []rune{}
    
    runes("白鵬翔")              // []rune{0x767d, 0x9d6c, 0x7fd4}
    
    []myRune("♫♬")              // []myRune{0x266b, 0x266c}
    []myRune(myString("🌐"))    // []myRune{0x1f310}
    
  5. 最后,出于历史原因,整数值可以转换为字符串类型。这种形式的转换会产生一个字符串,其中包含给定整数值对应的 Unicode 码点的 (可能多字节) UTF-8 表示。超出有效 Unicode 码点范围的值将被转换为 "\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.AppendRuneutf8.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

常量表达式

常量表达式只能包含常量操作数,并在编译时求值。

无类型的布尔、数字和字符串常量可以在任何合法使用布尔、数字或字符串类型操作数的地方用作操作数。

常量比较总是产生一个无类型布尔常量。如果常量移位表达式的左操作数是无类型常量,结果是整数常量;否则,结果是与左操作数相同类型的常量,左操作数必须是整数类型

对无类型常量的任何其他操作都会产生相同种类的无类型常量;即布尔、整数、浮点、复数或字符串常量。如果二元操作(移位除外)的无类型操作数种类不同,结果将是在此列表中后出现的那个操作数种类:整数、rune、浮点、复数。例如,无类型整数常量除以无类型复数常量会产生一个无类型复数常量。

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 应用于无类型整数、rune 或浮点常量会产生一个无类型复数常量。

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()<-cg()k()。然而,这些事件与对 x 的求值和索引以及对 yz 的求值的顺序未指定,除非词法上有要求。例如,在 g 的参数被求值之前,不能调用 g

a := 1
f := func() int { a++; return a }
x := []int{a, f()}            // x may be [1, 2] or [2, 2]: evaluation order between a and f() is not specified
m := map[int]int{a: 1, a: 2}  // m may be {2: 1} or {2: 2}: evaluation order between the two map assignments is not specified
n := map[int]int{a: f()}      // n may be {2: 3} or {3: 3}: evaluation order between the key and the value is not specified

在包级别,初始化依赖关系会覆盖单个初始化表达式的从左到右规则,但不会覆盖每个表达式内部操作数的求值顺序

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 }

// functions u and v are independent of all other variables and functions

函数调用发生的顺序是 u()sqr()v()f()v()g()

单个表达式中的浮点运算根据运算符的结合性进行求值。显式括号通过覆盖默认结合性来影响求值。在表达式 x + (y + z) 中,加法 y + z 在加上 x 之前执行。

语句

语句控制执行流程。

Statement  = Declaration | LabeledStmt | SimpleStmt |
             GoStmt | ReturnStmt | BreakStmt | ContinueStmt | GotoStmt |
             FallthroughStmt | Block | IfStmt | SwitchStmt | SelectStmt | ForStmt |
             DeferStmt .

SimpleStmt = EmptyStmt | ExpressionStmt | SendStmt | IncDecStmt | Assignment | ShortVarDecl .

终止语句

一个终止语句会中断中正常的控制流。以下语句是终止语句

  1. 一个“return”“goto”语句。
  2. 对内置函数panic的调用。
  3. 一个,其语句列表以终止语句结束。
  4. 一个“if”语句,其中
    • 存在“else”分支,并且
    • 两个分支都是终止语句。
  5. 一个“for”语句,其中
    • 没有指向该“for”语句的“break”语句,并且
    • 循环条件不存在,并且
    • 该“for”语句未使用 range 子句。
  6. 一个“switch”语句,其中
    • 没有指向该“switch”语句的“break”语句,
    • 存在 default 分支,并且
    • 每个 case(包括 default)中的语句列表以终止语句,或可能带有标签的“fallthrough”语句结束。
  7. 一个“select”语句,其中
    • 没有指向该“select”语句的“break”语句,并且
    • 每个 case(包括 default,如果存在)中的语句列表以终止语句结束。
  8. 一个标记终止语句的带标签语句

所有其他语句都不是终止语句。

如果语句列表不为空,并且其最后一个非空语句是终止语句,则该语句列表以终止语句结束。

空语句

空语句不做任何事情。

EmptyStmt = .

带标签语句

带标签语句可以是 gotobreakcontinue 语句的目标。

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 .

通道和值表达式都在通信开始前求值。通信会阻塞直到发送可以进行。无缓冲通道上的发送可以在接收方准备好时进行。有缓冲通道上的发送可以在缓冲区有空间时进行。在已关闭通道上的发送会引起运行时恐慌。在 nil 通道上的发送会永远阻塞。

ch <- 3  // send value 3 to channel ch

增减语句

“++”和“--”语句将其操作数增加或减少无类型常量 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 = '一', '二', '三'

空白标识符提供了一种在赋值中忽略右侧值的方法

_ = x       // evaluate x but ignore it
x, _ = f()  // evaluate f() but ignore second result value

赋值分两个阶段进行。首先,左侧的索引表达式指针间接引用(包括选择器中隐式的指针间接引用)的操作数以及右侧的表达式都按照常规顺序求值。其次,赋值按照从左到右的顺序执行。

a, b = b, a  // exchange a and b

x := []int{1, 2, 3}
i := 0
i, x[i] = 1, 2  // set i = 1, x[0] = 2

i = 0
x[i], i = 2, 1  // set x[0] = 2, i = 1

x[0], x[0] = 1, 2  // set x[0] = 1, then x[0] = 2 (so x[0] == 2 at end)

x[1], x[3] = 4, 5  // set x[1] = 4, then panic setting x[3] = 5.

type Point struct { x, y int }
var p *Point
x[2], p.x = 6, 7  // set x[2] = 6, then panic setting p.x = 7

i = 2
x = []int{3, 5, 7}
for i, x[i] = range x {  // set i, x[2] = 0, x[0]
	break
}
// after this loop, i == 0 and x is []int{3, 5, 3}

在赋值中,每个值必须可赋值给其被赋值的操作数的类型,并有以下特殊情况

  1. 任何有类型的值都可以赋值给空白标识符。
  2. 如果将无类型常量赋值给接口类型的变量或空白标识符,该常量会首先隐式转换为其默认类型
  3. 如果将无类型布尔值赋值给接口类型的变量或空白标识符,它会首先隐式转换为 bool 类型。

将值赋给变量时,只会替换存储在变量中的数据。如果该值包含引用,赋值会复制该引用,但不会复制引用的数据(例如切片的底层数组)。

var s1 = []int{1, 2, 3}
var s2 = s1                    // s2 stores the slice descriptor of s1
s1 = s1[:1]                    // s1's length is 1 but it still shares its underlying array with s2
s2[0] = 42                     // setting s2[0] changes s1[0] as well
fmt.Println(s1, s2)            // prints [42] [42 2 3]

var m1 = make(map[string]int)
var m2 = m1                    // m2 stores the map descriptor of m1
m1["foo"] = 42                 // setting m1["foo"] changes m2["foo"] as well
fmt.Println(m2["foo"])         // prints 42

If 语句

“If”语句根据布尔表达式的值指定两个分支的条件执行。如果表达式求值为 true,则执行“if”分支;否则,如果存在“else”分支,则执行“else”分支。

IfStmt = "if" [ SimpleStmt ";" ] Expression Block [ "else" ( IfStmt | Block ) ] .
if x > max {
	x = max
}

表达式前面可以有一个简单语句,该语句在表达式求值之前执行。

if x := f(); x < y {
	return x
} else if x > z {
	return z
} else {
	return y
}

Switch 语句

“Switch”语句提供多分支执行。将表达式或类型与“switch”内部的“cases”进行比较,以确定执行哪个分支。

SwitchStmt = ExprSwitchStmt | TypeSwitchStmt .

有两种形式:“表达式 switch”和“类型 switch”。在表达式 switch 中,cases 包含与 switch 表达式值进行比较的表达式。在类型 switch 中,cases 包含与特殊标注的 switch 表达式类型进行比较的类型。switch 表达式在 switch 语句中只求值一次。

表达式 switch

在表达式 switch 中,先对 switch 表达式求值,然后对 case 表达式(不必是常量)按从左到右、从上到下的顺序求值;第一个等于 switch 表达式的 case 会触发执行相关 case 的语句;其他 case 被跳过。如果没有 case 匹配,且存在“default” case,则执行其语句。最多只能有一个 default case,它可以出现在“switch”语句中的任何位置。缺少的 switch 表达式等价于布尔值 true

ExprSwitchStmt = "switch" [ SimpleStmt ";" ] [ Expression ] "{" { ExprCaseClause } "}" .
ExprCaseClause = ExprSwitchCase ":" StatementList .
ExprSwitchCase = "case" ExpressionList | "default" .

如果 switch 表达式求值为无类型常量,它会首先隐式转换为其默认类型。预声明的无类型值 nil 不能用作 switch 表达式。switch 表达式的类型必须可比较

如果 case 表达式是无类型的,它会首先隐式转换为 switch 表达式的类型。对于每个(可能已转换的)case 表达式 x 和 switch 表达式的值 tx == t 必须是有效的比较

换句话说,switch 表达式被视为用于声明和初始化一个没有显式类型的临时变量 t;每个 case 表达式 x 都是针对 t 的这个值进行相等性测试的。

在 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

类型 switch 比较类型而非值。它在其他方面与表达式 switch 相似。它由一个特殊 switch 表达式标记,该表达式的形式是使用关键字 type 而非实际类型进行的类型断言

switch x.(type) {
// cases
}

然后,cases 将实际类型 T 与表达式 x 的动态类型进行匹配。与类型断言一样,x 必须是接口类型,但不能是类型参数,并且 case 中列出的每个非接口类型 T 必须实现 x 的类型。类型 switch 的 cases 中列出的类型必须都不同

TypeSwitchStmt  = "switch" [ SimpleStmt ";" ] TypeSwitchGuard "{" { TypeCaseClause } "}" .
TypeSwitchGuard = [ identifier ":=" ] PrimaryExpr "." "(" "type" ")" .
TypeCaseClause  = TypeSwitchCase ":" StatementList .
TypeSwitchCase  = "case" TypeList | "default" .

TypeSwitchGuard 可以包含短变量声明。使用该形式时,变量在每个子句的隐式块中 TypeSwitchCase 的末尾声明。在只列出一个类型的 case 的子句中,变量具有该类型;否则,变量具有 TypeSwitchGuard 中表达式的类型。

除了类型之外,case 还可以使用预声明标识符nil;当 TypeSwitchGuard 中的表达式是 nil 接口值时,会选择该 case。最多只能有一个 nil case。

给定一个类型为 interface{} 的表达式 x,以下类型 switch

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{})
}

可以重写为

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

类型 switch guard 前面可以有一个简单语句,该语句在 guard 求值之前执行。

“fallthrough”语句不允许出现在类型 switch 中。

For 语句

“for”语句指定块的重复执行。有三种形式:迭代可以由单个条件、“for”子句或“range”子句控制。

ForStmt   = "for" [ Condition | ForClause | RangeClause ] Block .
Condition = Expression .

带单个条件的 For 语句

在其最简单的形式中,“for”语句指定只要布尔条件求值为 true,就重复执行一个块。条件在每次迭代之前求值。如果条件缺失,则等价于布尔值 true

for a < b {
	a *= 2
}

for 子句的 For 语句

带 ForClause 的“for”语句也受其条件控制,但此外还可以指定一个init 和一个post 语句,例如赋值、增量或减量语句。init 语句可以是短变量声明,但 post 语句不能。

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

range 子句的 For 语句

带“range”子句的“for”语句遍历数组、切片、字符串或映射的所有条目,通道上接收的值,从零到一个上限的整数值 [Go 1.22],或传递给迭代器函数的 yield 函数的值 [Go 1.23]。对于每个条目,如果存在,它将迭代值赋给相应的迭代变量,然后执行块。

RangeClause = [ ExpressionList "=" | IdentifierList ":=" ] "range" Expression .

“range”子句右侧的表达式称为range 表达式,其核心类型必须是数组、数组指针、切片、字符串、映射、允许接收操作的通道、整数或具有特定签名的函数(见下文)。与赋值一样,如果存在,左侧的操作数必须可寻址或为映射索引表达式;它们表示迭代变量。如果 range 表达式是一个函数,则迭代变量的最大数量取决于函数签名。如果 range 表达式是通道或整数,最多只允许一个迭代变量;否则最多可以有两个。如果最后一个迭代变量是空白标识符,则 range 子句等价于没有该标识符的相同子句。

range 表达式 x 在循环开始前求值,但有一个例外:如果最多只有一个迭代变量存在,并且 xlen(x)常量,则 range 表达式不求值。

左侧的函数调用在每次迭代中求值一次。对于每次迭代,如果相应的迭代变量存在,则按如下方式产生迭代值

Range expression                                       1st value                2nd value

array or slice      a  [n]E, *[n]E, or []E             index    i  int          a[i]       E
string              s  string type                     index    i  int          see below  rune
map                 m  map[K]V                         key      k  K            m[k]       V
channel             c  chan E, <-chan E                element  e  E
integer value       n  integer type, or untyped int    value    i  see below
function, 0 values  f  func(func() bool)
function, 1 value   f  func(func(V) bool)              value    v  V
function, 2 values  f  func(func(K, V) bool)           key      k  K            v          V
  1. 对于数组、数组指针或切片值 a,索引迭代值按递增顺序产生,从元素索引 0 开始。如果最多只有一个迭代变量存在,range 循环会产生从 0 到 len(a)-1 的迭代值,并且不索引到数组或切片本身。对于 nil 切片,迭代次数为 0。
  2. 对于字符串值,“range”子句从字节索引 0 开始遍历字符串中的 Unicode 码点。在连续的迭代中,索引值将是字符串中连续 UTF-8 编码码点的第一个字节的索引,而第二个值(类型为 rune)将是相应码点的值。如果迭代遇到无效的 UTF-8 序列,第二个值将是 0xFFFD,即 Unicode 替换字符,并且下一次迭代将在字符串中前进一个字节。
  3. 映射的迭代顺序未指定,并且不保证每次迭代都相同。如果在迭代期间删除了尚未到达的映射条目,则不会产生相应的迭代值。如果在迭代期间创建了映射条目,则该条目可能在迭代期间产生,也可能被跳过。对于每个创建的条目以及从一次迭代到下一次迭代,选择可能会有所不同。如果映射是 nil,迭代次数为 0。
  4. 对于通道,产生的迭代值是通道上连续发送的值,直到通道被关闭。如果通道是 nil,range 表达式会永远阻塞。
  5. 对于整数值 n,其中 n整数类型或无类型整数常量,迭代值从 0 到 n-1 按递增顺序产生。如果 n 是整数类型,迭代值具有相同的类型。否则,n 的类型确定方式如同它被赋值给迭代变量一样。具体来说:如果迭代变量是预先存在的,则迭代值的类型是迭代变量的类型,该类型必须是整数类型。否则,如果迭代变量由“range”子句声明或不存在,则迭代值的类型是默认类型。如果 n <= 0,循环不执行任何迭代。
  6. 对于函数 f,迭代通过调用 f 并将一个新的、合成的 yield 函数作为其参数来进行。如果在 f 返回之前调用了 yield,则 yield 的参数会成为执行一次循环体的迭代值。在每次连续的循环迭代之后,yield 返回 true,并且可以再次调用以继续循环。只要循环体不终止,“range”子句将继续以这种方式为每次 yield 调用生成迭代值,直到 f 返回。如果循环体终止(例如通过 break 语句),yield 返回 false,并且不得再次调用。

可以使用短变量声明:=)的形式,由“range”子句声明迭代变量。在这种情况下,它们的作用域是“for”语句的块,并且每次迭代都有自己新的变量 [Go 1.22](参见带 ForClause 的“for”语句)。变量具有其各自迭代值的类型。

如果迭代变量没有由“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”语句,但其 cases 都指向通信操作。

SelectStmt = "select" "{" { CommClause } "}" .
CommClause = CommCase ":" StatementList .
CommCase   = "case" ( SendStmt | RecvStmt ) | "default" .
RecvStmt   = [ ExpressionList "=" | IdentifierList ":=" ] RecvExpr .
RecvExpr   = Expression .

带有 RecvStmt 的 case 可以将 RecvExpr 的结果赋值给一个或两个变量,这些变量可以使用短变量声明来声明。RecvExpr 必须是一个(可能加括号的)接收操作。最多只能有一个 default case,它可以出现在 case 列表中的任何位置。

“select”语句的执行按以下几个步骤进行

  1. 对于语句中的所有 case,接收操作的通道操作数以及发送语句的通道和右侧表达式在进入“select”语句时按源顺序精确求值一次。结果是一组要从中接收或向其发送的通道,以及要发送的相应值。求值中的任何副作用都会发生,无论(如果存在的话)选择了哪种通信操作进行。带有短变量声明或赋值的 RecvStmt 左侧的表达式尚未求值。
  2. 如果一个或多个通信可以进行,则通过均匀的伪随机选择选取一个可以进行的操作。否则,如果存在 default case,则选择该 case。如果不存在 default case,“select”语句会阻塞,直到至少有一个通信可以进行。
  3. 除非选择的 case 是 default case,否则执行相应的通信操作。
  4. 如果选择的 case 是带有短变量声明或赋值的 RecvStmt,则求值左侧表达式,并赋值接收到的值(或多个值)。
  5. 执行所选 case 的语句列表。

由于在 nil 通道上的通信永远无法进行,因此一个只包含 nil 通道且没有 default case 的 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 中的“return”语句终止 F 的执行,并可选地提供一个或多个结果值。任何由 F defer 的函数都会在 F 返回其调用者之前执行。

ReturnStmt = "return" [ ExpressionList ] .

在没有结果类型的函数中,“return”语句不得指定任何结果值。

func noResult() {
	return
}

从带结果类型的函数中返回值有三种方式

  1. 返回值可以在 "return" 语句中显式列出。每个表达式必须是单值表达式,并且可赋值给函数结果类型对应的元素。
    func simpleF() int {
    	return 2
    }
    
    func complexF1() (re float64, im float64) {
    	return -7.0, -4.0
    }
    
  2. "return" 语句中的表达式列表可以是单次调用一个多值函数。其效果等同于将该函数返回的每个值赋给一个具有相应值类型的临时变量,然后是一个列出这些变量的 "return" 语句,此时适用前述情况的规则。
    func complexF2() (re float64, im float64) {
    	return complexF1()
    }
    
  3. 如果函数的结果参数指定了名称,表达式列表可以为空。结果参数作为普通的局部变量,函数可以根据需要为其赋值。"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 语句所在位置不在作用域内的变量进入作用域。例如,以下示例

	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" 语句将控制流转移到表达式 "switch" 语句中下一个 case 子句的第一个语句。它只能用作此类子句中的最后一个非空语句。

FallthroughStmt = "fallthrough" .

Defer 语句

"defer" 语句调用一个函数,该函数的执行被推迟到其所在的函数返回时,这可能是因为外层函数执行了return 语句,到达了其函数体的末尾,或者因为相应的 goroutine 正在发生 panic

DeferStmt = "defer" Expression .

该表达式必须是函数或方法调用;它不能加括号。内置函数的调用受到限制,与表达式语句相同。

每次执行 "defer" 语句时,函数值和调用参数都会照常求值并重新保存,但实际函数不会被调用。相反,延迟函数会在外层函数返回之前立即被调用,顺序与它们被延迟的顺序相反。也就是说,如果外层函数通过显式的return 语句返回,延迟函数会在该 return 语句设置任何结果参数之后执行,但函数返回其调用者之前执行。如果延迟的函数值求值为 nil,则在调用函数时会发生panic,而不是在执行 "defer" 语句时。

例如,如果延迟函数是一个函数字面量,并且外层函数具有在其字面量作用域内可访问的命名结果参数,则延迟函数可以在结果参数返回之前访问和修改它们。如果延迟函数有任何返回值,它们将在函数完成后被丢弃。(另请参阅处理 panic 一节。)

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
}

内建函数

内建函数是预声明的。它们像其他函数一样被调用,但其中一些接受一个类型而不是表达式作为第一个参数。

内建函数没有标准的 Go 类型,因此它们只能出现在调用表达式中;它们不能用作函数值。

追加和复制切片

内建函数 appendcopy 辅助完成常见的切片操作。对于这两个函数,结果与参数引用的内存是否重叠无关。

变长参数函数 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 接受mapslice类型参数类型参数,并删除或清零所有元素 [Go 1.21]。

Call        Argument type     Result

clear(m)    map[K]T           deletes all entries, resulting in an
                              empty map (len(m) == 0)

clear(s)    []T               sets all elements up to the length of
                              s to the zero value of T

clear(t)    type parameter    see below

如果 clear 函数参数的类型是类型参数,则其类型集中的所有类型都必须是 map 或 slice,并且 clear 会执行与实际类型参数对应的操作。

如果 map 或 slice 是 nilclear 是一个空操作。

Close

对于核心类型channel的参数 ch,内建函数 close 会记录该 channel 不再发送值。如果 ch 是只接收 channel,则会报错。向已关闭的 channel 发送或关闭已关闭的 channel 会导致运行时 panic。关闭 nil channel 也会导致运行时 panic。调用 close 后,并且在任何先前发送的值已被接收后,接收操作将返回 channel 类型的零值而不会阻塞。多值接收操作返回接收到的值以及 channel 是否已关闭的指示。

操纵复数

有三个函数用于组装和分解复数。内建函数 complex 从浮点实部和虚部构造一个复数,而 realimag 提取复数的实部和虚部。

complex(realPart, imaginaryPart floatT) complexT
real(complexT) floatT
imag(complexT) floatT

参数和返回值的类型是对应的。对于 complex,两个参数必须具有相同的浮点类型,返回类型是具有相应浮点分量的复数类型:对于 float32 参数,返回类型是 complex64;对于 float64 参数,返回类型是 complex128。如果其中一个参数求值为无类型常量,它会首先隐式地转换为另一个参数的类型。如果两个参数都求值为无类型常量,它们必须是非复数或其虚部必须为零,并且函数的返回值为无类型复数常量。

对于 realimag,参数必须是复数类型,返回类型是相应的浮点类型:对于 complex64 参数,返回类型是 float32;对于 complex128 参数,返回类型是 float64。如果参数求值为无类型常量,它必须是一个数字,并且函数的返回值为无类型浮点常量。

realimag 函数共同构成了 complex 函数的逆运算,因此对于一个复数类型 Z 的值 z,有 z == Z(complex(real(z), imag(z)))

如果这些函数的所有操作数都是常量,则返回值为常量。

var a = complex(2, -2)             // complex128
const b = complex(1.0, -1.4)       // untyped complex constant 1 - 1.4i
x := float32(math.Cos(math.Pi/2))  // float32
var c64 = complex(5, -x)           // complex64
var s int = complex(1, 0)          // untyped complex constant 1 + 0i can be converted to int
_ = complex(1, 2<<s)               // illegal: 2 assumes floating-point type, cannot shift
var rl = real(c64)                 // float32
var im = imag(a)                   // float64
const c = imag(b)                  // untyped constant -1.4
_ = imag(3 << s)                   // illegal: 3 assumes complex type, cannot shift

不允许类型参数类型的参数。

删除 map 元素

内建函数 deletemap m 中移除键为 k 的元素。值 k 必须可赋值给 m 的键类型。

delete(m, k)  // remove element m[k] from map m

如果 m 的类型是类型参数,则该类型集中的所有类型都必须是 map 类型,并且它们必须都具有相同的键类型。

如果 map mnil 或元素 m[k] 不存在,delete 是一个空操作。

长度和容量

内建函数 lencap 接受各种类型的参数,并返回类型为 int 的结果。实现保证结果总是能放入 int 类型。

Call      Argument type    Result

len(s)    string type      string length in bytes
          [n]T, *[n]T      array length (== n)
          []T              slice length
          map[K]T          map length (number of defined keys)
          chan T           number of elements queued in channel buffer
          type parameter   see below

cap(s)    [n]T, *[n]T      array length (== n)
          []T              slice capacity
          chan T           channel buffer capacity
          type parameter   see below

如果参数类型是类型参数 P,则调用 len(e)(或相应的 cap(e))必须对 P 的类型集中的每种类型都有效。结果是与其类型参数对应的那种类型参数的长度(或容量,分别地),P 就是用这种类型参数实例化的。

切片的容量是底层数组中分配了空间的元素数量。在任何时候都满足以下关系

0 <= len(s) <= cap(s)

nil 切片、map 或 channel 的长度为 0。nil 切片或 channel 的容量为 0。

如果 s 是字符串常量,则表达式 len(s)常量。如果 s 的类型是数组或指向数组的指针,并且表达式 s 不包含channel 接收或(非常量)函数调用,则表达式 len(s)cap(s) 是常量;在这种情况下,s 不会被求值。否则,调用 lencap 不是常量,并且 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

创建切片、map 和 channel

内建函数 make 接受一个类型 T,后跟一个可选的类型特定的表达式列表。T核心类型必须是 slice、map 或 channel。它返回类型为 T 的值(而不是 *T)。内存按照初始值部分的描述进行初始化。

Call             Core type    Result

make(T, n)       slice        slice of type T with length n and capacity n
make(T, n, m)    slice        slice of type T with length n and capacity m

make(T)          map          map of type T
make(T, n)       map          map of type T with initial space for approximately n elements

make(T)          channel      unbuffered channel of type T
make(T, n)       channel      buffered channel of type T, buffer size n

大小参数 nm 都必须是整数类型,或者具有仅包含整数类型的类型集,或者是一个无类型常量。常量大小参数必须是非负的,并且可以由类型 int 的值表示;如果它是无类型常量,则赋予类型 int。如果同时提供了 nm 且它们是常量,则 n 不得大于 m。对于 slice 和 channel,如果在运行时 n 为负或大于 m,则会发生运行时 panic

s := make([]int, 10, 100)       // slice with len(s) == 10, cap(s) == 100
s := make([]int, 1e3)           // slice with len(s) == cap(s) == 1000
s := make([]int, 1<<63)         // illegal: len(s) is not representable by a value of type int
s := make([]int, 10, 0)         // illegal: len(s) > cap(s)
c := make(chan int, 10)         // channel with a buffer size of 10
m := make(map[string]int, 100)  // map with initial space for approximately 100 elements

使用 map 类型和大小提示 n 调用 make 将创建一个 map,其初始空间可以容纳 n 个 map 元素。具体行为取决于实现。

Min 和 max

内建函数 minmax 分别计算固定数量的有序类型参数中的最小值或最大值。必须至少有一个参数 [Go 1.21]。

适用于运算符的相同类型规则也适用于此:对于有序参数 xy,如果 x + y 有效,则 min(x, y) 有效,并且 min(x, y) 的类型是 x + y 的类型(max 也类似)。如果所有参数都是常量,则结果为常量。

var x, y int
m := min(x)                 // m == x
m := min(x, y)              // m is the smaller of x and y
m := max(x, y, 10)          // m is the larger of x and y but at least 10
c := max(1, 2.0, 10)        // c == 10.0 (floating-point kind)
f := max(0, float32(x))     // type of f is float32
var s []string
_ = min(s...)               // invalid: slice arguments are not permitted
t := max("", "foo", "bar")  // t == "foo" (string kind)

对于数值参数,假设所有 NaN 都相等,minmax 满足交换律和结合律

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    // negative zero is smaller than (non-negative) zero
  -Inf      y         -Inf            y    // negative infinity is smaller than any other number
  +Inf      y            y         +Inf    // positive infinity is larger than any other number
   NaN      y          NaN          NaN    // if any argument is a NaN, the result is a NaN

对于字符串参数,min 的结果是按字节进行字典序比较后值最小的(对于 max 则是值最大的)第一个参数

min(x, y)    == if x <= y then x else y
min(x, y, z) == min(min(x, y), z)

分配

内建函数 new 接受一个类型 T,在运行时为该类型的变量分配存储空间,并返回一个指向该变量的类型为 *T 的值。该变量按照初始值部分的描述进行初始化。

new(T)

例如

type S struct { a int; b float64 }
new(S)

为类型为 S 的变量分配存储空间,初始化它(a=0b=0.0),并返回一个包含该位置地址的类型为 *S 的值。

处理 panic

两个内建函数 panicrecover 协助报告和处理运行时 panic 以及程序定义的错误条件。

func panic(interface{})
func recover() interface{}

在执行函数 F 期间,显式调用 panic 或发生运行时 panic 会终止 F 的执行。然后,F 延迟的所有函数照常执行。接下来,执行 F 的调用者延迟的所有函数,依此类推,直到执行 goroutine 中的顶层函数延迟的所有函数。此时,程序终止并报告错误条件,包括传递给 panic 的参数值。这种终止序列称为发生 panic

panic(42)
panic("unreachable")
panic(Error("cannot parse"))

recover 函数允许程序管理正在发生 panic 的 goroutine 的行为。假设函数 G 延迟了一个调用 recover 的函数 D,并且在与 G 正在执行的同一 goroutine 中的某个函数中发生了 panic。当延迟函数运行到 D 时,D 调用 recover 的返回值将是传递给 panic 调用的值。如果 D 正常返回,没有开始新的 panic,则 panic 序列停止。在这种情况下,在 Gpanic 调用之间调用的函数的状态被丢弃,正常执行恢复。GD 之前延迟的任何函数随后会运行,并且 G 的执行通过返回其调用者而终止。

当 goroutine 没有发生 panic 或 recover 不是由延迟函数直接调用时,recover 的返回值为 nil。相反,如果 goroutine 正在发生 panic 且 recover 由延迟函数直接调用,则 recover 的返回值保证不是 nil。为确保这一点,使用 nil 接口值(或无类型 nil)调用 panic 会导致运行时 panic

下面示例中的 protect 函数调用函数参数 g,并保护调用者免受 g 引发的运行时 panic。

func protect(g func()) {
	defer func() {
		log.Println("done")  // Println executes normally even if there is a panic
		if x := recover(); x != nil {
			log.Printf("run time panic: %v", x)
		}
	}()
	log.Println("start")
	g()
}

启动

当前的实现提供了几个在启动过程中有用的内建函数。这些函数是出于完整性目的而记录的,但不保证会保留在语言中。它们不返回结果。

Function   Behavior

print      prints all arguments; formatting of arguments is implementation-specific
println    like print but prints spaces between arguments and a newline at the end

实现限制:printprintln 不需要接受任意参数类型,但必须支持打印布尔、数值和字符串类型

Go 程序通过链接*包*来构建。一个包又由一个或多个源文件构成,这些源文件共同声明属于该包的常量、类型、变量和函数,并且在同一包的所有文件中都可访问。这些元素可以被导出并在另一个包中使用。

源文件组织

每个源文件包含一个定义其所属包的 package 子句,后跟一组可能为空的 import 声明,声明其希望使用的包的内容,再后跟一组可能为空的函数、类型、变量和常量声明。

SourceFile = PackageClause ";" { ImportDecl ";" } { TopLevelDecl ";" } .

Package 子句

package 子句是每个源文件的开头,并定义文件所属的包。

PackageClause = "package" PackageName .
PackageName   = identifier .

PackageName 不能是空标识符

package math

共享同一 PackageName 的一组文件构成一个包的实现。实现可能会要求一个包的所有源文件位于同一目录中。

Import 声明

import 声明表明包含该声明的源文件依赖于被*导入*包的功能(§程序初始化和执行),并允许访问该包的导出标识符。import 声明指定一个用于访问的标识符(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 子句 package math 的编译包,该包导出了函数 Sin,并将编译包安装在由 "lib/math" 标识的文件中。下表说明了在各种类型的 import 声明之后,导入该包的文件如何访问 Sin

Import declaration          Local name of Sin

import   "lib/math"         math.Sin
import m "lib/math"         m.Sin
import . "lib/math"         Sin

import 声明声明了导入包和被导入包之间的依赖关系。包直接或间接导入自身,或者直接导入一个包而没有引用其任何导出标识符是非法的。为了仅仅因为其副作用(初始化)而导入一个包,请使用标识符作为显式包名

import _ "lib/math"

示例包

这是一个实现并发素数筛的完整 Go 包。

package main

import "fmt"

// Send the sequence 2, 3, 4, … to channel 'ch'.
func generate(ch chan<- int) {
	for i := 2; ; i++ {
		ch <- i  // Send 'i' to channel 'ch'.
	}
}

// Copy the values from channel 'src' to channel 'dst',
// removing those divisible by 'prime'.
func filter(src <-chan int, dst chan<- int, prime int) {
	for i := range src {  // Loop over values received from 'src'.
		if i%prime != 0 {
			dst <- i  // Send 'i' to channel 'dst'.
		}
	}
}

// The prime sieve: Daisy-chain filter processes together.
func sieve() {
	ch := make(chan int)  // Create a new channel.
	go generate(ch)       // Start generate() as a subprocess.
	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,字符串类型为 "",指针、函数、接口、切片、channel 和 map 为 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 and b are initialized together, before x is initialized

出于包初始化的目的,变量在声明中被视同任何其他变量。

在多个文件中声明的变量的声明顺序由文件呈现给编译器的顺序决定:第一个文件中声明的变量在第二个文件中声明的任何变量之前声明,依此类推。为了确保可重现的初始化行为,建议构建系统将属于同一包的多个文件按词法文件名顺序呈现给编译器。

依赖分析不依赖于变量的实际值,仅依赖于源代码中对它们的词法引用,并进行传递性分析。例如,如果变量 x 的初始化表达式引用了一个函数,该函数的函数体引用了变量 y,那么 x 依赖于 y。具体来说

  • 对变量或函数的引用是表示该变量或函数的标识符。
  • 对方法 m 的引用是形式为 t.m方法值方法表达式,其中 t 的(静态)类型不是接口类型,并且方法 mt方法集中。结果函数值 t.m 是否被调用无关紧要。
  • 如果变量、函数或方法 x 的初始化表达式或函数体(对于函数和方法)包含对 y 或依赖于 y 的函数或方法的引用,则 x 依赖于变量 y

例如,给定声明:

var (
	a = c + b  // == 9
	b = f()    // == 4
	c = f()    // == 5
	d = 3      // == 5 after initialization has finished
)

func f() int {
	d++
	return d
}

初始化顺序是 d, b, c, a。请注意,初始化表达式中子表达式的顺序无关紧要:在本例中,a = c + ba = b + c 导致相同的初始化顺序。

依赖分析按包进行;只考虑引用当前包中声明的变量、函数和(非接口)方法的引用。如果变量之间存在其他隐藏的数据依赖关系,则这些变量之间的初始化顺序未指定。

例如,给定声明

var x = I(T{}).ab()   // x has an undetected, hidden dependency on a and b
var _ = sideEffect()  // unrelated to x, a, or 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 之前、在 ba 之间还是在 a 之后初始化,以及因此 sideEffect() 被调用的时刻(在 x 初始化之前或之后)都没有指定。

变量也可以使用在包块中声明的名为 init 的函数进行初始化,这些函数没有参数也没有结果参数。

func init() { … }

每个包可以定义多个这样的函数,即使在单个源文件中也是如此。在包块中,标识符 init 只能用于声明 init 函数,但该标识符本身并未被声明。因此,init 函数不能在程序中的任何地方被引用。

整个包通过为所有包级别变量分配初始值,然后按它们在源代码中出现的顺序(可能在多个文件中,按呈现给编译器的顺序)调用所有 init 函数来初始化。

程序初始化

完整程序的包按步骤初始化,一次一个包。如果一个包有导入,则在初始化该包本身之前先初始化被导入的包。如果多个包导入同一个包,则该被导入包只会被初始化一次。从结构上来说,包的导入保证不会出现循环初始化依赖。更精确地说

给定所有包的列表,按导入路径排序,每一步选择列表中所有已导入包(如果存在)都已初始化的第一个未初始化包进行初始化。重复此步骤,直到所有包都初始化完成。

包初始化——变量初始化和 init 函数的调用——发生在单个 goroutine 中,按顺序,一次一个包。一个 init 函数可以启动其他 goroutine,它们可以与初始化代码并发运行。然而,初始化总是按顺序执行 init 函数:它不会调用下一个 init 函数,直到前一个 init 函数返回。

程序执行

完整的程序是通过链接一个未被导入的、称为主包的包及其所有传递导入的包来创建的。主包必须具有包名 main 并声明一个名为 main 的函数,该函数没有参数也没有返回值。

func main() { … }

程序执行从初始化程序开始,然后调用 main 包中的 main 函数。当该函数调用返回时,程序退出。它不会等待其他(非 main)goroutine 完成。

错误

预声明类型 error 定义为

type error interface {
	Error() string
}

它是表示错误条件的约定接口,nil 值表示没有错误。例如,一个从文件读取数据的函数可能定义为

func Read(f *File, b []byte) (n int, err error)

运行时 panic

执行错误(例如尝试访问数组越界)会触发一个运行时 panic,这等同于使用实现定义的接口类型 runtime.Error 的值调用内建函数panic。该类型满足预声明接口类型error。表示不同运行时错误条件的具体错误值未指定。

package runtime

type Error interface {
	error
	// and perhaps other methods
}

系统考量

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 的类型,反之亦然。在 Pointeruintptr 之间转换的效果取决于实现。

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

函数 AlignofSizeof 接受任何类型的表达式 x,并分别返回一个假想变量 v 的对齐方式或大小,就像 v 是通过 var v = x 声明的一样。

函数 Offsetof 接受一个(可能带括号的)选择器 s.f,它表示由 s*s 表示的结构体的字段 f,并返回该字段相对于结构体地址的字节偏移量。如果 f 是一个嵌入字段,则必须可以通过结构体的字段无需指针间接访问。对于包含字段 f 的结构体 s

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 的)变量具有可变大小。否则大小是常量。如果 AlignofOffsetofSizeof 的参数(或 Offsetof 的选择器表达式 s.f 中的结构体 s)是常量大小的类型,则这些函数的调用是类型为 uintptr 的编译时常量表达式

函数 Addlen 添加到 ptr 并返回更新后的指针 unsafe.Pointer(uintptr(ptr) + uintptr(len)) [Go 1.17]。参数 len 必须是整数类型或无类型常量。常量参数 len 必须可以由类型 int 的值表示;如果它是无类型常量,则赋予类型 int。关于Pointer 有效使用的规则仍然适用。

函数 Slice 返回一个 slice,其底层数组从 ptr 开始,长度和容量为 lenSlice(ptr, len) 等价于

(*[len]ArbitraryType)(unsafe.Pointer(ptr))[:]

特殊情况是,如果 ptrnillen 为零,则 Slice 返回 nil [Go 1.17]。

参数 len 必须是整数类型或无类型常量。常量参数 len 必须是非负的,并且可以由类型 int 的值表示;如果它是无类型常量,则赋予类型 int。在运行时,如果 len 为负,或者 ptrnillen 不为零,则会发生运行时 panic [Go 1.17]。

函数 SliceData 返回 slice 参数的底层数组的指针。如果 slice 的容量 cap(slice) 不为零,则该指针是 &slice[:1][0]。如果 slicenil,则结果为 nil。否则,它是一个指向未指定内存地址的非 nil 指针 [Go 1.20]。

函数 String 返回一个 string 值,其底层字节从 ptr 开始,长度为 len。对参数 ptrlen 的要求与函数 Slice 中的要求相同。如果 len 为零,结果为空字符串 ""。由于 Go 字符串是不可变的,传递给 String 的字节之后不得修改。 [Go 1.20]

函数 StringData 返回字符串参数 str 的底层字节的指针。对于空字符串,返回值未指定,可能为 nil。由于 Go 字符串是不可变的,由 StringData 返回的字节不得修改 [Go 1.20]。

大小和对齐保证

对于数值类型,保证以下大小

type                                 size in bytes

byte, uint8, int8                     1
uint16, int16                         2
uint32, int32, float32                4
uint64, int64, float64, complex64     8
complex128                           16

保证以下最小对齐属性

  1. 对于任何类型的变量 xunsafe.Alignof(x) 至少为 1。
  2. 对于结构体类型的变量 xunsafe.Alignof(x)x 的每个字段 f 的所有 unsafe.Alignof(x.f) 值中的最大值,但至少为 1。
  3. 对于数组类型的变量 xunsafe.Alignof(x) 与该数组元素类型的变量的对齐方式相同。

如果一个结构体或数组类型不包含大小大于零的字段(或元素),则其大小为零。两个不同的零大小变量可能在内存中具有相同的地址。

附录

语言版本

The Go 1 compatibility guarantee ensures that programs written to the Go 1 specification will continue to compile and run correctly, unchanged, over the lifetime of that specification. More generally, as adjustments are made and features added to the language, the compatibility guarantee ensures that a Go program that works with a specific Go language version will continue to work with any subsequent version.

For instance, the ability to use the prefix 0b for binary integer literals was introduced with Go 1.13, indicated by [Go 1.13] in the section on integer literals. Source code containing an integer literal such as 0b1011 will be rejected if the implied or required language version used by the compiler is older than Go 1.13.

The following table describes the minimum language version required for features introduced after Go 1.

Go 1.9

Go 1.13

  • Integer literals may use the prefixes 0b, 0B, 0o, and 0O for binary, and octal literals, respectively.
  • Hexadecimal floating-point literals may be written using the prefixes 0x and 0X.
  • The imaginary suffix i may be used with any (binary, decimal, hexadecimal) integer or floating-point literal, not just decimal literals.
  • The digits of any number literal may be separated (grouped) using underscores _.
  • The shift count in a shift operation may be a signed integer type.

Go 1.14

Go 1.17

  • A slice may be converted to an array pointer if the slice and array element types match, and the array is not longer than the slice.
  • The built-in package unsafe includes the new functions Add and Slice.

Go 1.18

The 1.18 release adds polymorphic functions and types ("generics") to the language. Specifically

Go 1.20

  • A slice may be converted to an array if the slice and array element types match and the array is not longer than the slice.
  • The built-in package unsafe includes the new functions SliceData, String, and StringData.
  • Comparable types (such as ordinary interfaces) may satisfy comparable constraints, even if the type arguments are not strictly comparable.

Go 1.21

  • The set of predeclared functions includes the new functions min, max, and clear.
  • Type inference uses the types of interface methods for inference. It also infers type arguments for generic functions assigned to variables or passed as arguments to other (possibly generic) functions.

Go 1.22

  • In a "for" statement, each iteration has its own set of iteration variables rather than sharing the same variables in each iteration.
  • A "for" statement with "range" clause may iterate over integer values from zero to an upper limit.

Go 1.23

  • A "for" statement with "range" clause accepts an iterator function as range expression.

Go 1.24

Type unification rules

The type unification rules describe if and how two types unify. The precise details are relevant for Go implementations, affect the specifics of error messages (such as whether a compiler reports a type inference or other error), and may explain why type inference fails in unusual code situations. But by and large these rules can be ignored when writing Go code: type inference is designed to mostly "work as expected", and the unification rules are fine-tuned accordingly.

Type unification is controlled by a matching mode, which may be exact or loose. As unification recursively descends a composite type structure, the matching mode used for elements of the type, the element matching mode, remains the same as the matching mode except when two types are unified for assignability (A): in this case, the matching mode is loose at the top level but then changes to exact for element types, reflecting the fact that types don't have to be identical to be assignable.

Two types that are not bound type parameters unify exactly if any of following conditions is true

  • Both types are identical.
  • Both types have identical structure and their element types unify exactly.
  • Exactly one type is an unbound type parameter with a core type, and that core type unifies with the other type per the unification rules for A (loose unification at the top level and exact unification for element types).

If both types are bound type parameters, they unify per the given matching modes if

  • Both type parameters are identical.
  • At most one of the type parameters has a known type argument. In this case, the type parameters are joined: they both stand for the same type argument. If neither type parameter has a known type argument yet, a future type argument inferred for one the type parameters is simultaneously inferred for both of them.
  • Both type parameters have a known type argument and the type arguments unify per the given matching modes.

A single bound type parameter P and another type T unify per the given matching modes if

  • P doesn't have a known type argument. In this case, T is inferred as the type argument for P.
  • P does have a known type argument A, A and T unify per the given matching modes, and one of the following conditions is true
    • Both A and T are interface types: In this case, if both A and T are also defined types, they must be identical. Otherwise, if neither of them is a defined type, they must have the same number of methods (unification of A and T already established that the methods match).
    • Neither A nor T are interface types: In this case, if T is a defined type, T replaces A as the inferred type argument for P.

Finally, two types that are not bound type parameters unify loosely (and per the element matching mode) if

  • Both types unify exactly.
  • One type is a defined type, the other type is a type literal, but not an interface, and their underlying types unify per the element matching mode.
  • Both types are interfaces (but not type parameters) with identical type terms, both or neither embed the predeclared type comparable, corresponding method types unify exactly, and the method set of one of the interfaces is a subset of the method set of the other interface.
  • Only one type is an interface (but not a type parameter), corresponding methods of the two types unify per the element matching mode, and the method set of the interface is a subset of the method set of the other type.
  • Both types have the same structure and their element types unify per the element matching mode.