2023年8月2日版本
介绍
这是 Go 编程语言的参考手册。不包含泛型的 Go1.18 之前的版本可以在这里找到。更多信息和其他文档,请参见golang.org。
Go 是一种以系统编程为目标的通用语言。它是强类型的、具有垃圾回收功能,并且明确支持并发编程。程序由_包_构建,其属性允许有效管理依赖关系。
语法紧凑且简单易解析,便于集成开发环境等自动化工具进行轻松分析。
注释
语法是使用扩展的巴科斯-瑙尔范式(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 "}" .
Productions are expressions constructed from terms and the following operators, in increasing precedence:
| 交替 () 分组 [] 选项 (0 或 1 次) {} 重复 (0 到 n 次)
小写的产品名称用于识别词法(终端)标记。非终结符采用驼峰命名法。词法标记用双引号 ""
或反引号 ``
括起来。
表达式a … b
表示从a
到b
的字符集作为备选。水平省略号…
也在规范的其他地方被用来非正式地表示各种枚举或代码片段,而不做进一步的说明。字符…
(而不是三个字符...
)不是 Go 语言的标记。
源代码表示
源代码是以UTF-8编码的Unicode文本。文本没有规范化,因此单个带重音的代码点与由重音和字母组合而成的相同字符是不同的;它们被视为两个代码点。为简单起见,本文档将使用不带限定的术语“字符”来指代源文本中的Unicode代码点。
每个代码点都是独特的;例如,大写和小写字母是不同的字符。
实现限制:为了与其他工具兼容,编译器可能会禁止在源代码中使用NUL字符(U+0000)。
实现限制:为了与其他工具兼容,编译器可能会忽略UTF-8编码的字节顺序标记(U+FEFF),如果它是源文本中的第一个Unicode代码点。在源代码的其他位置,字节顺序标记可能会被禁止使用。
角色
以下术语用于表示特定的Unicode字符类别:
newline = /* Unicode代码点U+000A / . unicode_char = / 除换行符外的任意Unicode代码点 / . unicode_letter = / 被归类为"字母"的Unicode代码点 / . unicode_digit = / 被归类为"数字,十进制数字"的Unicode代码点 */ .
在Unicode标准8.0中,第4.5节“通用类别”定义了一组字符类别。Go语言将所有属于字母类别Lu、Ll、Lt、Lm或Lo的字符视为Unicode字母,将数字类别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" .
词法元素
注释用作程序文档。有两种形式:
- 行注释 以字符序列
//
开始,并在行尾结束。 - 一般注释 以字符序列
/*
开始,并在遇到第一个后续字符序列*/
时结束。
不能在rune或string literal内部,或在注释内部开始注释。不含换行符的一般注释起到空格的作用。其他任何注释起到换行符的作用。
令牌
标记是 Go 语言的词汇。有四类:标识符、关键字、运算符和标点符号 和 字面量。空白字符 由空格(U+0020)、水平制表符(U+0009)、回车(U+000D)和换行符(U+000A)组成,除了用于分隔本应合并为单个标记的标记之外,都会被忽略。此外,换行符或文件结尾可能会触发插入 分号。在将输入分解为标记时,下一个标记是形成有效标记的最长字符序列。
分号
正式语法在许多产生式中使用分号 ";"
作为终结符。Go 程序可以遵循以下两条规则省略大部分这些分号:
- 当输入被分解为标记时,如果该标记是 标识符 、整数 、浮点数 、虚数 、符文 或 字符串 文本,或者是 break 、continue 、fallthrough 或 return 关键字之一,或者是 ++ 、-- 、) 、] 或 } 之一的 运算符和标点 ,则在该行的最后一个标记之后会自动插入一个分号到标记流中。2. 为了允许复杂语句占据一行,可以在闭合的
)
或}
前省略分号。
为了反映惯用法,本文档中的代码示例省略分号,遵循这些规则。
标识符
标识符用于命名程序实体,如变量和类型。标识符是一个由一个或多个字母和数字组成的序列。标识符的第一个字符必须是字母。
identifier = 字母 { 字母 | 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
运算符和标点符号
- & += &= && == != ( )
- | -= |= || < <= [ ]
- ^ *= ^= <- > >= { } / << /= <<= ++ = := , ; % >> %= >>= -- ! ... . : &^ &^= ~
整数字面量
整数字面量是表示整数常量的数字序列。可选的前缀设置为非十进制基数:0b
或0B
表示二进制,0
、0o
或0O
表示八进制,0x
或0X
表示十六进制。单个0
被视为十进制零。在十六进制字面量中,字母a
到f
和A
到F
表示值10到15。
为了提高可读性,在基本前缀后或连续数字之间可以出现下划线字符 _
;这些下划线不会改变文字的值。
int_lit = 十进制字面量 | 二进制字面量 | 八进制字面量 | 十六进制字面量 . decimal_lit = "0" | ( "1" … "9" ) [ [ "" ] 十进制数字 ] . binary_lit = "0" ( "b" | "B" ) [ "" ] 二进制数字 . octal_lit = "0" [ "o" | "O" ] [ "" ] 八进制数字 . hex_lit = "0" ( "x" | "X" ) [ "" ] 十六进制数字 .
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 0xBadFace 0xBad_Face 0x_67_7a_2f_cc_40_c6 170141183460469231731687303715884105727 170_141183_460469_231731_687303_715884_105727
42 // 标识符,而非整数字面值 42 // 无效:必须分隔连续的数字 4__2 // 无效:一次只能有一个 0_xBadFace // 无效:_必须分隔连续的数字
浮点数文字
浮点字面量是浮点常量的十进制或十六进制表示。
十进制浮点数字面量由整数部分(十进制数字)、小数点、小数部分(十进制数字)和指数部分(e
或E
后面跟着一个可选的符号和十进制数字)组成。整数部分或小数部分中的一个可以省略;小数点或指数部分中的一个可以省略。指数值 exp 通过 10exp 缩放尾数(整数部分和小数部分)。
十六进制浮点数文字由0x
或0X
前缀、整数部分(十六进制数字)、基数点、小数部分(十六进制数字)和指数部分(p
或P
后跟可选的符号和十进制数字)组成。整数部分或小数部分中的一个可以省略;基数点也可以省略,但指数部分是必需的。(此语法与IEEE 754-2008 §5.12.3中给出的语法相匹配。)指数值exp将尾数(整数部分和小数部分)按2exp进行缩放。
为了提高可读性,在基本前缀后或连续数字之间可以出现下划线字符 _
;这些下划线不会改变文字的字面值。
decimal_float_lit = 十进制数字 "." [ 十进制数字 ] [ 十进制指数 ] | 十进制数字 十进制指数 | "." 十进制数字 [ 十进制指数 ] . decimal_exponent = ( "e" | "E" ) [ "+" | "-" ] 十进制数字 .
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 .
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 // 无效:尾数没有数字 1p-2 // 无效:p 指数需要十六进制尾数 0x1.5e-2 // 无效:十六进制尾数需要 p 指数 1_.5 // 无效:_ 必须分隔连续的数字 1.5 // 无效: 必须分隔连续的数字 1.5_e1 // 无效:_ 必须分隔连续的数字 1.5e_1 // 无效:_ 必须分隔连续的数字 1.5e1_ // 无效:_ 必须分隔连续的数字
虚数字面量
虚部字面值表示复数常量的虚部。它由整数或浮点数字面值后跟小写字母 i
组成。虚部字面值的值是相应整数或浮点数字面值乘以虚数单位 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 常量,一个标识 Unicode 代码点的整数值。rune 字面值表示为一个或多个字符,用单引号括起来,例如'x'
或' '
。在引号内,除了换行符和未转义的单引号之外,任何字符都可以出现。单引号字符表示字符本身的 Unicode 值,而以反斜杠开头的多字符序列表示以各种格式编码的值。
最简单的形式表示引号内的单个字符;由于 Go 源文本是以 UTF-8 编码的 Unicode 字符,多个 UTF-8 编码的字节可能表示单个整数值。例如,字面量 a
包含一个表示字面量 a
,Unicode U+0061,值 0x61
的单个字节,而'ä'
包含两个字节(0xc3
0xa4
)表示字面量 a
-dieresis,U+00E4,值 0xe4
。
几个反斜杠转义允许将任意值编码为ASCII文本。有四种方法可以将整数值表示为数字常量:
后面跟着确切的两个十六进制数字;
后面跟着确切的四个十六进制数字;
后面跟着确切的八个十六进制数字,以及一个普通的反斜杠
后面跟着确切的三个八进制数字。在每种情况下,文字的值是由相应基数中的数字表示的值。
尽管这些表示都会得到一个整数,但它们的有效范围是不同的。八进制转义必须表示一个介于0和255之间的值。十六进制转义通过构造满足这个条件。转义符\u
和\U
表示Unicode代码点,因此其中一些值是非法的,特别是那些大于0x10FFFF
和代理半对。
在反斜杠后,某些单字符转义代表特殊值:
\a U+0007 警报或响铃 \b U+0008 退格 \f U+000C 换页 \n U+000A 换行 \r U+000D 回车 \t U+0009 水平制表符 \v U+000B 垂直制表符 \ U+005C 反斜杠 ' U+0027 单引号(仅在rune文字中有效) " U+0022 双引号(仅在字符串文字中有效)
在符文文字面值中,反斜杠后面的未识别字符是非法的。
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' ''' 'aa' '\k' '\xa' '\0' '\400' '\uDFFF' '\U00110000'
字符串字面量
字符串字面量表示从连接字符序列获得的字符串常量。有两种形式:原始字符串字面量和解释字符串字面量。
原始字符串字面量是反引号之间的字符序列,如`foo`
。在引号内,除了反引号之外,任何字符都可以出现。原始字符串字面量的值是由引号之间未解释的(隐式UTF-8编码的)字符组成的字符串;特别地,反斜杠没有特殊含义,字符串可以包含换行符。原始字符串字面量中的回车符('\r')会从原始字符串值中丢弃。
解释的字符串字面量是双引号之间的字符序列,如"bar"
。在引号内,除了换行符和未转义的双引号之外,任何字符都可以出现。引号之间的文本形成字面量的值,反斜杠转义被解释为rune literals中的方式(除了'
是非法的,"
是合法的),具有相同的限制。三位八进制( nn
)和两位十六进制(
)转义表示生成字符串的单个字节;所有其他转义表示单个字符的(可能是多字节的)UTF-8编码。因此,在字符串字面量内部, 377
和 xFF
表示值为0xFF
(即255)的单个字节,而ÿ
、 00FF
、 U000000FF
和 c3xbf
表示字符U+00FF的UTF-8编码的两个字节0xc3
0xbf
。
string_lit = 原始字符串文本 | 解释字符串文本 .
raw_string_lit = "" { [unicode字符](#unicode%5Fchar) | [换行符](#newline) } "
" .
interpreted_string_lit = "
{ unicode值 | 字节值 } "
.
abc
// 和 "abc" 一样
\n\n
// 和 "\n\n\n" 一样
"\n"
""" // 和 "
一样
"Hello, world!\n"
"日本語"
"\u65e5本\U00008a9e"
"\xff\u00FF"
"\uD800" // 非法: 代理半对
"\U00110000" // 非法: 无效的 Unicode 代码点
这些示例都代表相同的字符串:
"日本語" // UTF-8 输入文本
日本語
// 作为原始文本的 UTF-8 输入文本
"\u65e5\u672c\u8a9e" // 显式的 Unicode 代码点
"\U000065e5\U0000672c\U00008a9e" // 显式的 Unicode 代码点
"\xe6\x97\xa5\xe6\x9c\xac\xe8\xaa\x9e" // 显式的 UTF-8 字节
如果源代码将一个字符表示为两个代码点,比如涉及重音和字母的组合形式,如果将其放置在rune文字中,结果将会出现错误(因为它不是单个代码点),如果将其放置在字符串文字中,将会显示为两个代码点。
常量
有_布尔常量_,符文常量,整数常量,浮点常量,复数常量_和_字符串常量。符文、整数、浮点和复数常量统称为_数值常量_。
常量值由rune、integer、floating-point、imaginary或string文本、表示常量的标识符、常量表达式、结果为常量的转换、或应用于常量参数的内置函数(如min
或max
)、应用于特定值的unsafe.Sizeof
、应用于某些表达式的cap
或len
、应用于复数常量的real
和imag
,以及应用于数值常量的complex
。布尔真值由预声明的常量true
和false
表示。预声明的标识符iota表示整数常量。
通常,复杂常量是常量表达式的一种形式,在该部分进行了讨论。
数值常量表示任意精度的精确值,不会溢出。因此,没有常量表示IEEE-754的负零、无穷大和非数值。
常量可以通过常量声明或转换显式地给定类型,也可以在变量声明、赋值语句中隐式地给定类型,或者作为表达式中的操作数。如果常量值不能作为相应类型的值进行表示,则会出现错误。如果类型是类型参数,则将常量转换为类型参数的非常量值。
无类型常量具有默认类型,这是常量在需要类型化值的上下文中隐式转换的类型,例如,在短变量声明中,如i := 0
,其中没有显式类型。无类型常量的默认类型分别是bool
、rune
、int
、float64
、complex128
或string
,具体取决于它是布尔值、符文、整数、浮点数、复数还是字符串常量。
实现限制: 虽然该语言中的数字常量具有任意精度,但编译器可能使用内部表示来实现它们,其精度受限。也就是说,每个实现必须:
- 用至少 256 位表示整数常量。
- 用至少 256 位的尾数和至少 16 位的有符号二进制指数表示浮点常量,包括复数常量的部分。
- 如果无法精确表示整数常量,则报错。
- 如果由于溢出而无法表示浮点或复数常量,则报错。
- 如果由于精度限制而无法表示浮点或复数常量,则四舍五入到最接近的可表示常量。
这些要求适用于字面常量和评估常量表达式的结果。
变量
变量是用于存储数值的存储位置。可取值的范围由变量的_类型_确定。
变量声明或者对于函数参数和结果,函数声明或函数文字的签名保留了命名变量的存储空间。调用内置函数new或者取地址复合文字在运行时为变量分配存储空间。这样的匿名变量通过(可能是隐式的)指针间接引用。
变量的_static type_(或者只是_type_)是在其声明中给定的类型,在new
调用或复合文字中提供的类型,或者结构化变量的元素的类型。接口类型的变量也有一个独特的_dynamic type_,这是在运行时分配给变量的值的(非接口)类型(除非该值是预声明的标识符nil
,它没有类型)。动态类型可能在执行过程中变化,但存储在接口变量中的值始终可以分配给变量的静态类型。
var x interface{} // x 是 nil,静态类型为 interface{} var v *T // v 是 nil,静态类型为 *T x = 42 // x 有值 42,动态类型为 int x = v // x 有值 (*T)(nil),动态类型为 *T
类型
类型确定了一组值,以及针对这些值特定的操作和方法。如果类型有类型名称,可以用类型名称表示类型,必须在类型名称后面跟上type arguments(如果类型是泛型的话)。类型也可以使用类型文字来指定,类型文字由现有类型组成。
Type = TypeName [ TypeArgs ] | TypeLit | "(" Type ")" . TypeName = identifier | QualifiedIdent . TypeArgs = "[" TypeList [ "," ] "]" . TypeList = Type { "," Type } . TypeLit = ArrayType | StructType | PointerType | FunctionType | InterfaceType | SliceType | MapType | ChannelType .
预声明类型、定义类型和类型参数被称为 命名类型。如果别名声明中给定的类型是命名类型,则该别名表示一个命名类型。
布尔类型
boolean type 代表由预声明常量 true
和 false
表示的布尔真值集。预声明的布尔类型是 bool
;它是一个定义类型。
数值类型
整数、浮点数_或_复数_类型分别表示整数、浮点数或复数值的集合。它们统称为_数值类型。预声明的与体系结构无关的数值类型包括:
uint8 所有无符号 8 位整数的集合(0 到 255) uint16 所有无符号 16 位整数的集合(0 到 65535) uint32 所有无符号 32 位整数的集合(0 到 4294967295) uint64 所有无符号 64 位整数的集合(0 到 18446744073709551615)
int8 所有有符号 8 位整数的集合(-128 到 127) int16 所有有符号 16 位整数的集合(-32768 到 32767) int32 所有有符号 32 位整数的集合(-2147483648 到 2147483647) int64 所有有符号 64 位整数的集合(-9223372036854775808 到 9223372036854775807)
float32 所有IEEE-754 32位浮点数的集合 float64 所有IEEE-754 64位浮点数的集合
complex64 所有具有float32实部和虚部的复数的集合 complex128 所有具有float64实部和虚部的复数的集合
byte 别名为 uint8 rune 别名为 int32
一个 n 位整数的值是 n 位宽,并使用二进制补码算术表示。
还有一组具有实现特定大小的预声明整数类型:
uint 32位或64位之一 int 与uint大小相同 uintptr 一个足够大以存储指针值未解释位的无符号整数
为避免可移植性问题,所有数字类型都是定义类型,因此是不同的,除了byte
,它是uint8
的别名,以及rune
,它是int32
的别名。在表达式或赋值中混合不同的数字类型时,需要进行显式转换。例如,int32
和int
虽然在特定架构上可能具有相同的大小,但它们并不是相同的类型。
字符串类型
字符串类型代表字符串值的集合。字符串值是一系列(可能为空的)字节。字节数称为字符串的长度,永远不会是负数。字符串是不可变的:一旦创建,就无法更改字符串的内容。预声明的字符串类型是 string
;它是一个定义类型。
字符串s
的长度可以通过内置函数len来获取。如果字符串是常量,长度就是一个编译时常量。可以通过整数索引0到len(s)-1
来访问字符串的字节。对这样的元素取地址是非法的;如果s[i]
是字符串的第i
个字节,那么&s[i]
是无效的。
数组类型
数组是同一类型元素的编号序列,称为元素类型。元素的数量称为数组的长度,永远不会是负数。
ArrayType = "[" ArrayLength "]" ElementType . ArrayLength = Expression . ElementType = Type .
长度是数组类型的一部分;它必须评估为非负的int常量可表示的int
类型的值。可以使用内置函数len来发现数组a
的长度。元素可以通过整数索引0到len(a)-1
进行访问。数组类型始终是一维的,但可以组合成多维类型。
[32]byte [2*N] 结构体 { x, y int32 } [1000]*float64 [3][5]int [2][2][2]float64 // 与 2 相同
数组类型 T
不能直接或间接地包含类型为 T
或包含 T
作为组件的类型的元素,如果这些包含类型只是数组类型或结构类型。
// 无效的数组类型 类型 ( T1 [10]T1 // T1 的元素类型是 T1 T2 [10]struct{ f T2 } // T2 包含 T2 作为结构体的组成部分 T3 [10]T4 // T3 包含 T3 作为 T4 结构体中的组成部分 T4 struct{ f T3 } // T4 包含 T4 作为结构体中数组 T3 的组成部分 )
// 有效的数组类型 类型 ( T5 [10]*T5 // T5 包含 T5 作为指针的组件 T6 [10]func() T6 // T6 包含 T6 作为函数类型的组件 T7 [10]struct{ f []T7 } // T7 包含 T7 作为结构体中切片的组件 )
切片类型
切片是对底层数组的连续片段的描述符,并提供对该数组中编号顺序的元素序列的访问。切片类型表示其元素类型的所有数组切片的集合。元素的数量称为切片的长度,永远不会是负数。未初始化切片的值为 nil
。
SliceType = "[" "]" ElementType .
一旦初始化,切片始终与包含其元素的基础数组相关联。因此,切片与其数组以及同一数组的其他切片共享存储;相比之下,不同的数组始终代表不同的存储。
切片底层数组可能延伸到切片末尾之外。_容量_是对这种延伸的度量:它是切片长度和切片末尾数组长度的总和;可以通过从原始切片中切片创建长度不超过该容量的新切片。可以使用内置函数 cap(a) 来发现切片 a
的容量。
使用内置函数make可以为给定的元素类型T
创建一个新的初始化切片值,该函数接受切片类型和指定长度以及可选容量的参数。使用make
创建的切片总是分配一个新的隐藏数组,返回的切片值指向该数组。也就是说,执行
make([]T, 长度, 容量)
分配一个数组并对其进行切片产生与切片表达式相同的切片,因此这两个表达式是等价的:
make([]int, 50, 100) new([100]int)[0:50]
与数组一样,切片始终是一维的,但可以组合成构建更高维对象。对于数组的数组,内部数组始终是相同长度;然而对于切片的切片(或者切片的数组),内部长度可以动态变化。此外,内部切片必须单独初始化。
结构类型
结构体是一系列命名元素的序列,称为字段,每个字段都有一个名称和一个类型。字段名称可以明确指定(IdentifierList),也可以隐式指定(EmbeddedField)。在结构体内,非空字段名称必须是唯一的(#Blank%5Fidentifier)。
StructType = "struct" "{" { FieldDecl ";" } "}" . FieldDecl = (IdentifierList Type | EmbeddedField) [ Tag ] . EmbeddedField = [ "*" ] TypeName [ TypeArgs ] . Tag = string_lit .
// 一个空的结构体。 struct {}
// 一个有6个字段的结构体。 struct { x, y int u float32 _ float32 // 填充 A *[]int F func() }
使用类型声明但没有显式字段名称的字段称为 嵌入字段。嵌入字段必须指定为类型名称 T
或非接口类型名称 *T
的指针,并且 T
本身不能是指针类型。未经限定的类型名称充当字段名称。
// 一个包含四个嵌入字段的结构,类型分别为 T1、*T2、P.T3 和 *P.T4 struct { T1 // 字段名为 T1 *T2 // 字段名为 T2 P.T3 // 字段名为 T3 *P.T4 // 字段名为 T4 x, y int // 字段名分别为 x 和 y }
以下声明是非法的,因为在结构类型中字段名称必须是唯一的:
struct { 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 }
提升字段的作用类似于结构体的普通字段,只是它们不能在该结构体的复合文法中用作字段名。
给定一个结构类型 S
和一个 命名类型 T
,推广的方法将按以下方式包含在结构的方法集中:
- 如果
S
包含嵌入字段T
,则S
和*S
的方法集都包括以接收者T
为接收者的提升方法。*S
的方法集还包括以接收者*T
为接收者的提升方法。 - 如果
S
包含嵌入字段*T
,则S
和*S
的方法集都包括以接收者T
或*T
为接收者的提升方法。
字段声明后面可以跟一个可选的字符串字面量 tag,它将成为相应字段声明中所有字段的属性。空标签字符串等同于不存在的标签。这些标签通过 reflection interface 可见,并参与结构体的 type identity,但在其他情况下会被忽略。
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" }
// 与 TimeStamp 协议缓冲区对应的结构。
// 标签字符串定义协议缓冲区字段编号;
// 它们遵循 reflect 包概述的约定。
struct {
microsec uint64 protobuf:"1"
serverIP6 uint64 protobuf:"2"
}
结构类型 T
不得直接或间接包含类型为 T
或包含 T
作为组件的类型的字段,如果这些包含类型只是数组类型或结构类型。
// 无效的结构类型 类型( T1 结构{ T1 } // T1 包含一个 T1 类型的字段 T2 结构{ f [10]T2 } // T2 包含 T2 作为数组的组成部分 T3 结构{ T4 } // T3 包含 T3 作为结构 T4 中数组的组成部分 T4 结构{ f [10]T3 } // T4 包含 T4 作为数组中结构 T3 的组成部分 )
// 有效的结构类型 类型( T5 结构{ f *T5 } // T5 包含 T5 作为指针的组件 T6 结构{ f func() T6 } // T6 包含 T6 作为函数类型的组件 T7 结构{ f [10][]T7 } // T7 包含 T7 作为数组中切片的组件 )
指针类型
指针类型表示指向给定类型的变量的所有指针的集合,称为指针的基本类型。未初始化指针的值为nil
。
*点 *[4]int
函数类型
函数类型表示具有相同参数和结果类型的所有函数的集合。函数类型的未初始化变量的值为 nil
。
FunctionType = "func" Signature . Signature = Parameters [ Result ] . Result = Parameters | Type . Parameters = "(" [ ParameterList [ "," ] ] ")" . ParameterList = ParameterDecl { "," ParameterDecl } . ParameterDecl = [ IdentifierList ] [ "..." ] Type .
在参数或结果列表中,名称(IdentifierList)要么全部存在,要么全部不存在。如果存在,每个名称代表指定类型的一个项目(参数或结果),签名中所有非blank的名称必须是unique的。如果不存在,每种类型代表该类型的一个项目。参数和结果列表始终带括号,除非只有一个未命名的结果,可以将其写为无括号类型。
函数签名中的最后一个传入参数可能带有以 ...
为前缀的类型。具有这种参数的函数称为 可变参数函数,可以使用零个或多个参数调用该参数。
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 .
接口类型由_interface elements_列表指定。接口元素可以是_method_或_type element_,其中type element是一个或多个_type terms_的联合体。_Type term_可以是单一类型或单一基础类型。
基本界面
在其最基本的形式中,接口指定了一个(可能为空的)方法列表。由这种接口定义的类型集是实现所有这些方法的类型集,相应的方法集正好由接口指定的方法组成。可以完全由方法列表定义其类型集的接口称为_基本接口。_
// 一个简单的文件接口。 interface { Read([]byte) (int, error) Write([]byte) (int, error) Close() error }
interface { String() string String() string // 非法: String 不唯一 _(x int) // 非法: 方法必须有非空白名称 }
一个接口可以由多种类型实现。例如,如果两种类型 S1
和 S2
都有方法 set
func (p T) Read(p []byte) (n int, err error) func (p T) Write(p []byte) (n int, err error) func (p T) Close() error
(其中T
代表S1
或S2
)那么File
接口将被S1
和S2
同时实现,而不管S1
和S2
可能具有或共享的其他方法是什么。
每个作为接口类型集成员的类型都实现了该接口。任何给定类型都可以实现多个不同的接口。例如,所有类型都实现了_empty interface_,它代表了所有(非接口)类型的集合:
接口{}
为了方便起见,预声明类型 any
是空接口的别名。
类似地,考虑一下这个接口规范,它出现在type declaration中,用于定义一个名为Locker
的接口:
type Locker interface { Lock() Unlock() }
如果 S1
和 S2
也实现
func (p T) Lock() { … } func (p T) Unlock() { … }
它们实现了Locker
接口和File
接口。
嵌入式接口
在稍微一般化的形式中,接口 T
可能使用(可能带限定的)接口类型名 E
作为接口元素。这称为在 T
中_嵌入_接口 E
。T
的类型集是由 T
明确定义的方法的类型集和 T
嵌入接口的类型集的_交集_。换句话说,T
的类型集是实现了 T
所有明确定义方法的所有类型的集合,同时也实现了E
的所有方法的集合。
type Reader interface { Read(p []byte) (n int, err error) Close() error }
type Writer interface { Write(p []byte) (n int, err error) Close() error }
// ReadWriter 的方法有 Read、Write 和 Close。
类型 ReadWriter 接口 { Reader // 在 ReadWriter 的方法集中包含 Reader 的方法 Writer // 在 ReadWriter 的方法集中包含 Writer 的方法 }
在嵌入接口时,具有相同名称的方法必须具有相同的签名。
type ReadCloser 接口 { Reader // 包括 ReadCloser 方法集中的 Reader 方法 Close() // 非法:Reader.Close 和 Close 的签名不同 }
通用界面
在它们最一般的形式中,接口元素也可以是任意类型项 T
,或者形式为 ~T
的项,指定底层类型 T
,或者项的并集 t1|t2|…|tn
。连同方法规范,这些元素使得可以精确定义接口的类型集合,如下所示:
- 空接口的类型集是所有非接口类型的集合。
- 非空接口的类型集是其接口元素类型集的交集。
- 方法规范的类型集是包含该方法的所有非接口类型的集合。
- 非接口类型项的类型集仅包括该类型。
- 形式为
~T
的项的类型集是所有底层类型为T
的类型的集合。 - 项的_并集_
t1|t2|…|tn
的类型集是这些项的类型集的并集。
量化"所有非接口类型的集合"不仅指手头程序中声明的所有(非接口)类型,还包括所有可能程序中的所有可能类型,因此是无限的。同样,给定实现特定方法的所有非接口类型的集合,这些类型的方法集的交集将恰好包含该方法,即使手头程序中的所有类型总是将该方法与另一个方法配对。
按照构造,接口的类型集永远不包含接口类型。
// 代表只有 int 类型的接口。 interface { int }
// 代表具有基础类型 int 的所有类型的接口。 interface { ~int }
// 表示所有具有基础类型 int 并实现 String 方法的类型的接口。 interface { ~int String() string }
// 表示一个空类型集的接口:既不是 int 也不是 string 类型。 interface { int string }
在形式为~T
的术语中,T
的基础类型必须是它本身,T
不能是一个接口。
type MyInt int
interface { ~[]byte // []byte 的底层类型是它本身 ~MyInt // 非法:MyInt 的底层类型不是 MyInt ~error // 非法:error 是一个接口 }
Union elements denote unions of type sets:
// Float 接口表示所有浮点类型(包括任何基础类型为 float32 或 float64 的命名类型)。
类型 Float 接口 { ~float32 | ~float64 }
在形式为 T
或 ~T
的术语中,类型 T
不能是type parameter ,并且所有非接口术语的类型集必须两两不相交(类型集的两两交集必须为空)。给定类型参数 P
:
interface { P // 非法: P 是类型参数 int | ~P // 非法: P 是类型参数 ~int | MyInt // 非法: ~int 和 MyInt 的类型集不是不相交的(~int 包括 MyInt) float32 | Float // 重叠的类型集但 Float 是一个接口 }
实现限制:联合(具有多个项)不能包含预声明标识符 comparable
或指定方法的接口,或者嵌入指定方法的 comparable
或接口。
不是基本接口的接口只能用作类型约束,或者作为其他接口的元素用作约束。它们不能作为值或变量的类型,也不能作为其他非接口类型的组成部分。
var x Float // 非法: Float 不是基本接口
var x interface{} = Float(nil) // 非法
type Floatish struct { f Float // 非法 }
接口类型 T
不能嵌入一个包含、包含或嵌入 T
的类型元素,直接或间接地。
// 非法: Bad 不能嵌入自身 类型 Bad 接口 { Bad }
// 非法: Bad1 不得使用 Bad2 嵌入自身 type Bad1 interface { Bad2 } type Bad2 interface { Bad1 }
// 非法: Bad3 不能嵌入包含 Bad3 的联合
// 非法: Bad4 不能嵌入包含 Bad4 作为元素类型的数组 类型 Bad4 接口 { [10]Bad4 }
实现接口
类型 T
如果实现了接口 I
,那么
T
不是一个接口,而是类型集合I
的元素;或者T
是一个接口,且类型集合T
是类型集合I
的子集。
类型 T
的值实现一个接口,如果 T
实现了该接口。
地图类型
地图是一种无序的元素组,称为元素类型,由另一种类型的唯一键索引,称为键类型。未初始化地图的值为nil
。
MapType = "map" "[" KeyType "]" ElementType . KeyType = Type .
map[string]int map[*T]struct{ x, y float64 } map[string]interface{}
地图元素的数量称为其长度。对于地图 m
,可以使用内置函数 len 发现它,并且在执行过程中可能会发生变化。元素可以在执行过程中使用 assignments 添加,并使用index expressions检索;它们可以使用内置函数 delete 和 clear 进行删除。
使用内置函数make可以创建一个新的空映射值,该函数接受映射类型和可选的容量提示作为参数:
make(map[string]int) make(map[string]int, 100)
初始容量不限制其大小:映射会根据其中存储的项目数量进行扩展,除了nil
映射。nil
映射等同于空映射,只是不能添加任何元素。
频道类型
ChannelType = ( "chan" | "chan" "<-" | "<-" "chan" ) ElementType .
chan T // 可用于发送和接收类型为 T 的值 chan<- float64 // 只能用于发送 float64 <-chan int // 只能用于接收 int
<-
运算符与最左边的 chan
可能关联:
chan<- chan int // 与 chan<- (chan int) 相同 chan<- <-chan int // 与 chan<- (<-chan int) 相同 <-chan <-chan int // 与 <-chan (<-chan int) 相同 chan (<-chan int)
可以使用内置函数make来创建一个新的初始化通道值,该函数接受通道类型和可选的 capacity 作为参数:
make(chan int, 100)
容量以元素数量表示,设置了通道中缓冲区的大小。如果容量为零或不存在,则通道是无缓冲的,只有在发送方和接收方都准备就绪时通信才会成功。否则,通道是有缓冲的,只要缓冲区不满(发送)或不为空(接收),通信就会成功而不会阻塞。nil
通道永远不会准备好进行通信。
单个通道可以在发送语句、接收操作和调用内置函数cap和len时,由任意数量的goroutine使用,无需进一步同步。通道充当先进先出队列。例如,如果一个goroutine在通道上发送值,而第二个goroutine接收它们,那么这些值将按发送顺序接收。
类型和值的属性
底层类型
每种类型 T
都有一个 底层类型:如果 T
是预声明的布尔、数值或字符串类型之一,或者是类型字面量,那么相应的底层类型就是 T
本身。否则,T
的底层类型就是 T
在声明中引用的类型的底层类型。对于作为其 类型约束 的底层类型的类型参数,该类型总是一个接口。
type ( A1 = 字符串 A2 = A1 )
type ( B1 字符串 B2 B1 B3 []B1 B4 B3 )
func f[P any](x P) { … }
string
、A1
、A2
、B1
和B2
的基础类型是string
。[]B1
、B3
和B4
的基础类型是[]B1
。P
的基础类型是interface{}
。
核心类型
每个非接口类型 T
都有一个 核心类型,它与 T
的底层类型相同。
接口 T
如果满足以下条件之一,则具有核心类型:
没有其他接口有核心类型。
接口的核心类型取决于满足的条件,可能是:
- 类型
U
;或 - 如果
T
只包含双向通道,则为类型chan E
;或者根据定向通道的方向,为类型chan<- E
或<-chan E
。
核心类型的接口示例:
type Celsius 浮点数32 类型 Kelvin 浮点数32
interface{ int } // 整数 interface{ Celsius|Kelvin } // 浮点数 interface{ ~chan int } // 通道 int interface{ ~chan int|~chan<- int } // 通道<- int interface{ ~[]*data; String() string } // []*data
没有核心类型的接口示例:
interface{} // 没有单一的基础类型 interface{ Celsius|float64 } // 没有单一的基础类型 interface{ chan int | chan<- string } // 通道具有不同的元素类型 interface{ <-chan int | chan<- int } // 方向通道具有不同的方向
一些操作(切片表达式,追加和复制)依赖于一种更宽松的核心类型形式,它接受字节切片和字符串。具体来说,如果恰好有两种类型,[]byte
和 string
,它们是接口 T
类型集合中所有类型的基础类型,那么 T
的核心类型被称为 bytestring
。
bytestring
核心类型的接口示例:
interface{ int } // int (same as ordinary core type) interface{ []byte | string } // bytestring interface{ ~[]byte | myString } // bytestring
请注意 bytestring
不是一个真正的类型;它不能用于声明变量或组合其他类型。它的存在仅仅是为了描述一些从字节序列中读取的操作的行为,这些字节序列可以是字节切片或字符串。
类型标识
两种类型要么相同,要么不同。
- 如果两个数组类型具有相同的元素类型和相同的数组长度,则它们是相同的。
- 如果两个切片类型具有相同的元素类型,则它们是相同的。
- 如果两个结构体类型具有相同的字段顺序,并且相应的字段具有相同的名称、相同的类型和相同的标签,则它们是相同的。来自不同包的非导出字段名称始终不同。
- 如果两个指针类型具有相同的基本类型,则它们是相同的。
- 如果两个函数类型具有相同数量的参数和结果值,相应的参数和结果类型是相同的,并且两个函数都是可变参数或都不是,则它们是相同的。参数和结果的名称不需要匹配。
- 如果两个接口类型定义了相同的类型集,则它们是相同的。
- 如果两个映射类型具有相同的键和元素类型,则它们是相同的。
- 如果两个通道类型具有相同的元素类型和相同的方向,则它们是相同的。
- 如果两个实例化类型的定义类型和所有类型参数都是相同的,则它们是相同的。
鉴于声明
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, 和 []string A2 和 struct{ a, b int } A3 和 int A4, func(int, float64) *[]string, 和 A5
B0 和 C0 D0[int, string] 和 E0 []int 和 []int struct{ a, b *B5 } 和 struct{ a, b *B5 } func(x int, y float64) *[]string, func(int, float64) (result *[]string), 和 A5
B0
和 B1
是不同的,因为它们是由不同的type definitions创建的新类型;func(int, float64) *B0
和 func(x int, y float64) *[]string
是不同的,因为 B0
与 []string
不同;P1
和 P2
是不同的,因为它们是不同的类型参数。D0[int, string]
和 struct{ x int; y string }
是不同的,因为前者是一个实例化定义的类型,而后者是一个类型字面量(但它们仍然是可赋值的)。
可分配性
类型为V
的值x
可以赋值给类型为T
的变量(x
可以赋值给T
),如果满足以下条件之一:
另外,如果 x
的类型 V
或 T
是类型参数,则如果满足以下条件之一,x
可以赋值给类型为 T
的变量:
x
是预声明的标识符nil
,T
是类型参数,并且x
可分配给T
的类型集中的每个类型。V
不是 命名类型,T
是类型参数,并且x
可分配给T
的类型集中的每个类型。V
是类型参数,T
不是命名类型,并且V
类型集中的每个类型的值可分配给T
。
代表性
常量 x
是 _可由类型 T
的值表示,其中 T
不是 类型参数 之一,如果满足以下条件之一:
如果T
是一个类型参数,x
是T
类型的值,当x
是T
类型集合中每种类型的值时,x
是可表示的。
x 可以用 T 的值来表示,因为
'a' byte 97 在字节值集合中 97 rune rune 是 int32 的别名,97 在 32 位整数集合中 "foo" string "foo" 在字符串值集合中 1024 int16 1024 在 16 位整数集合中 42.0 byte 42 在无符号 8 位整数集合中 1e10 uint64 10000000000 在无符号 64 位整数集合中 2.718281828459045 float32 2.718281828459045 四舍五入为 2.7182817,属于 float32 值集合 -1e-1000 float64 -1e-1000 四舍五入为 IEEE -0.0,进一步简化为 0.0 0i int 0 是整数值 (42 + 0i) float32 42.0(虚部为零)属于 float32 值集合
x T x 无法用 T 的值表示,因为
0 bool 0 不在布尔值集合中 'a' string 'a' 是一个rune,不在字符串值集合中 1024 byte 1024 不在无符号8位整数集合中 -1 uint16 -1 不在无符号16位整数集合中 1.1 int 1.1 不是整数值 42i float32 (0 + 42i) 不在float32值集合中 1e1000 float64 1e1000 在四舍五入后溢出为IEEE +Inf
方法集
类型的_method set_确定了可以在该类型的操作数上调用的方法。每种类型都有与之关联的(可能为空的)方法集:
对包含嵌入字段的结构体(和指向结构体的指针)有进一步的规则,如在结构体类型一节中所述。任何其他类型都具有空方法集。
区块
一个 块 是在匹配的大括号内的可能为空的声明和语句序列。
Block = "{" StatementList "}" . StatementList = { Statement ";" } .
除了源代码中的显式块之外,还有隐式块:
块嵌套并影响作用域。
声明和作用域
声明将非空标识符绑定到常量声明,类型声明,类型参数声明,变量声明,函数声明,标签声明或包声明。程序中的每个标识符都必须声明。在同一代码块中不能声明两次标识符,在文件和包代码块中也不能声明标识符。
已声明标识符的作用域是源代码中标识符表示指定的常量、类型、变量、函数、标签或包的范围。
Go 使用 blocks 进行词法作用域:
- 预声明标识符 的作用域是 universe 块。
- 在顶层(在任何函数之外)声明的常量、类型、变量或函数(但不是方法)的标识符的作用域是包块。
- 导入包的包名的作用域是包含导入声明的文件块。
- 表示方法接收器、函数参数或结果变量的标识符的作用域是函数体。
- 表示函数类型参数或方法接收器声明的类型参数的标识符的作用域从函数名称之后开始,直到函数体结束。
- 表示类型类型参数的标识符的作用域从类型名称之后开始,直到 TypeSpec 结束。
- 在函数内声明的常量或变量标识符的作用域从 ConstSpec 或 VarSpec 结束(对于短变量声明为 ShortVarDecl),直到最内层包含块结束。
- 在函数内声明的类型标识符的作用域从 TypeSpec 中的标识符开始,直到最内层包含块结束。
在一个块中声明的标识符可以在内部块中重新声明。在内部声明的标识符在作用域内时,它表示内部声明所声明的实体。
package clause 不是一个声明;包名不会出现在任何作用域中。它的目的是标识属于同一package 的文件,并指定导入声明的默认包名。
标签范围
标签由labeled statements声明,并在'break'、'continue'和'goto'语句中使用。定义从未使用的标签是非法的。与其他标识符不同,标签不是块作用域,也不会与非标签的标识符发生冲突。标签的作用域是声明它的函数体,不包括任何嵌套函数的函数体。
空白标识符
blank identifier 由下划线字符 _
表示。它作为匿名占位符,而不是常规(非空白)标识符,在declarations、operand和assignment statements中具有特殊含义。
预声明标识符
以下标识符在宇宙块中被隐式声明:
Types: 任意 布尔 字节 可比较 复数64 复数128 错误 浮点数32 浮点数64 整数 整数8 整数16 整数32 整数64 符文 字符串 无符号整数 无符号整数8 无符号整数16 无符号整数32 无符号整数64 指针
常量:true false iota
零值:nil
函数:append cap clear close complex copy delete imag len make max min new panic print println real recover
导出标识符
标识符可以被导出以允许从另一个包访问它。如果满足以下两个条件,则标识符将被导出:
- 标识符名称的第一个字符是 Unicode 大写字母(Unicode 字符类别 Lu);并且
- 该标识符在 package block 中声明,或者是 field name 或 method name。
所有其他标识符均不导出。
标识符的唯一性
给定一组标识符,如果它与集合中的每个其他标识符都不同,则称为“unique”。如果两个标识符拼写不同,或者它们出现在不同的packages中并且不是exported,则它们是不同的。否则,它们是相同的。
常量声明
常量声明将一组标识符(常量的名称)绑定到一组常量表达式的值。标识符的数量必须等于表达式的数量,左侧的第n个标识符绑定到右侧第n个表达式的值。
ConstDecl = "const" ( ConstSpec | "(" { ConstSpec ";" } ")" ) . ConstSpec = IdentifierList [ [ Type ] "=" ExpressionList ] .
如果类型存在,则所有常量都采用指定的类型,并且表达式必须是可分配到该类型的表达式,该类型不能是类型参数。如果省略了类型,则常量采用相应表达式的各自类型。如果表达式值是无类型的常量,则声明的常量保持无类型,并且常量标识符表示常量值。例如,如果表达式是浮点文字,即使文字的小数部分为零,常量标识符也表示浮点常量。
const Pi float64 = 3.14159265358979323846 const zero = 0.0 // 未命名浮点常量 const ( size int64 = 1024 eof = -1 // 未命名整数常量 ) const a, b, c = 3, 4, "foo" // a = 3, b = 4, c = "foo", 未命名整数和字符串常量 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 )
约塔
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 (未命名整数常量) v float64 = iota * 42 // v == 42.0 (float64 常量) w = iota * 42 // w == 84 (未命名整数常量) )
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) )
这个最后的例子利用了最后一个非空表达式列表的隐式重复。
类型声明
类型声明将标识符(类型名称)绑定到type。类型声明有两种形式:别名声明和类型定义。
别名声明
别名声明将标识符绑定到给定的类型。
在标识符的范围内,它作为类型的_别名_。
type ( nodeList = []*Node // nodeList 和 []*Node 是相同的类型 Polar = polar // Polar 和 polar 表示相同的类型 )
类型定义
类型定义创建一个新的、独特的类型,具有与给定类型相同的基础类型和操作,并将标识符(类型名称)绑定到它。
新类型被称为“定义类型”。它与任何其他类型都不同,包括它所创建的类型。
type ( Point struct{ x, y float64 } // Point 和 struct{ x, y float64 } 是不同的类型 polar Point // polar 和 Point 表示不同的类型 )
type TreeNode struct { left, right *TreeNode value any }
type Block interface { BlockSize() int Encrypt(src, dst []byte) Decrypt(src, dst []byte) }
// Mutex 是一种具有两种方法的数据类型,Lock 和 Unlock。 type Mutex struct { /* Mutex fields */ } func (m Mutex) Lock() { / Lock implementation */ } func (m Mutex) Unlock() { / Unlock implementation */ }
// NewMutex 具有与 Mutex 相同的组成,但其方法集为空。 type NewMutex Mutex
// PtrMutex 的基础类型 *Mutex 的方法集保持不变,但 PtrMutex 的方法集为空。
// *PrintableMutex 的方法集包含了与其嵌入字段 Mutex 绑定的 Lock 和 Unlock 方法。 type PrintableMutex struct { Mutex }
// MyBlock 是一个接口类型,其方法集与 Block 相同。 类型 MyBlock Block
类型定义可用于定义不同的布尔、数字或字符串类型,并将方法与它们关联起来:
类型 TimeZone int
const ( EST 东部标准时间 = -(5 + iota) CST 中部标准时间 MST 山区标准时间 PST 太平洋标准时间 )
func (tz TimeZone) String() string { return fmt.Sprintf("GMT%+dh", tz) }
如果类型定义指定了type parameters,类型名称表示一个_泛型类型_。在使用泛型类型时,必须进行实例化。
type List[T any] struct { next *List[T] value T }
在类型定义中,给定的类型不能是类型参数。
type T[P any] P // 非法: P 是一个类型参数
func fT any { type L T // illegal: T is a type parameter declared by the enclosing function }
通用类型也可以有与之关联的方法。在这种情况下,方法接收器必须声明与通用类型定义中相同数量的类型参数。
Len 方法返回链表 l 中元素的数量。func (l *List[T]) Len() int { … }
类型参数声明
类型参数列表声明了泛型函数或类型声明的类型参数。类型参数列表看起来像普通的函数参数列表,只是类型参数的名称必须全部存在,并且列表用方括号而不是圆括号括起来。
TypeParameters = "[" TypeParamList [ "," ] "]" . TypeParamList = TypeParamDecl { "," TypeParamDecl } . TypeParamDecl = IdentifierList TypeConstraint .
列表中的所有非空名称必须是唯一的。每个名称都声明了一个类型参数,这是一个新的、不同的named type,它充当声明中(迄今为止)未知类型的占位符。在泛型函数或类型的实例化时,类型参数将被替换为一个_type argument_。
[P 任意类型] [S 接口{ ~[]字节|字符串 }] [S ~[]E, E 任意类型] [P 约束[整数]] [_ 任意类型]
就像每个普通函数参数都有一个参数类型一样,每个类型参数都有一个对应的(元)类型,称为其类型约束。
当泛型类型的类型参数列表声明单个类型参数 P
并带有约束 C
时,会出现解析歧义,使得文本 P C
形成有效表达式:
type T[P *C] … type T[P (C)] … type T[P *C|Q] … …
在这些罕见的情况下,类型参数列表与表达式无法区分,类型声明被解析为数组类型声明。为了消除歧义,在interface中嵌入约束,或者使用尾随逗号:
type T[P interface{*C}] … type T[P *C,] …
类型参数也可以通过与泛型类型相关的方法声明的接收器规范来声明。
在泛型类型 T
的类型参数列表中,类型约束可能不会(直接或间接通过另一个泛型类型的类型参数列表)引用 T
。
type T1[P T1[P]] … // 非法: T1 指向自身 type T2[P interface{ T2[int] }] … // 非法: T2 指向自身 type T3[P interface{ m(T3[int])}] … // 非法: T3 指向自身 type T4[P T5[P]] … // 非法: T4 指向 T5, type T5[P T4[P]] … // T5 指向 T4
type T6[P int] struct{ f *T6[P] } // ok: reference to T6 is not in type parameter list
类型约束
type constraint 是一个 interface ,它定义了相应类型参数的可允许类型参数集合,并控制该类型参数的值支持的操作。
TypeConstraint = TypeElem .
如果约束是形式为interface{E}
的接口文字,其中E
是一个嵌入的type element(而不是一个方法),在类型参数列表中,为了方便起见,可以省略封闭的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
。但是,它们满足comparable
。
int // 实现了可比较性 (int 是严格可比较的) []byte // 未实现可比较性 (切片不可比较) interface{} // 未实现可比较性 (参见上文) interface{ ~int | ~string } // 仅限类型参数: 实现了可比较性 (int、string 类型是严格可比较的) interface{ comparable } // 仅限类型参数: 实现了可比较性 (comparable 实现了自身) interface{ ~int | ~[]byte } // 仅限类型参数: 未实现可比较性 (切片不可比较) interface{ ~struct{ any } } // 仅限类型参数: 未实现可比较性 (字段 any 不是严格可比较的)
comparable
接口和直接或间接嵌入comparable
的接口只能用作类型约束。它们不能作为值或变量的类型,也不能作为其他非接口类型的组件。
满足类型约束
类型参数 T
满足 类型约束 C
,如果 T
是由 C
定义的类型集合的元素;即,如果 T
implements C
。作为例外,严格可比较的类型约束也可以由可比较的(不一定是严格可比较的)类型参数满足。更准确地说:
类型 T 满足 约束 C
如果
type argument type constraint // 约束满足
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() }
由于约束满足规则中的异常,比较参数类型的操作数可能会在运行时出现恐慌(即使可比较的类型参数始终是严格可比较的)。
变量声明
变量声明创建一个或多个变量,将相应的标识符绑定到它们,并为每个变量指定类型和初始值。
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
实现限制: 如果变量从未被使用,编译器可能会禁止在函数体内声明变量。
短变量声明
短变量声明 使用以下语法:
这是一个常规变量声明的简写形式,带有初始化表达式但没有类型:
"var" 标识符列表 "=" 表达式列表 .
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
与常规变量声明不同,短变量声明可以重新声明变量,前提是它们最初在同一代码块中(或者如果该代码块是函数体,则在参数列表中)以相同的类型进行了声明,并且至少有一个非blank变量是新的。因此,重新声明只能出现在多变量短声明中。重新声明不会引入新变量;它只是为原始变量赋予新值。:=
左侧的非空白变量名必须是unique。
field1, offset := nextField(str, 0) field2, offset := nextField(str, offset) // 重新声明 offset x, y, x := 1, 2, 3 // 非法:在 := 的左侧重复声明 x
短变量声明只能出现在函数内部。在某些情况下,比如"if"(#If%5Fstatements)语句的初始化器,"for"(#For%5Fstatements)语句的初始化器,或者"switch"(#Switch%5Fstatements)语句的初始化器,它们可以用来声明临时的局部变量。
函数声明
函数声明将标识符(函数名称)绑定到一个函数。
func IndexRune(s string, r rune) int { for i, c := range s { if c == r { return i } } // invalid: missing return statement }
如果函数声明指定了type parameters,函数名表示泛型函数。泛型函数必须在调用或用作值之前进行实例化。
func min[T ~int|~float64](x, y T) T { if x < y { return x } return y }
没有类型参数的函数声明可以省略主体。这样的声明提供了一个在Go之外实现的函数的签名,比如汇编例程。
func flushICache(begin, end uintptr) // 在外部实现
方法声明
方法是具有接收器的函数。方法声明将标识符(方法名称)绑定到方法,并将方法与接收器的基本类型关联起来。
接收器是通过在方法名之前指定的额外参数部分来指定的。该参数部分必须声明一个单一的非可变参数,即接收器。其类型必须是一个定义类型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 }
将方法Length
和Scale
与接收器类型*Point
绑定到基本类型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
表达式
表达式通过将运算符和函数应用于操作数来指定值的计算。
操作数
Operand = 字面量 | 操作数名称 [ 类型参数 ] | "(" 表达式 ")" . Literal = 基本字面量 | 复合字面量 | 函数字面量 . BasicLit = 整数字面量 | 浮点数字面量 | 虚数字面量 | 字符字面量 | 字符串字面量 . OperandName = 标识符 | 限定标识符 .
实现限制: 如果操作数的类型是具有空type set的type parameter,编译器无需报告错误。 具有这种类型参数的函数无法被instantiated; 任何尝试都将导致在实例化站点出现错误。
合格标识符
qualified identifier 是一个带有包名前缀的标识符。包名和标识符都不能是blank。
math.Sin // 表示 math 包中的 Sin 函数
复合文字
复合文字每次被评估时都会构造新的复合值。它们由文字类型和花括号包围的元素列表组成。每个元素可能可选地由相应的键前置。
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 的 core type T
必须是结构体、数组、切片或映射类型(除非类型以 TypeName 给出,语法会强制执行此约束)。元素和键的类型必须与类型 T
的相应字段、元素和键类型可赋值;不会进行额外的转换。对于结构体字面量,键被解释为字段名;对于数组和切片字面量,键被解释为索引;对于映射字面量,键被解释为键。对于映射字面量,所有元素必须有一个键。指定具有相同字段名或常量键值的多个元素是错误的。对于非常量映射键,请参阅evaluation order部分。
对于结构字面量,以下规则适用:
- 键必须是在结构类型中声明的字段名。
- 不包含任何键的元素列表必须按照字段声明的顺序列出每个结构字段的元素。
- 如果任何元素有一个键,每个元素都必须有一个键。
- 包含键的元素列表不需要为每个结构字段都有一个元素。省略的字段将获得该字段的零值。
- 文字量可以省略元素列表;这样的文字量将评估为其类型的零值。
- 为不同包中的结构的非导出字段指定元素是错误的。
鉴于声明
type Point3D struct { x, y, z float64 } type Line struct { p, q Point3D }
一个人可能会写
origin := Point3D{} // Point3D 的零值 line := Line{origin, Point3D{y: -4, z: 12.3}} // line.q.x 的零值
对于数组和切片字面量,以下规则适用:
- 每个元素都有一个关联的整数索引,标记其在数组中的位置。
- 具有键的元素使用该键作为其索引。键必须是一个非负常数,可以由
int
类型的值表示;如果键被类型化,它必须是整数类型。 - 没有键的元素使用前一个元素的索引加一。如果第一个元素没有键,则其索引为零。
获取复合文字的地址会生成一个指向使用该文字值初始化的唯一变量的指针。
var pointer *Point3D = &Point3D{y: 1000}
p1 := &[]int{} // p1 指向一个已初始化的空切片,值为 []int{},长度为 0 p2 := new([]int) // p2 指向一个未初始化的切片,值为 nil,长度为 0
数组文字的长度是文字类型中指定的长度。如果文字中提供的元素少于长度,则缺少的元素将设置为数组元素类型的零值。如果提供的元素具有超出数组索引范围的索引值,则会出现错误。符号...
指定的数组长度等于最大元素索引加一。
buffer := [10]string{} // len(buffer) == 10 intSet := [6]int{1, 2, 3, 5} // len(intSet) == 6 days := [...]string{"Sat", "Sun"} // len(days) == 2
切片字面量描述了整个底层数组字面量。因此,切片字面量的长度和容量是最大元素索引加一。切片字面量的形式为
[]T{x1, x2, … xn}
并且是对数组进行切片操作的简写形式:
tmp := [n]T{x1, x2, … xn} tmp[0 : n]
在数组、切片或映射类型 T
的复合字面量中,如果元素或映射键本身是复合字面量,则当它们的类型与 T
的元素或键类型相同时,可以省略相应的字面量类型。类似地,当元素或键的类型为 *T
时,是复合字面量的地址时,可以省略 &T
。
[...]Point{{1.5, -3.5}, {0, 0}} // 与[...]Point{Point{1.5, -3.5}, Point{0, 0}}相同 [][]int{{1, 2, 3}, {4, 5}} // 与[][]int{[]int{1, 2, 3}, []int{4, 5}}相同 [][]Point{{{0, 1}, {1, 2}}} // 与[][]Point{[]Point{Point{0, 1}, Point{1, 2}}}相同 map[string]Point{"orig": {0, 0}} // 与map[string]Point{"orig": Point{0, 0}}相同 map[Point]string{{0, 0}: "orig"} // 与map[Point]string{Point{0, 0}: "orig"}相同
type PPoint *Point [2]*Point{{1.5, -3.5}, {}} // 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{})}
当使用LiteralType的TypeName形式的复合文字字面量作为“if”、“for”或“switch”语句的关键字和块的开括号之间的操作数时,会出现解析歧义,而且复合文字字面量没有被括号、方括号或花括号括起来。在这种罕见情况下,文字字面量的开括号会被错误地解析为引入语句块的开括号。为了解决这种歧义,复合文字字面量必须出现在括号内。
如果 x == (T{a,b,c}[i]) { … } 如果 (x == T{a,b,c}[i]) { … }
有效的数组、切片和映射字面量的示例:
// 质数列表 primes := []int{2, 3, 5, 7, 9, 2147483647}
// 如果 ch 是元音字母,vowels[ch] 为 true vowels := [128]bool{'a': true, 'e': true, 'i': true, 'o': true, 'u': true, 'y': true}
// 数组 [10]float32{-1, 0, 0, 0, -0.1, -0.1, 0, 0, 0, -1} filter := [10]float32{-1, 4: -0.1, -0.1, 9: -1}
// 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, }
函数文字
函数文本表示一个匿名函数。函数文本不能声明类型参数。
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 = 操作数 | 转换 | 方法表达式 | PrimaryExpr 选择器 | PrimaryExpr 索引 | PrimaryExpr 切片 | PrimaryExpr 类型断言 | PrimaryExpr 参数 .
Selector = "." 标识符 . Index = "[" 表达式 [ "," ] "]" . Slice = "[" [ 表达式 ] ":" [ 表达式 ] "]" | "[" [ 表达式 ] ":" 表达式 ":" 表达式 "]" . TypeAssertion = "." "(" 类型 ")" . Arguments = "(" [ ( 表达式列表 | 类型 "," 表达式列表 ] ) [ "..." ] [ "," ] ] ")" .
x 2 (s + ".txt") f(3.1415, true) Point{1, 2} m["foo"] s[i : j + 1] obj.color f.p[i].x()
选择器
x.f
选择器 f
可能表示类型 T
的字段或方法 f
,也可能表示 T
的嵌套嵌入字段 的字段或方法 f
。到达 f
的嵌入字段的数量称为其在 T
中的 深度。在 T
中声明的字段或方法 f
的深度为零。在 T
的嵌入字段 A
中声明的字段或方法 f
的深度为 A
中 f
的深度加一。
以下规则适用于选择器:
- 对于类型为
T
或*T
的值x
,其中T
不是指针或接口类型,x.f
表示在T
中最浅层的地方存在f
时的字段或方法。如果最浅层不止一个f
,则选择器表达式是非法的。 - 对于类型为
I
的值x
,其中I
是接口类型,x.f
表示x
的动态值的实际方法名称为f
。如果I
的 方法集 中没有名称为f
的方法,则选择器表达式是非法的。 - 作为例外,如果
x
的类型是 定义的 指针类型,并且(*x).f
是一个有效的选择器表达式,表示一个字段(但不是一个方法),则x.f
是(*x).f
的简写。 - 在所有其他情况下,
x.f
是非法的。 - 如果
x
是指针类型且具有值nil
,并且x.f
表示一个结构字段,则对x.f
进行赋值或评估会导致 运行时恐慌。 - 如果
x
是接口类型且具有值nil
,调用 或 评估 方法x.f
会导致 运行时恐慌。
例如,给定以下声明:
type T0 struct { x int }
func (*T0) M0()
type T1 struct { y int }
func (T1) M1()
type T2 struct { z int T1 *T0 }
func (*T2) M2()
类型 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).T0).x (*q).x 是一个有效的字段选择器
p.M0() // ((*p).T0).M0() M0 期望 *T0 接收器 p.M1() // ((*p).T1).M1() M1 期望 T1 接收器 p.M2() // p.M2() M2 期望 *T2 接收器 t.M2() // (&t).M2() M2 期望 *T2 接收器,请参阅调用部分
但以下内容是无效的:
q.M0() // (*q).M0 is valid but not a field selector
方法表达式
如果M
在类型T
的method set中,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
的静态类型为 T
,并且M
在类型 T
的method set中,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) // 类似于 t.Mv(7) f := pt.Mp; f(7) // 类似于 pt.Mp(7) f := pt.Mv; f(7) // 类似于 (*pt).Mv(7) f := t.Mp; f(7) // 类似于 (&t).Mp(7) f := makeT().Mp // 无效:makeT() 的结果不可寻址
虽然上面的示例使用了非接口类型,但也可以从接口类型的值创建方法值。
var i interface { M(int) } = myVal f := i.M; f(7) // like i.M(7)
索引表达式
形式为的主要表达
a[x]
表示数组、数组指针、切片、字符串或映射 a
中由 x
索引的元素。值 x
分别称为 索引 或 映射键。以下规则适用:
如果 a
既不是映射也不是类型参数:
对于A
的数组类型:
- 必须在范围内的 constant 索引
- 如果
x
在运行时超出范围,将发生 run-time panic a[x]
是索引为x
的数组元素,a[x]
的类型是A
的元素类型
对于指向数组类型的指针 a
:
a[x]
是 (*a)[x]
的简写
对于S
的slice type中的a
:
- 如果
x
在运行时超出范围,会发生运行时恐慌 a[x]
是索引为x
的切片元素,a[x]
的类型是S
的元素类型
对于a
的string type:
- 如果字符串
a
也是常量,则常量索引必须在范围内 - 如果
x
在运行时超出范围,则会发生运行时恐慌 a[x]
是索引x
处的非常量字节值,其类型为byte
- 不能对
a[x]
进行赋值
对于M
的映射类型:
对于P
的type parameter type:
- 索引表达式
a[x]
必须对P
类型集合中所有类型的值有效。 P
类型集合中所有类型的元素类型必须相同。在此上下文中,字符串类型的元素类型是byte
。- 如果
P
的类型集合中有映射类型,则该类型集合中的所有类型必须是映射类型,并且相应的键类型必须全部相同。 a[x]
是P
实例化的类型参数的数组、切片或字符串中索引为x
的元素,或者具有键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
映射的元素会导致a运行时恐慌。
切片表达式
切片表达式从字符串、数组、数组指针或切片中构造子字符串或切片。有两种变体:简单形式指定了下界和上界,完整形式还指定了容量上限。
简单的切片表达式
主要表达式
a[低:高]
构造一个子字符串或切片。a
的核心类型必须是字符串、数组、数组指针、切片或者字节串。索引 low
和 high
选择操作数 a
中出现在结果中的元素。结果的索引从0开始,长度等于high
- low
。在对数组 a
进行切片后
a := [5]int{1, 2, 3, 4, 5} s := a[1:4]
切片 s
的类型为 []int
,长度为 3,容量为 4,元素为
s[0] == 2 s[1] == 3 s[2] == 4
为了方便起见,可以省略任何一个索引。缺少的 low
索引默认为零;缺少的 high
索引默认为被切片操作数的长度:
a[2:] // 与 a[2:len(a)] 相同 a[:3] // 与 a[0:3] 相同 a[:] // 与 a[0:len(a)] 相同
如果 a
是指向数组的指针,a[low : high]
是(*a)[low : high]
的简写。
对于数组或字符串,如果0
<= low
<= high
<= len(a)
,则索引处于范围内,否则它们处于范围外。对于切片,上限索引边界是切片容量cap(a)
,而不是长度。常量索引必须是非负的,并且可以由int
类型的值表示;对于数组或常量字符串,常量索引也必须在范围内。如果两个索引都是常量,则它们必须满足low <= high
。如果索引在运行时超出范围,则会发生运行时恐慌。
除了未命名字符串之外,如果被切片的操作数是字符串或切片,则切片操作的结果是与操作数相同类型的非常量值。对于未命名字符串操作数,结果是string
类型的非常量值。如果被切片的操作数是数组,则它必须是可寻址的,切片操作的结果是具有与数组相同元素类型的切片。
如果有效的切片表达式的切片操作数是nil
切片,则结果是nil
切片。否则,如果结果是切片,则它与操作数共享其底层数组。
var a [10]int s1 := a[3:7] // s1 的底层数组是数组 a; &s1[2] == &a[5] s2 := s1[1:4] // s2 的底层数组是 s1 的底层数组,即数组 a; &s2[1] == &a[5] s2[1] = 42 // s2[1] == s1[2] == a[5] == 42; 它们都指向同一个底层数组元素
var s []int s3 := s[:0] // s3 == nil
完整切片表达式
主要表达式
a[低 : 高 : 最大]
构造一个与简单切片表达式 a[low : high]
相同类型、相同长度和元素的切片。此外,通过将其设置为 max - low
,它还控制了结果切片的容量。只有第一个索引可以省略;默认为 0。a
的核心类型必须是数组、数组指针或切片(但不能是字符串)。在对数组 a
进行切片后
a := [5]int{1, 2, 3, 4, 5} t := a[1:3:5]
切片 t
的类型为 []int
,长度为 2,容量为 4,元素为
t[0] == 2 t[1] == 3
就简单的切片表达式而言,如果a
是数组的指针,a[low : high : max]
等同于(*a)[low : high : max]
。如果被切片的操作数是数组,它必须是可寻址的。
如果0 <= low <= high <= max <= cap(a)
,则索引处于范围内,否则它们就处于范围外。常量索引必须是非负的,并且可以由int
类型的值表示;对于数组,常量索引还必须在范围内。如果多个索引是常量,则出现的常量必须相对于彼此处于范围内。如果索引在运行时超出范围,则会发生运行时恐慌。
类型断言
对于接口类型[#Interface%5Ftypes]的表达式x
,但不是类型参数[#Type%5Fparameter%5Fdeclarations],以及类型T
,主要表达式
x.(T)
断言 x
不是 nil
,并且存储在 x
中的值是类型 T
。符号 x.(T)
称为 类型断言。
更准确地说,如果 T
不是接口类型,x.(T)
断言 x
的动态类型与类型 T
相同。在这种情况下,T
必须实现x
的(接口)类型;否则类型断言无效,因为x
不可能存储类型为T
的值。如果 T
是接口类型,x.(T)
断言 x
的动态类型实现接口 T
。
如果类型断言成立,则表达式的值是存储在 x
中的值,其类型为 T
。如果类型断言不成立,则会发生 运行时恐慌。换句话说,即使 x
的动态类型只在运行时才知道,x.(T)
的类型在正确的程序中也被认为是 T
。
var x interface{} = 7 // x 具有动态类型 int 和值 7 i := x.(int) // i 具有类型 int 和值 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
电话
f(a1, a2, … an)
使用参数a1, a2, … an
调用f
。除了一个特殊情况外,参数必须是F
的参数类型的可赋值单值表达式,并在调用函数之前进行求值。表达式的类型是F
的结果类型。方法调用类似,但方法本身是在方法的接收器类型的值上指定为选择器。
math.Atan2(x, y) // 函数调用 var pt *Point pt.Scale(3.5) // 带有接收器 pt 的方法调用
如果f
表示一个通用函数,在调用或将其用作函数值之前,必须对其进行实例化。
在函数调用中,函数值和参数按照通常的顺序进行评估。在它们被评估之后,调用的参数按值传递给函数,然后被调用的函数开始执行。函数的返回参数在函数返回时按值传递回调用者。
调用nil
函数值会引发运行时恐慌。
作为特例,如果函数或方法g
的返回值数量与另一个函数或方法f
的参数数量相等,并且可以分别赋值给f
的参数,那么调用f(g(_parametersofg_))
将在按顺序绑定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") }
var p Point p.Scale(3.5)
没有明确的方法类型,也没有方法文字。
将参数传递给 ...
参数
如果f
是可变参数的,最后一个参数p
的类型是...T
,那么在f
内部,p
的类型等同于[]T
的类型。如果对p
没有实际参数调用f
,那么传递给p
的值是nil
。否则,传递的值是一个新的[]T
类型的切片,其底层数组是连续的实际参数,这些参数都必须是可赋值的到T
。因此,切片的长度和容量是绑定到p
的参数数量,可能在每个调用点都不同。
给定函数和调用
func Greeting(prefix string, who ...string) Greeting("nobody") Greeting("hello:", "Joe", "Anna", "Eileen")
在问候
中,who
在第一次调用时将具有值nil
,在第二次调用时将具有值[]string{"Joe", "Anna", "Eileen"}
。
如果最后一个参数可以赋值给切片类型[]T
,并且后面跟着...
,它将不变地作为...T
参数的值传递。在这种情况下,不会创建新的切片。
给定切片s
和调用
s := []string{"詹姆斯", "茉莉"} 问候("再见:", s...)
在Greeting
中,who
将具有与相同基础数组的s
相同的值。
实例化
通过为类型参数替换类型参数,可以实例化通用函数或类型。 实例化分为两个步骤:
- 在泛型声明中,每个类型参数都会被替换为其对应的类型参数。这种替换会在整个函数或类型声明中进行,包括类型参数列表本身以及列表中的任何类型。
- 替换后,每个类型参数必须满足相应类型参数的约束(如有必要,进行实例化)。否则实例化将失败。
实例化类型会产生一个新的非泛型 命名类型; 实例化函数会产生一个新的非泛型函数。
类型参数列表 类型参数 替换后
int int 满足任何条件 []int, int []int, int 满足~[]int, int 满足任何条件 string 非法: string 不满足 io.Writer any 任何条件满足(但不实现)comparable
在使用通用函数时,类型参数可以明确提供,也可以根据函数使用的上下文部分或完全推断。只要可以推断出类型参数,如果函数是以下情况之一,类型参数列表可以完全省略:
在所有其他情况下,必须存在(可能是部分的)类型参数列表。如果类型参数列表不存在或部分存在,则必须能够从函数使用的上下文中推断出所有缺失的类型参数。
// sum 返回其参数的和(对于字符串来说是连接)。 func sum[T ~int | ~float64 | ~string](x... T) T { … }
x := sum // 非法:x 的类型未知 intSum := sum[int] // intSum 的类型为 func(x... int) int a := intSum(2, 3) // a 的值为 5,类型为 int b := sum[float64](2.0, 3) // b 的值为 5.0,类型为 float64 c := sum(b, -1) // c 的值为 4.0,类型为 float64
type sumFunc func(x... string) string var f sumFunc = sum // 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[] // 非法:类型参数列表不能为空 f1 := apply[[]int] // 显式提供 S 的类型参数,推断 E 的类型参数 f2 := apply[[]string, string] // 两个类型参数都显式提供
var bytes []byte r := apply(bytes, func(byte) byte { … }) // both type arguments inferred from the function arguments
对于泛型类型,所有类型参数必须始终明确提供。
类型推断
使用通用函数时,如果可以从函数使用的上下文中推断出类型参数,包括函数类型参数的约束条件,可以省略部分或全部类型参数。如果可以推断出缺失的类型参数,并且使用推断的类型参数进行实例化成功,则类型推断成功。否则,类型推断失败,程序无效。
类型推断使用类型之间的关系来进行推断:例如,函数参数必须能够赋值给相应的函数参数;这建立了参数的类型和参数的类型之间的关系。如果这两种类型中有任何一个包含类型参数,类型推断会寻找类型参数来替换类型参数,以满足赋值关系。类似地,类型推断使用类型参数必须满足其相应类型参数的约束这一事实。
每对匹配的类型对应一个包含一个或多个类型参数的类型方程,来自一个或可能多个泛型函数。推断缺失的类型参数意味着解决相应类型参数的类型方程集。
例如,给定
// dedup 返回一个去除重复条目的参数切片的副本。 func dedupS ~[]E, E comparable S { … }
type Slice []int var s Slice s = dedup(s) // same as s = dedupSlice, int
类型为Slice
的变量s
必须能够赋值给函数参数类型S
,程序才能有效。为了降低复杂性,类型推断会忽略赋值的方向性,因此Slice
和S
之间的类型关系可以通过(对称的)类型方程Slice ≡A S
(或者反过来写作S ≡A Slice
)来表示,其中≡A
中的A
表示左右两侧的类型必须符合可赋值规则(详见type unification部分)。同样,类型参数S
必须满足其约束~[]E
。这可以表示为S ≡C ~[]E
,其中X ≡C Y
表示“X
满足约束Y
”。这些观察结果导致了一组两个方程式。
切片 ≡A S (1) S ≡C ~[]E (2)
现在可以解决类型参数S
和E
。从(1)中,编译器可以推断出S
的类型参数是Slice
。同样,因为Slice
的底层类型是[]int
,而[]int
必须匹配约束条件的[]E
,编译器可以推断出E
必须是int
。因此,对于这两个方程,类型推断推断出
S ➞ 切片 E ➞ 整数
给定一组类型方程,需要解决的类型参数是需要实例化的函数的类型参数,且没有提供显式类型参数。这些类型参数称为_绑定_类型参数。例如,在上面的dedup
示例中,类型参数P
和E
绑定到dedup
。泛型函数调用的参数可能是一个泛型函数本身。该函数的类型参数包括在绑定类型参数集合中。函数参数的类型可能包含来自其他函数的类型参数(例如封闭函数调用的泛型函数)。这些类型参数也可能出现在类型方程中,但在该上下文中它们不是绑定的。类型方程总是仅针对绑定类型参数进行求解。
类型推断支持对泛型函数的调用以及将泛型函数分配给(显式函数类型的)变量。这包括将泛型函数作为参数传递给其他(可能也是泛型的)函数,并将泛型函数作为结果返回。类型推断针对每种情况都有一组特定的方程式。方程式如下(为了清晰起见,省略了类型参数列表):
- 对于函数调用
f(a0, a1, …)
,其中f
或函数参数ai
是一个通用函数:每对对应的函数参数和参数(ai, pi)
,其中ai
不是无类型常量,产生一个等式typeof(pi) ≡A typeof(ai)
。如果ai
是一个无类型常量cj
,并且typeof(pi)
是一个绑定的类型参数Pk
,则对类型方程式进行单独收集(cj, Pk)
。* 对于将通用函数f
分配给函数类型的(非通用)变量v
的赋值v = f
:typeof(v) ≡A typeof(f)
。* 对于返回语句return …, f, …
,其中f
是作为结果返回给(非通用)结果变量r
的通用函数:typeof(r) ≡A typeof(f)
。
另外,每个类型参数 Pk
和相应的类型约束 Ck
都产生类型方程 Pk ≡C Ck
。
类型推断优先考虑从已定义操作数中获取的类型信息,然后再考虑未定义的常量。因此,推断分为两个阶段:
- 使用type unification解决绑定类型参数的类型方程。如果统一失败,类型推断也失败。
- 对于尚未推断出类型参数
Pk
的每个绑定类型参数,并且已收集到一个或多个具有相同类型参数(cj, Pk)
的对,以与constant expressions相同的方式确定所有这些对中常量cj
的constant kind。对于Pk
的类型参数是确定的常量种类的default type。如果由于冲突的常量种类而无法确定常量种类,则类型推断失败。
如果在这两个阶段之后仍未找到所有类型参数,则类型推断失败。
如果这两个阶段都成功,类型推断将为每个限定类型参数确定一个类型参数。
Pk ➞ Ak
类型参数 Ak
可能是一个复合类型,包含其他绑定类型参数 Pk
作为元素类型(甚至可能只是另一个绑定类型参数)。在重复简化的过程中,每个类型参数中的绑定类型参数都被替换为相应的类型参数的类型参数,直到每个类型参数不再包含绑定类型参数为止。
如果类型参数通过绑定类型参数包含对自身的循环引用,简化和因此类型推断将失败。否则,类型推断将成功。
类型统一化
类型推断通过类型统一解决类型方程。类型统一递归地比较方程的左侧和右侧类型,其中左侧和右侧类型可以是绑定类型参数或包含绑定类型参数,然后寻找这些类型参数的类型参数,使得左侧和右侧匹配(根据上下文变得相同或可赋值)。为此,类型推断维护一个绑定类型参数到推断类型参数的映射;在类型统一期间,将查询并更新此映射。最初,绑定类型参数是已知的,但映射为空。在类型统一期间,如果推断出新的类型参数'A',则将从类型参数到参数的映射'P ➞ A'添加到映射中。相反,当比较类型时,已知的类型参数(已存在映射条目的类型参数)取代其对应的类型参数。随着类型推断的进行,映射会越来越多地填充,直到考虑了所有方程,或者直到统一失败。如果没有统一步骤失败并且映射对每个类型参数都有条目,则类型推断成功。
例如,给定带有绑定类型参数P
的类型方程
[10]struct{ elem P, list []P } ≡A [10]struct{ elem string; list []string }
类型推断从空映射开始。统一首先比较左侧和右侧类型的顶层结构。两者都是相同长度的数组;如果元素类型统一,则它们统一。两个元素类型都是结构体;如果它们具有相同数量的具有相同名称的字段,并且字段类型统一,则它们统一。P
的类型参数目前未知(没有映射条目),因此将P
与string
统一会将映射P ➞ string
添加到映射中。统一list
字段的类型需要统一[]P
和[]string
,因此需要统一P
和string
。由于此时P
的类型参数已知(存在P
的映射条目),因此它的类型参数string
取代了P
。由于string
与string
相同,因此此统一步骤也成功。现在方程的左侧和右侧的统一已经完成。类型推断成功,因为只有一个类型方程,没有统一步骤失败,并且映射已完全填充。
Unification uses a combination of exact and _loose_unification depending on whether two types have to beidentical,assignment-compatible, or only structurally equal. The respective type unification rulesare spelled out in detail in the Appendix.
对于形式为 X ≡A Y
的等式,其中 X
和 Y
是涉及赋值(包括参数传递和返回语句)的类型,顶层类型结构可能松散统一,但元素类型必须精确统一,符合赋值规则。
对于形式为 P ≡C C
的方程,其中 P
是类型参数,C
是其对应的约束,统一规则会更加复杂:
- 如果
C
有一个核心类型core(C)
,而P
有一个已知的类型参数A
,那么core(C)
和A
必须松散统一。如果P
没有已知的类型参数,并且C
恰好包含一个不是基础(波浪线)类型的类型项T
,统一会将映射P ➞ T
添加到映射中。 - 如果
C
没有核心类型,而P
有一个已知的类型参数A
,那么A
必须具有C
的所有方法(如果有的话),并且相应的方法类型必须精确统一。
在解决类型约束的类型方程时,解决一个方程可能会推断出额外的类型参数,进而可能使依赖于这些类型参数的其他方程得以解决。类型推断会重复类型统一,直到推断不出新的类型参数为止。
运算符
运算符将操作数组合成表达式。
binary_op = "||" | "&&" | rel_op | add_op | mul_op . rel_op = "==" | "!=" | "<" | "<=" | ">" | ">=" . add_op = "+" | "-" | "|" | "^" . mul_op = "*" | "/" | "%" | "<<" | ">>" | "&" | "&^" .
unary_op = "+" | "-" | "!" | "^" | "*" | "&" | "<-" .
在移位表达式中,右操作数必须具有整数类型或者是一个可以用uint
类型的值表示的无类型常量。如果非常量移位表达式的左操作数是一个无类型常量,那么它首先会被隐式转换为它在移位表达式中所代表的类型。
var a [1024]byte var s uint = 33
// 以下示例的结果是针对 64 位整数的。 var i = 1<<s // 1 的类型为 int var j int32 = 1<<s // 1 的类型为 int32; j == 0 var k = uint64(1<<s) // 1 的类型为 uint64; k == 1<<33 var m int = 1.0<<s // 1.0 的类型为 int; m == 1<<33 var n = 1.0<<s == j // 1.0 的类型为 int32; n == true var o = 1<<s == 2<<s // 1 和 2 的类型为 int; o == false var p = 1<<s == 1<<33 // 1 的类型为 int; p == true var u = 1.0<<s // 非法: 1.0 的类型为 float64,无法进行位移 var u1 = 1.0<<s != 0 // 非法: 1.0 的类型为 float64,无法进行位移 var u2 = 1<<s != 1.0 // 非法: 1 的类型为 float64,无法进行位移 var v1 float32 = 1<<s // 非法: 1 的类型为 float32,无法进行位移 var v2 = string(1<<s) // 非法: 1 被转换为字符串,无法进行位移 var w int64 = 1.0<<33 // 1.0<<33 是一个常量位移表达式; w == 1<<33 var x = a[1.0<<s] // 引发 panic: 1.0 的类型为 int,但 1<<33 超出了数组边界 var b = make([]byte, 1.0<<s) // 1.0 的类型为 int; len(b) == 1<<33
// 以下示例的结果是针对32位整数的,这意味着移位将会溢出。 var mm int = 1.0<<s // 1.0 的类型为 int; mm == 0 var oo = 1<<s == 2<<s // 1 和 2 的类型为 int; oo == true var pp = 1<<s == 1<<33 // 非法: 1 的类型为 int, 但 1<<33 溢出 int var xx = a[1.0<<s] // 1.0 的类型为 int; xx == a[0] var bb = make([]byte, 1.0<<s) // 1.0 的类型为 int; len(bb) == 0
运算符优先级
一元运算符具有最高的优先级。由于 ++
和 --
运算符形成语句而不是表达式,它们不属于运算符层次结构。因此,语句 *p++
等同于 (*p)++
。
二进制运算符有五个优先级别。乘法运算符优先级最高,其次是加法运算符,比较运算符,&&
(逻辑与),最后是||
(逻辑或):
5 * / % << >> & &^ 4 + - | ^ 3 == != < <= > >= 2 && 1 ||
相同优先级的二元运算符从左到右结合。例如,x / y * z
等同于 (x / y) * z
。
+x 23 + 3*x[i] x <= f() ^a >> b f() || g() x == y+1 && <-chanInt > 0
算术运算符
- 求和 整数,浮点数,复数值,字符串
- 差值 整数,浮点数,复数值
- 乘积 整数,浮点数,复数值 / 商 整数,浮点数,复数值 % 余数 整数
& 按位与 整数 | 按位或 整数 ^ 按位异或 整数 &^ 按位清除 (与非) 整数
<< 左移 整数 << 整数 >= 0
右移 整数 >> 整数 >= 0
如果操作数类型是type parameter,则运算符必须适用于该类型集合中的每种类型。操作数表示为类型参数实例化时的类型参数值,并且使用该类型参数的精度计算操作。例如,给定函数:
func dotProduct[F ~float32|~float64](v1, v2 []F) F { var s F for i, x := range v1 { y := v2[i] s += x * y } return s }
产品 x * y
和加法 s += x * y
分别使用 float32
或 float64
精度进行计算,具体取决于 F
的类型参数。
整数运算符
对于两个整数值 x
和 y
,整数商 q = x / y
和余数 r = x % y
满足以下关系:
x = q*y + r and |r| < |y|
向零截断的 x / y
(截断除法)。
x y x / y x % y 5 3 1 2 -5 3 -1 -2 5 -3 -1 2 -5 -3 1 -2
唯一的例外是,如果除数 x
是 x
的 int 类型中最负的值,那么商 q = x / -1
等于 x
(且 r = 0
),这是由于二进制补码 整数溢出。
x, q
int8 -128 int16 -32768 int32 -2147483648 int64 -9223372036854775808
x x / 4 x % 4 x >> 2 x & 3 11 2 3 2 3 -11 -2 -3 -3 1
移位运算符将左操作数按右操作数指定的移位计数进行移位,右操作数必须是非负的。如果运行时移位计数为负,将发生运行时恐慌。如果左操作数是有符号整数,则移位运算符实现算术移位,如果是无符号整数,则实现逻辑移位。移位计数没有上限。移位的行为就好像左操作数按1进行n
次移位,对于移位计数为n
。因此,x << 1
等同于x*2
,x >> 1
等同于x/2
,但是向负无穷大截断。
对于整数操作数,一元运算符+
、-
和^
的定义如下:
+x 是 0 + x -x 取反 是 0 - x ^x 按位取反 是 m ^ x,其中对于无符号 x,m =“所有位设置为1”,对于有符号 x,m = -1
整数溢出
对于无符号整数值,操作+
、-
、*
和<<
是模2_n_计算的,其中_n_是无符号整数类型的位宽。粗略地说,这些无符号整数操作在溢出时会丢弃高位,并且程序可能依赖“环绕”行为。
对于有符号整数,操作+
、-
、*
、/
和<<
可能会发生合法的溢出,结果值存在且由有符号整数表示、操作和操作数确定性定义。溢出不会引发运行时恐慌。编译器不得在假设不会发生溢出的情况下优化代码。例如,它不能假设x < x + 1
始终为真。
浮点运算符
对于浮点数和复数,+x
与x
相同,而-x
是x
的取反。浮点数或复数除以零的结果未指定超出IEEE-754标准;是否发生运行时恐慌取决于具体实现。
例如,一些架构提供了“融合乘加”(FMA)指令,可以计算x*y + z
而不会对中间结果x*y
进行舍入。以下示例展示了Go实现何时可以使用该指令:
// FMA 允许计算 r,因为 xy 没有显式舍入: r = xy + z r = z; r += xy t = xy; r = t + z p = xy; r = p + z r = xy + float64(z)
// 由于它会省略 xy 的四舍五入,因此不允许对 r 进行 FMA 计算: r = float64(xy) + z r = z; r += float64(xy) t = float64(xy); r = t + z
字符串连接
可以使用+
运算符或+=
赋值运算符来连接字符串:
s := "hi" + string(c) s += " and good bye"
字符串相加通过连接操作数创建一个新的字符串。
比较运算符
比较运算符比较两个操作数,并产生一个无类型的布尔值。
== 等于 != 不等于 < 小于 <= 小于或等于
大于
= 大于或等于
在任何比较中,第一个操作数必须能够赋值给第二个操作数的类型,反之亦然。
等号运算符 ==
和 !=
适用于可比较类型的操作数。排序运算符 <
、<=
、>
和 >=
适用于有序类型的操作数。这些术语和比较的结果定义如下:
- 布尔类型是可比较的。如果两个布尔值都是
true
或都是false
,则它们相等。 - 整数类型是可比较且有序的。两个整数值按照通常的方式进行比较。
- 浮点数类型是可比较且有序的。两个浮点数值按照 IEEE-754 标准定义进行比较。
- 复数类型是可比较的。如果两个复数值
u
和v
满足real(u) == real(v)
和imag(u) == imag(v)
,则它们相等。 - 字符串类型是可比较且有序的。两个字符串值按字节逐字节进行词法比较。
- 指针类型是可比较的。如果两个指针值指向相同的变量或者都具有值
nil
,则它们相等。指向不同的零大小变量的指针可能相等,也可能不相等。 - 通道类型是可比较的。如果两个通道值是由相同的make调用创建的,或者都具有值
nil
,则它们相等。 - 非类型参数的接口类型是可比较的。如果两个接口值具有相同的动态类型和相等的动态值,或者都具有值
nil
,则它们相等。 - 非接口类型
X
的值x
和接口类型T
的值t
可以进行比较,如果类型X
是可比较的,并且X
实现T
。如果t
的动态类型与X
相同,并且t
的动态值等于x
,则它们相等。 - 如果所有字段类型都是可比较的,则结构类型是可比较的。如果两个结构值的对应非空白字段值相等,则它们相等。字段按源代码顺序进行比较,并且一旦两个字段值不同(或者所有字段都已比较),比较就会停止。
- 如果数组元素类型是可比较的,则数组类型是可比较的。如果两个数组值的对应元素值相等,则它们相等。元素按升序索引顺序进行比较,并且一旦两个元素值不同(或者所有元素都已比较),比较就会停止。
- 如果类型参数是严格可比较的(见下文),则它们是可比较的。
如果两个具有相同动态类型的接口值进行比较,而该类型不可比较,会导致运行时恐慌。这种行为不仅适用于直接接口值比较,还适用于比较接口值数组或具有接口值字段的结构体。
切片、映射和函数类型不可比较。但是,作为特例,切片、映射或函数值可以与预声明的标识符 nil
进行比较。指针、通道和接口值与 nil
的比较也是允许的,并遵循上述的一般规则。
const c = 3 < 4 // c 是未类型化的布尔常量 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 )
如果类型是可比较的,并且不是接口类型,也不是由接口类型组成,则它是严格可比较的类型。具体来说:
- 布尔、数字、字符串、指针和通道类型是严格可比的。
- 如果结构类型的所有字段类型都是严格可比的,则结构类型是严格可比的。
- 如果数组类型的数组元素类型是严格可比的,则数组类型是严格可比的。
- 如果类型参数的类型集合中的所有类型都是严格可比的,则类型参数是严格可比的。
逻辑运算符
逻辑运算符适用于boolean值,并产生与操作数相同类型的结果。右操作数是有条件地进行评估。
&& 条件与 p && q 是 "如果 p 则 q 否则为假" || 条件或 p || q 是 "如果 p 则为真 否则 q" ! 非 !p 是 "非 p"
地址运营商
对于类型为 T
的操作数 x
,地址操作&x
会生成一个类型为 *T
的指向 x
的指针。操作数必须是可寻址的,即变量、指针间接引用或切片索引操作;或可寻址结构操作数的字段选择器;或可寻址数组的数组索引操作。作为可寻址要求的例外,x
也可以是(可能带括号的)复合字面值。如果对 x
的评估会导致运行时恐慌,那么对 &x
的评估也会如此。
&x &a[f(2)] &Point{2, 3} *p *pf(x)
var x *int = nil *x // 导致运行时恐慌 &*x // 导致运行时恐慌
接收操作符
对于操作数 ch
,其核心类型为channel,接收操作 <-ch
的值是从通道 ch
接收到的值。通道方向必须允许接收操作,接收操作的类型是通道的元素类型。该表达式会一直阻塞,直到有值可用。从 nil
通道接收会一直阻塞。在关闭的通道上进行接收操作总是可以立即进行,接收到之前发送的值后,会产生元素类型的零值。
v1 := <-ch v2 = <-ch f(<-ch) <-strobe // 等待时钟脉冲并丢弃接收到的值
在赋值语句或特殊形式的初始化中使用的接收表达式
x, ok = <-ch x, ok := <-ch var x, ok = <-ch var x, ok T = <-ch
返回一个额外的未命名布尔结果,报告通信是否成功。如果接收到的值是通过成功的发送操作传递到通道的,则ok
的值为true
,如果它是因为通道已关闭且为空而生成的零值,则为false
。
转换
转换会将表达式的type更改为转换指定的类型。转换可能会直接出现在源代码中,也可能是由表达式出现的上下文_隐含的。
_显式_转换是形式为 T(x)
的表达式,其中 T
是一种类型,x
是可以转换为类型 T
的表达式。
如果类型以运算符 *
或 <-
开头,或者类型以关键字 func
开头且没有结果列表,则必须在必要时加括号以避免歧义:
Point(p) // 与(Point(p))相同 (Point)(p) // p被转换为Point <-chan int(c) // 与<-(chan int(c))相同 (<-chan int)(c) // c被转换为<-chan int func()(x) // 函数签名func() x (func())(x) // x被转换为func() (func() int)(x) // x被转换为func() int func() int(x) // x被转换为func() int(不含糊)
常量值 x
可以被转换为类型 T
,如果 x
能够被 T
类型的值表示。特殊情况下,整数常量 x
可以使用与非常量 x
相同的规则显式转换为string type。
将常量转换为不是type parameter的类型会产生一个类型化的常量。
uint(iota) // uint 类型的 iota 值 float32(2.718281828) // float32 类型的 2.718281828 complex128(1) // complex128 类型的 1.0 + 0.0i float32(0.49999999) // float32 类型的 0.5 float64(-1e-1000) // float64 类型的 0.0 string('x') // string 类型的 "x" string(0x266c) // string 类型的 "♬" myString("foo" + "bar") // myString 类型的 "foobar" string([]byte{'a'}) // 非常量: []byte{'a'} 不是常量 (*int)(nil) // 非常量: nil 不是常量, *int 不是布尔、数值或字符串类型 int(1.2) // 非法: 1.2 不能表示为 int string(65.0) // 非法: 65.0 不是整数常量
将常量转换为类型参数会产生该类型的一个 非常量 值,该值以类型参数实例化时的类型参数值表示。例如,给定以下函数:
func fP ~float32|~float64 { … P(1.1) … }
转换P(1.1)
的结果是P
类型的非常量值,值1.1
表示为float32
或float64
,具体取决于f
的类型参数。因此,如果f
实例化为float32
类型,则表达式P(1.1) + 1.2
的数值将以与相应的非常量float32
加法相同的精度计算。
非常数值 x
可以在以下任何情况下转换为类型 T
:
x
可以赋值给T
。- 忽略结构标签(见下文),
x
的类型和T
不是type parameters,但具有相同的underlying types。 - 忽略结构标签(见下文),
x
的类型和T
都是指针类型,不是 named types,它们的指针基本类型不是类型参数,但具有相同的 underlying types。 x
的类型和T
都是整数或浮点数类型。x
的类型和T
都是复数类型。x
是整数或字节片段或符文片段,T
是字符串类型。x
是字符串,T
是字节片段或符文片段。x
是片段,T
是数组或数组指针,片段和数组类型具有相同的元素类型。
此外,如果T
或x
的类型V
是类型参数,则如果满足以下条件之一,x
也可以转换为类型T
:
V
和T
都是类型参数,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) // 忽略标签,底层类型是相同的
特定规则适用于数值类型之间的(非常量)转换,或者与字符串类型之间的转换。这些转换可能会改变x
的表示并产生运行时成本。所有其他转换只会改变类型而不会改变x
的表示。
没有语言机制可以在指针和整数之间进行转换。该包 unsafe 在受限情况下实现了这种功能。
不同数值类型之间的转换
对于非常量数值的转换,应用以下规则:
- 在整数类型之间转换时,如果值是有符号整数,则将其符号扩展为隐式无限精度;否则将其零扩展。然后将其截断以适应结果类型的大小。例如,如果
v := uint16(0x10F0)
,那么uint32(int8(v)) == 0xFFFFFFF0
。转换始终产生有效值;没有溢出的指示。 - 将浮点数转换为整数时,小数部分被丢弃(朝零截断)。
- 将整数或浮点数转换为浮点类型,或将复数转换为另一个复数类型时,结果值将四舍五入到目标类型指定的精度。例如,类型为
float32
的变量x
的值可能使用超出IEEE-754 32位数字的精度存储,但float32(x)
表示将x
的值舍入为32位精度的结果。类似地,x + 0.1
可能使用超过32位的精度,但float32(x + 0.1)
不会。
在所有涉及浮点数或复数值的非常量转换中,如果结果类型无法表示该值,则转换会成功,但结果值取决于实现。
转换为字符串类型和从字符串类型转换
- 将字节切片转换为字符串类型会产生一个字符串,其连续的字节是切片的元素。 string([]byte{'h', 'e', 'l', 'l', '\xc3', '\xb8'}) // "hellø" string([]byte{}) // "" string([]byte(nil)) // "" type bytes []byte string(bytes{'h', 'e', 'l', 'l', '\xc3', '\xb8'}) // "hellø" type myByte byte string([]myByte{'w', 'o', 'r', 'l', 'd', '!'}) // "world!" myString([]myByte{'\xf0', '\x9f', '\x8c', '\x8d'}) // "🌍"
- 将符文切片转换为字符串类型会产生一个字符串,其中是将各个符文值转换为字符串后连接而成。 string([]rune{0x767d, 0x9d6c, 0x7fd4}) // "\u767d\u9d6c\u7fd4" == "白鵬翔" string([]rune{}) // "" string([]rune(nil)) // "" type runes []rune string(runes{0x767d, 0x9d6c, 0x7fd4}) // "\u767d\u9d6c\u7fd4" == "白鵬翔" type myRune rune string([]myRune{0x266b, 0x266c}) // "\u266b\u266c" == "♫♬" myString([]myRune{0x1f30e}) // "\U0001f30e" == "🌎"
- 将字符串类型的值转换为字节切片类型会产生一个切片,其中连续的元素是字符串的字节。 []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'}
- 将字符串类型的值转换为符文切片类型会产生一个包含字符串的各个Unicode码点的切片。 []rune(myString("白鵬翔")) // []rune{0x767d, 0x9d6c, 0x7fd4} []rune("") // []rune{} runes("白鵬翔") // []rune{0x767d, 0x9d6c, 0x7fd4} []myRune("♫♬") // []myRune{0x266b, 0x266c} []myRune(myString("🌐")) // []myRune{0x1f310}
- 最后,出于历史原因,整数值可以转换为字符串类型。这种转换形式会产生一个包含给定整数值的(可能是多字节的)UTF-8表示的Unicode码点的字符串。超出有效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.AppendRune或utf8.EncodeRune等库函数。
从切片转换为数组或数组指针
将切片转换为数组会产生一个包含切片底层数组元素的数组。类似地,将切片转换为数组指针会产生一个指向切片底层数组的指针。在这两种情况下,如果切片的长度小于数组的长度,则会发生运行时恐慌。
s := make([]byte, 2, 4)
a0 := [0]byte(s) a1 := [1]byte(s[1:]) // a1[0] == s[1] a2 := [2]byte(s) // a2[0] == s[0] a4 := [4]byte(s) // panics: len([4]byte) > len(s)
s0 := ([0]byte)(s) // s0 != nil s1 := ([1]byte)(s[1:]) // &s1[0] == &s[1] s2 := ([2]byte)(s) // &s2[0] == &s[0] s4 := ([4]byte)(s) // panics: len([4]byte) > len(s)
var t []string t0 := [0]string(t) // 对于空切片 t 是可以的 t1 := ([0]string)(t) // t1 == nil t2 := ([1]string)(t) // 报错: len([1]string) > len(t)
u := make([]byte, 0) u0 := (*[0]byte)(u) // u0 != nil
常量表达式
常量表达式可能只包含 常量 操作数,并且在编译时进行求值。
未类型化的布尔、数字和字符串常量可以分别用作布尔、数字或字符串类型的操作数,只要在相应的地方使用操作数是合法的。
常量比较始终产生一个无类型的布尔常量。如果常量的左操作数是无类型常量,则结果是整数常量;否则它是与左操作数相同类型的常量,左操作数必须是整数类型。
对未命名常量进行其他操作将得到相同类型的未命名常量;也就是说,布尔、整数、浮点数、复数或字符串常量。如果二元操作(除了移位操作)的未命名操作数类型不同,结果将是操作数类型列表中后出现的类型:整数、rune、浮点数、复数。例如,未命名整数常量除以未命名复数常量将得到未命名复数常量。
const a = 2 + 3.0 // a == 5.0 (未命名浮点常量) const b = 15 / 4 // b == 3 (未命名整数常量) const c = 15 / 4.0 // c == 3.75 (未命名浮点常量) const Θ float64 = 3/2 // Θ == 1.0 (类型为 float64, 3/2 是整数除法) const Π float64 = 3/2. // Π == 1.5 (类型为 float64, 3/2. 是浮点除法) const d = 1 << 3.0 // d == 8 (未命名整数常量) const e = 1.0 << 3 // e == 8 (未命名整数常量) const f = int32(1) << 33 // 非法 (常量 8589934592 溢出 int32) const g = float64(2) >> 1 // 非法 (float64(2) 是有类型的浮点常量) const h = "foo" > "bar" // h == true (未命名布尔常量) const j = true // j == true (未命名布尔常量) const k = 'w' + 1 // k == 'x' (未命名 rune 常量) const l = "hi" // l == "hi" (未命名字符串常量) const m = string(k) // m == "x" (类型为 string) const Σ = 1 - 0.707i // (未命名复数常量) const Δ = Σ + 2.0e-4 // (未命名复数常量) const Φ = iota*1i - 1/1i // (未命名复数常量)
将内置函数 complex
应用于无类型整数、符文或浮点常量会产生无类型复数常量。
const ic = complex(0, c) // ic == 3.75i (未命名复数常量) const iΘ = complex(0, Θ) // iΘ == 1i (类型 complex128)
常量表达式始终精确计算;中间值和常量本身可能需要比语言中任何预声明类型支持的精度大得多。以下是合法的声明:
const Huge = 1 << 100 // Huge == 1267650600228229401496703205376 (untyped integer constant) const Four int8 = Huge >> 98 // Four == 4 (type int8)
常数除法或取余操作的除数不能为零:
3.14 / 0.0 // 非法操作:除以零
typed 常量的值必须始终能够被常量类型的值准确表示。以下常量表达式是非法的:
uint(-1) // -1 无法表示为 uint int(3.14) // 3.14 无法表示为 int int64(Huge) // 1267650600228229401496703205376 无法表示为 int64 Four * 300 // 操作数 300 无法表示为 int8 (Four 的类型) Four * 100 // 乘积 400 无法表示为 int8 (Four 的类型)
一元位求补运算符 ^
使用的掩码符合非常量的规则:对于无符号常量,掩码全为1;对于有符号和无类型常量,掩码为-1。
^1 // 无类型整数常量,等同于 -2 uint8(^1) // 非法:等同于 uint8(-2),-2 无法表示为 uint8 ^uint8(1) // 类型化的 uint8 常量,等同于 0xFF ^ uint8(1) = uint8(0xFE) int8(^1) // 等同于 int8(-2) ^int8(1) // 等同于 -1 ^ int8(1) = -2
实现限制: 编译器在计算无类型浮点数或复数常量表达式时可能会使用四舍五入;请参阅常量部分中的实现限制。这种四舍五入可能会导致浮点常量表达式在整数上下文中无效,即使在使用无限精度计算时它可能是整数,反之亦然。
评估顺序
例如,在(函数局部)赋值中
y[f()], ok = g(h(), i()+x[j()], <-c), k()
函数调用和通信按顺序发生在f()
、h()
、i()
、j()
、<-c
、g()
和k()
。然而,这些事件与x
的评估和索引以及y
的评估的顺序未指定。
a := 1 f := func() int { a++; return a } x := []int{a, f()} // x 可能是 [1, 2] 或 [2, 2]:a 和 f() 的求值顺序未指定 m := map[int]int{a: 1, a: 2} // m 可能是 {2: 1} 或 {2: 2}:两个 map 赋值之间的求值顺序未指定 n := map[int]int{a: f()} // n 可能是 {2: 3} 或 {3: 3}:键和值之间的求值顺序未指定
在包级别,初始化依赖项会覆盖单个初始化表达式的从左到右规则,但不会覆盖每个表达式内的操作数。
var a, b, c = f() + v(), g(), sqr(u()) + v()
func f() int { return c } func g() int { return a } func sqr(x int) int { return x*x }
// 函数 u 和 v 与所有其他变量和函数无关
函数调用按顺序发生在u()
,sqr()
,v()
,f()
,v()
和g()
。
在单个表达式中,浮点运算根据运算符的结合性进行评估。显式括号通过覆盖默认的结合性来影响评估。在表达式 x + (y + z)
中,先执行加法 y + z
,然后再加上 x
。
语句
语句控制执行。
语句 = 声明 | 标签语句 | 简单语句 | Go 语句 | 返回语句 | 中断语句 | 继续语句 | 跳转语句 | 贯穿语句 | 块 | if 语句 | switch 语句 | select 语句 | for 语句 | 延迟语句 .
终止语句
终止语句会打断块中的正常控制流。以下语句属于终止语句:
- 一个"return"或者"goto"语句。
- 对内置函数panic的调用。
- 一个以终止语句结束的块。
- 一个包含以下内容的"if"语句:
- 存在"else"分支,并且
- 两个分支都是终止语句。
- 一个包含以下内容的"for"语句:
- 没有引用"for"语句的"break"语句,并且
- 循环条件不存在,并且
- "for"语句没有使用范围子句。
- 一个包含以下内容的"switch"语句:
- 没有引用"switch"语句的"break"语句,并且
- 存在默认情况,并且
- 每个情况的语句列表(包括默认情况)以终止语句或可能带标签的"fallthrough"语句结束。
- 一个包含以下内容的"select"语句:
- 没有引用"select"语句的"break"语句,并且
- 每个情况的语句列表(包括默认情况,如果存在)以终止语句结束。
- 对终止语句进行标记的标记语句。
所有其他语句都不是终止语句。
语句列表 在以下情况下以终止语句结束:如果列表不为空且其最后一个非空语句是终止语句。
空语句
空语句什么也不做。
EmptyStmt = .
标记语句
标记语句可以成为goto
、break
或continue
语句的目标。
错误:log.Panic("遇到错误")
表达式语句
ExpressionStmt = 表达式 .
以下内置函数在语句上下文中不允许使用:
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") // 如果 len 是内置函数,则非法
发送声明
send 语句在通道上发送一个值。通道表达式的 core type 必须是一个 channel,通道方向必须允许发送操作,并且要发送的值的类型必须是 assignable 给通道的元素类型。
通道和值表达式在通信开始之前都会被评估。发送操作会阻塞,直到可以进行发送。在无缓冲通道上,如果接收方准备好,发送操作可以进行。在有缓冲通道上,如果缓冲区有空间,发送操作可以进行。在关闭的通道上进行发送会导致运行时恐慌。在nil
通道上进行发送会永久阻塞。
ch <- 3 // 将值 3 发送到通道 ch
增减语句
"++" 和 "--" 语句通过无类型的 constant 1
来增加或减少其操作数。与赋值一样,操作数必须是addressable或者映射索引表达式。
IncDecStmt = 表达式 ( "++" | "--" ) .
以下是需要翻译的内容 : 'The following assignment statements are semantically equivalent:'
IncDec 语句 赋值 x++ x += 1 x-- x -= 1
赋值语句
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
元组赋值将多值操作的各个元素分配给变量列表。有两种形式。在第一种形式中,右操作数是单个多值表达式,例如函数调用、channel或map操作,或type assertion。左操作数的操作数数量必须与值的数量相匹配。例如,如果f
是一个返回两个值的函数,
x, y = f()
将第一个值赋给 x
,将第二个值赋给 y
。在第二种形式中,左侧的操作数数量必须等于右侧表达式的数量,每个表达式必须是单值的,右侧的第 n 个表达式被赋给左侧的第 n 个操作数:
one, two, three = '一', '二', '三'
空白标识符提供了一种在赋值中忽略右侧数值的方法:
_ = x // 评估 x 但忽略它 x, _ = f() // 评估 f() 但忽略第二个结果值
赋值分为两个阶段。首先,对左侧的index expressions和pointer indirections(包括selectors中的隐式指针间接引用)的操作数以及右侧的表达式都按照通常的顺序进行求值。其次,按照从左到右的顺序执行赋值操作。
a, b = b, a // 交换 a 和 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 // 设置 x[0] = 2, i = 1
x[0], x[0] = 1, 2 // 设置 x[0] = 1,然后 x[0] = 2(因此最终 x[0] == 2)
x[1],x[3] = 4,5 // 设置 x[1] = 4,然后尝试设置 x[3] = 5 时会引发错误。
type Point struct { x, y int } var p *Point x[2], p.x = 6, 7 // 设置 x[2] = 6,然后设置 p.x = 7 会导致 panic
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}
在赋值中,每个值必须可赋值给它所赋值的操作数的类型,以下是特殊情况:
如果语句
"If"语句根据布尔表达式的值指定条件执行两个分支。如果表达式求值为true,则执行"if"分支,否则(如果存在)执行"else"分支。
IfStmt = "if" [ SimpleStmt ";" ] Expression Block [ "else" ( IfStmt | Block ) ] .
如果 x > max { x = max }
表达式可能前面会有一个简单语句,在表达式求值之前执行。
如果 x := f(); x < y { return x } else if x > z { return z } else { return y }
Switch 语句
"Switch"语句提供多路执行。将表达式或类型与"switch"内的"cases"进行比较,以确定执行哪个分支。
有两种形式:表达式开关和类型开关。在表达式开关中,case 包含要与开关表达式的值进行比较的表达式。在类型开关中,case 包含要与特殊注释的开关表达式的类型进行比较的类型。在 switch 语句中,开关表达式被精确地评估一次。
表达式开关
在表达式开关中,将评估开关表达式和情况表达式,这些表达式不需要是常量,按从左到右和从上到下的顺序进行评估;与开关表达式相等的第一个触发执行相关情况的语句;其他情况将被跳过。如果没有匹配的情况,并且有一个“默认”情况,则执行其语句。最多可以有一个默认情况,并且它可以出现在“开关”语句的任何位置。缺少开关表达式等效于布尔值true
。
ExprSwitchStmt = "switch" [ SimpleStmt ";" ] [ Expression ] "{" { ExprCaseClause } "}" . ExprCaseClause = ExprSwitchCase ":" StatementList . ExprSwitchCase = "case" ExpressionList | "default" .
如果 case 表达式是无类型的,首先会被隐式转换为 switch 表达式的类型。对于每个(可能被转换的)case 表达式 x
和 switch 表达式的值 t
,x == 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(); { // 缺少 switch 表达式意味着“true
switch { case x < y: f1() case x < z: f2() case x == 4: f3() }
实现限制: 编译器可能禁止多个 case 表达式评估为相同的常量。例如,当前的编译器禁止在 case 表达式中使用重复的整数、浮点数或字符串常量。
类型开关
类型开关比较的是类型而不是值。它与表达式开关类似。它由一个特殊的开关表达式标记,其形式为type assertion,使用关键字type
而不是实际类型:
switch x.(type) { // cases }
在类型断言中,case 语句会将实际类型 T
与表达式 x
的动态类型进行匹配。与类型断言类似,x
必须是接口类型,但不能是类型参数,并且在 type switch 的每个非接口类型 T
中列出的类型都必须实现 x
的类型。在类型 switch 的 case 中列出的类型必须都是不同的。
TypeSwitchStmt = "switch" [ SimpleStmt ";" ] TypeSwitchGuard "{" { TypeCaseClause } "}" . TypeSwitchGuard = [ identifier ":=" ] PrimaryExpr "." "(" "type" ")" . TypeCaseClause = TypeSwitchCase ":" StatementList . TypeSwitchCase = "case" TypeList | "default" .
TypeSwitchGuard 可能包括短变量声明。当使用该形式时,变量在每个子句的隐式块的 TypeSwitchCase 结尾处声明。在列出恰好一个类型的子句中,变量具有该类型;否则,变量具有 TypeSwitchGuard 表达式的类型。
而不是类型,一个 case 可能使用预声明的标识符 nil;当 TypeSwitchGuard 中的表达式是 nil
接口值时,将选择该 case。最多只能有一个 nil
case。
给定类型为interface{}
的表达式x
,以下是类型判断:
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 } }
类型开关保护可能前面有一个简单语句,在保护被评估之前执行。
"fallthrough" 语句在类型开关语句中不被允许。
For 语句
"for"语句指定重复执行一个代码块。有三种形式:迭代可以由单个条件、"for"子句或"range"子句控制。
ForStmt = "for" [ 条件 | ForClause | RangeClause ] 块 . Condition = 表达式 .
对于带有单个条件的语句
在其最简单的形式中,“for”语句指定了一个块的重复执行,只要布尔条件求值为true。条件在每次迭代之前进行评估。如果条件不存在,则相当于布尔值true
。
对于 a < b { a *= 2 }
对带有 for
子句的语句
一个带有 ForClause 的“for”语句也受其条件控制,但另外它可以指定 init 和 post 语句,比如赋值语句、增量或减量语句。init 语句可以是短变量声明,但 post 语句不行。由 init 语句声明的变量在每次迭代中都会被重用。
for i := 0; i < 10; i++ { f(i) }
如果非空,则在首次迭代之前执行init语句一次;在每次执行代码块后(仅在执行了代码块时),执行post语句。ForClause的任何元素都可以为空,但必须有分号,除非只有一个条件。如果条件不存在,则相当于布尔值true
。
for cond { S() } 就相当于 for ; cond ; { S() } for { S() } 就相当于 for true { S() }
对带有 range
子句的语句
一个带有“range”子句的“for”语句会遍历数组、切片、字符串或映射的所有条目,或者接收到的通道上的值。对于每个条目,如果存在,它会将迭代值分配给相应的迭代变量,然后执行代码块。
RangeClause = [ ExpressionList "=" | IdentifierList ":=" ] "range" Expression .
"range"子句中右侧的表达式称为_range expression_,其core type必须是数组、数组指针、切片、字符串、映射或允许receive operations的通道。与赋值类似,如果存在左侧的操作数,它们必须是addressable或映射索引表达式;它们表示迭代变量。如果range表达式是通道,则最多允许一个迭代变量,否则最多可以有两个。如果最后一个迭代变量是blank identifier,则range子句等效于不带该标识符的相同子句。
范围表达式 x
在循环开始前被评估一次,但有一个例外:如果最多只有一个迭代变量存在且len(x)
是constant,则不会评估范围表达式。
左侧的函数调用在每次迭代中都会被评估。对于每次迭代,如果存在相应的迭代变量,则迭代值将按以下方式生成:
范围表达式 第一个值 第二个值
数组或切片 a [n]E, *[n]E, 或 []E 索引 i int a[i] E 字符串 s 字符串类型 索引 i int 详见下文 rune 映射 m map[K]V 键 k K m[k] V 通道 c chan E, <-chan E 元素 e E
- 对于数组、指向数组或切片值
a
,索引迭代值按递增顺序生成,从元素索引 0 开始。如果最多只有一个迭代变量存在,则 range 循环会生成从 0 到len(a)-1
的迭代值,并且不会对数组或切片本身进行索引。对于nil
切片,迭代次数为 0。 - 对于字符串值,"range" 子句会从字符串的字节索引 0 开始迭代字符串中的 Unicode 代码点。在后续迭代中,索引值将是字符串中连续 UTF-8 编码代码点的第一个字节的索引,第二个值,类型为
rune
,将是相应代码点的值。如果迭代遇到无效的 UTF-8 序列,则第二个值将是0xFFFD
,即 Unicode 替换字符,并且下一次迭代将在字符串中前进一个字节。 - 对于映射,迭代顺序未指定,并且不能保证从一次迭代到下一次迭代是相同的。如果在迭代期间删除了尚未到达的映射条目,则相应的迭代值将不会生成。如果在迭代期间创建了映射条目,则该条目可能在迭代期间生成,也可能被跳过。对于每个创建的条目,选择可能会有所不同,并且从一次迭代到下一次迭代也可能不同。如果映射为
nil
,迭代次数为 0。 - 对于通道,生成的迭代值是在通道上发送的连续值,直到通道被 关闭。如果通道为
nil
,range 表达式将永远阻塞。
迭代值被分配给相应的迭代变量,就像在赋值语句中一样。
迭代变量可以通过"range"子句使用短变量声明(:=
)的形式进行声明。在这种情况下,它们的类型被设置为相应迭代值的类型,它们的作用域是"for"语句的块;它们在每次迭代中被重复使用。如果迭代变量在"for"语句之外声明,在执行后它们的值将是最后一次迭代的值。
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{} // m 的元素类型可赋值给 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 == 迭代中遇到的最后一个 map 键 // val == map[key]
var ch chan Work = producer() for w := range ch { doWork(w) }
// 清空通道 for range ch {}
Go 语句
一个“go”语句启动一个函数调用的执行,作为独立的并发控制线程,或者称为_goroutine_,在相同的地址空间内。
GoStmt = "go" Expression .
表达式必须是一个函数或方法调用;不能加括号。内置函数的调用受限于表达式语句。
函数值和参数在调用的goroutine中按照惯例进行评估,但与常规调用不同,程序执行不会等待被调用的函数完成。相反,该函数会在新的goroutine中独立执行。当函数终止时,其goroutine也会终止。如果函数有任何返回值,在函数完成时它们将被丢弃。
go Server() go func(ch chan<- bool) { for { sleep(10); ch <- true }} (c)
选择语句
SelectStmt = "select" "{" { CommClause } "}" . CommClause = CommCase ":" StatementList . CommCase = "case" ( SendStmt | RecvStmt ) | "default" . RecvStmt = [ ExpressionList "=" | IdentifierList ":=" ] RecvExpr . RecvExpr = Expression .
一个包含RecvStmt的情况可能会将RecvExpr的结果分配给一个或两个变量,这些变量可以使用短变量声明进行声明。RecvExpr必须是一个(可能带括号的)接收操作。最多只能有一个默认情况,并且可以出现在情况列表的任何位置。
"select"语句的执行分为几个步骤:
- 对于语句中的所有情况,接收操作的通道操作数和发送语句的通道和右侧表达式在进入"select"语句时按源顺序精确计算一次。结果是一组要接收或发送的通道,以及要发送的相应值。在该评估中的任何副作用将发生,无论选择了哪个(如果有的话)通信操作来进行。具有短变量声明或赋值的RecvStmt左侧的表达式尚未计算。2. 如果一个或多个通信可以进行,将通过统一伪随机选择选择一个可以进行的通信。否则,如果存在默认情况,则选择该情况。如果没有默认情况,则"select"语句将阻塞,直到至少有一个通信可以进行。3. 除非所选情况是默认情况,否则将执行相应的通信操作。4. 如果所选情况是具有短变量声明或赋值的RecvStmt,则将评估左侧表达式并分配接收到的值(或值)。5. 执行所选情况的语句列表。
由于nil
通道上的通信永远无法进行,只有nil
通道且没有默认情况的选择将永远阻塞。
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 { // 向 c 发送随机比特序列 select { case c <- 0: // 注意:没有语句,没有穿透,没有折叠的情况 case c <- 1: } }
选择 {} // 永远阻塞
返回语句
函数F
中的"return"语句终止了F
的执行,并可选择性地提供一个或多个结果值。F
延迟执行的任何函数在F
返回给其调用者之前都会被执行。
ReturnStmt = "return" [ ExpressionList ] .
在没有结果类型的函数中,“return”语句不能指定任何结果值。
func noResult() { return }
有三种方法可以从具有结果类型的函数中返回值:
- 返回值可以在"return"语句中明确列出。每个表达式必须是单值的,并且可分配给函数结果类型的相应元素。 func simpleF() int { return 2 } func complexF1() (re float64, im float64) { return -7.0, -4.0 }
- "return"语句中的表达式列表可以是对多值函数的单个调用。其效果就好像从该函数返回的每个值都被分配给一个临时变量,其类型为相应的值类型,然后是一个列出这些变量的"return"语句,在这一点上,适用前一种情况的规则。 func complexF2() (re float64, im float64) { return complexF1() }
- 如果函数的结果类型为其结果参数指定了名称,则表达式列表可以为空。结果参数充当普通的局部变量,函数可以根据需要为它们分配值。"return"语句返回这些变量的值。 func complexF3() (re float64, im float64) { re = 7.0 im = 4.0 return } func (devnull) Write(p []byte) (n int, _ error) { n = len(p) return }
无论它们是如何声明的,所有的结果值在进入函数时都会被初始化为它们类型的零值。指定结果的“return”语句会在执行任何延迟函数之前设置结果参数。
如果在返回语句的位置上存在与结果参数同名的不同实体(常量、类型或变量),编译器可能会禁止使用空表达式列表。
func f(n int) (res int, err error) { if _, err := f(n-1); err != nil { return // invalid return statement: err is shadowed } return }
中断语句
BreakStmt = "break" [ 标签 ] .
如果有标签,那么它必须是“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"语句通过将控制权推进到循环块的末尾,开始内层最近的包含的"for"循环的下一次迭代。"for"循环必须在同一个函数内。
ContinueStmt = "continue" [ 标签 ] .
如果有一个标签,那么它必须是一个包围“for”语句的标签,并且这个标签是执行前进的标签。
RowLoop: for y, row := range rows { for x, data := range row { if data == endOfRow { continue RowLoop } row[x] = data + bias(x, y) } }
跳转语句
"goto"语句将控制转移到同一函数内具有相应标签的语句。
GotoStmt = "goto" 标签 .
转到错误
执行"goto"语句不得导致任何变量进入#声明和作用域 ,这些变量在goto语句的位置之前不在作用域内。例如,以下示例:
goto L // BAD v := 3 L:
因为跳转到标签 L
会跳过对 v
的创建,所以是错误的。
"goto" 语句在block之外不能跳转到该块内的标签。例如,以下示例:
if n%2 == 1 { goto L1 } for n > 0 { f() n-- L1: f() n-- }
因为标签 L1
在“for”语句的块内,但 goto
不在其中,所以是错误的。
fallthrough 语句
"fallthrough"语句将控制转移到表达式"switch"语句的下一个case子句的第一条语句。它只能用作这种子句中的最后一个非空语句。
FallthroughStmt = "穿透" .
延迟语句
"defer"语句调用一个函数,该函数的执行被延迟到包围函数返回的时刻,要么是因为包围函数执行了一个return statement,要么是因为包围函数执行完了其function body,要么是因为相应的goroutine正在panicking。
DeferStmt = "defer" Expression .
表达式必须是函数或方法调用;不能加括号。内置函数的调用受限于表达式语句。
每次执行“defer”语句时,函数值和调用的参数会像往常一样进行评估,然后被重新保存,但实际的函数不会被调用。相反,延迟的函数会在包围函数返回之前立即被调用,按照它们被延迟的相反顺序。也就是说,如果包围函数通过显式的return语句返回,延迟的函数会在该return语句设置任何结果参数之后执行,但在函数返回给其调用者之前执行。如果延迟的函数值评估为nil
,则在调用函数时会引发panic,而不是在执行“defer”语句时。
例如,如果延迟函数是函数文字,并且周围的函数具有命名结果参数,这些参数在文字内是可见的,延迟函数可以在它们返回之前访问和修改结果参数。如果延迟函数有任何返回值,在函数完成时它们将被丢弃。(另请参阅处理panic部分。)
lock(l) defer unlock(l) // unlocking happens before surrounding function returns
// 在包围函数返回之前打印 3 2 1 0 for i := 0; i <= 3; i++ { defer fmt.Print(i) }
// f 返回 42 func f() (result int) { defer func() { // result 在被 return 语句设置为 6 后被访问 result *= 7 }() return 6 }
内置函数
内置函数是预声明。它们像其他函数一样被调用,但其中一些接受类型而不是表达式作为第一个参数。
内置函数没有标准的 Go 类型,因此它们只能出现在 调用表达式 中;不能用作函数值。
追加和复制切片
内置函数 append
和 copy
有助于常见的切片操作。对于这两个函数,结果与参数引用的内存是否重叠无关。
variadic 函数 append
会将零个或多个值 x
添加到切片 s
,并返回与 s
相同类型的结果切片。s
的核心类型必须是类型为 []E
的切片。值 x
会传递给类型为 ...E
的参数,并适用相应的参数传递规则。作为特例,如果 s
的核心类型是 []byte
,append
还会接受一个后跟 ...
的字节串核心类型的第二个参数。这种形式会将字节片段或字符串的字节追加到切片中。
append(s S, x ...E) S // S 的核心类型是 []E
如果s
的容量不足以容纳额外的值,append
分配 一个新的、足够大的基础数组,以适应现有的切片元素和额外的值。否则,append
会重用基础数组。
s0 := []int{0, 0} s1 := append(s0, 2) // 追加单个元素 s1 is []int{0, 0, 2} s2 := append(s1, 3, 5, 7) // 追加多个元素 s2 is []int{0, 0, 2, 3, 5, 7} s3 := append(s2, s0...) // 追加切片 s3 is []int{0, 0, 2, 3, 5, 7, 0, 0} s4 := append(s3[3:6], s3[2:]...) // 追加重叠切片 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
接受 map、slice 或 type parameter 类型的参数,并删除或将所有元素清零。
调用 参数类型 结果
clear(m) map[K]T 删除所有条目,导致一个空映射 (len(m) == 0)
clear(s) []T 将长度为s
的所有元素设置为T的零值
清除(t) 类型参数 详见下文
如果clear
的参数类型是type parameter,则其类型集中的所有类型必须是映射或切片,clear
执行与实际类型参数对应的操作。
如果地图或切片为nil
,clear
操作不会执行任何操作。
关闭
对于参数ch
,它是一个核心类型的通道,内置函数close
会记录不会再有值被发送到该通道。如果ch
是只接收的通道,则会报错。向已关闭的通道发送或关闭通道会引发运行时恐慌。关闭空通道也会引发运行时恐慌。在调用close
之后,并且在接收到之前发送的所有值之后,接收操作将返回通道类型的零值而不会阻塞。多值接收操作会返回接收到的值以及通道是否已关闭的指示。
操作复数
三个函数用于组装和拆解复数。内置函数 complex
从浮点实部和虚部构造复数值,而 real
和 imag
提取复数值的实部和虚部。
complex(realPart, imaginaryPart floatT) complexT real(complexT) floatT imag(complexT) floatT
参数的类型和返回值相对应。对于complex
,两个参数必须是相同的浮点类型,返回类型是对应的浮点构成部分的复数类型:对于float32
参数,返回complex64
,对于float64
参数,返回complex128
。如果其中一个参数求值为无类型常量,则首先隐式地转换为另一个参数的类型。如果两个参数都求值为无类型常量,则它们必须是非复数或者它们的虚部必须为零,并且函数的返回值是无类型复数常量。
对于real
和imag
,参数必须是复数类型,返回类型是相应的浮点类型:对于complex64
参数,返回float32
类型,对于complex128
参数,返回float64
类型。如果参数求值为无类型常量,它必须是一个数字,函数的返回值是一个无类型浮点常量。
real
和 imag
函数一起构成了complex
的反函数,因此对于复数类型Z
的值z
,z == Z(complex(real(z), imag(z)))
。
如果这些函数的操作数都是常量,则返回值是一个常量。
var a = complex(2, -2) // complex128 const b = complex(1.0, -1.4) // 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
不允许使用类型参数类型的参数。
删除地图元素
delete(m, k) // 从映射 m 中删除元素 m[k]
如果m
的类型是type parameter,那么该类型集合中的所有类型都必须是映射,并且它们必须具有相同的键类型。
如果地图 m
是 nil
或元素 m[k]
不存在,则 delete
操作不执行任何操作。
长度和容量
内置函数 len
和 cap
接受各种类型的参数,并返回 int
类型的结果。实现保证结果始终适合于 int
类型。
调用 参数类型 结果
len(s) string类型 字符串长度(以字节为单位) [n]T, *[n]T 数组长度(== n) []T 切片长度 map[K]T map长度(已定义键的数量) chan T 通道缓冲区中排队的元素数量 类型参数 见下文
cap(s) [n]T, *[n]T 数组长度 (== n) []T 切片容量 chan T 通道缓冲容量 类型参数 见下文
如果参数类型是type parameterP
,则对于P
类型集合中的每种类型,调用len(e)
(或cap(e)
)必须有效。结果是与用于P
实例化的类型参数对应的参数的长度(或容量)。
切片的容量是指底层数组中已分配空间的元素数量。任何时候,以下关系成立:
0 <= len(s) <= cap(s)
nil
切片、映射或通道的长度为 0。nil
切片或通道的容量为 0。
如果s
是一个字符串常量,表达式len(s)
是常量。如果s
的类型是数组或指向数组的指针,并且表达式s
不包含channel receives或(非常量)function calls,那么表达式len(s)
和cap(s)
是常量;在这种情况下,s
不会被求值。否则,len
和cap
的调用不是常量,s
会被求值。
const ( c1 = imag(2i) // imag(2i) = 2.0 is a constant c2 = len([10]float64{2}) // [10]float64{2} contains no function calls c3 = len([10]float64{c1}) // [10]float64{c1} contains no function calls c4 = len([10]float64{imag(2i)}) // imag(2i) is a constant and no function call is issued c5 = len([10]float64{imag(z)}) // invalid: imag(z) is a (non-constant) function call ) var z complex128
切片、映射和通道的创建
Call 核心类型 结果
make(T, n) 切片 长度为n,容量为n的类型为T的切片 make(T, n, m) 切片 长度为n,容量为m的类型为T的切片
make(T) map T 类型的映射 make(T, n) map 具有大约 n 个元素的 T 类型的映射
make(T) channel 类型为T的非缓冲通道 make(T, n) channel 类型为T的缓冲通道,缓冲区大小为n
n
和 m
中的每个大小参数都必须是整数类型,具有仅包含整数类型的类型集,或者是无类型的常量。常量大小参数必须是非负的,并且可以由int
类型的值表示;如果它是无类型常量,则被赋予int
类型。如果提供了n
和m
并且它们都是常量,则n
必须不大于m
。对于切片和通道,如果在运行时n
为负数或大于m
,则会发生运行时恐慌。
s := make([]int, 10, 100) // 切片,len(s) == 10, cap(s) == 100 s := make([]int, 1e3) // 切片,len(s) == cap(s) == 1000 s := make([]int, 1<<63) // 非法:len(s) 无法用 int 类型的值表示 s := make([]int, 10, 0) // 非法:len(s) > cap(s) c := make(chan int, 10) // 带有缓冲区大小为 10 的通道 m := make(map[string]int, 100) // 初始空间大约为 100 个元素的映射
使用带有映射类型和大小提示 n
的 make
将创建一个具有初始空间以容纳 n
个映射元素的映射。具体行为取决于实现。
最小值和最大值
内置函数 min
和 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 均相等,min
和 max
是可交换的和可结合的:
min(x, y) == min(y, x) min(x, y, z) == min(min(x, y), z) == min(x, min(y, z))
对于浮点参数负零、NaN 和无穷大,适用以下规则:
x y min(x, y) max(x, y)
-0.0 0.0 -0.0 0.0 // 负零小于(非负)零 -Inf y -Inf y // 负无穷小于任何其他数 +Inf y y +Inf // 正无穷大于任何其他数 NaN y NaN NaN // 如果任何参数是 NaN,则结果是 NaN
对于字符串参数,min
的结果是具有最小值(或对于 max
,最大值)的第一个参数,按字节逐字比较:
min(x, y) == 若 x <= y 则返回 x 否则返回 y min(x, y, z) == min(min(x, y), z)
分配
内置函数new
接受类型T
,在运行时为该类型分配存储空间,并返回指向该存储空间的类型为*T
的值。变量的初始化方式如initial values部分所述。
新(T)
例如
type S struct { a int; b float64 } new(S)
为类型 S
的变量分配存储空间,对其进行初始化(a=0
,b=0.0
),并返回包含该位置地址的类型 *S
的值。
处理 panic
两个内置函数,panic
和 recover
,用于报告和处理运行时恐慌和程序定义的错误条件。
func panic(interface{}) func recover() interface{}
在执行函数F
时,对panic
的显式调用或运行时panic会终止F
的执行。然后,F
延迟的任何函数会像往常一样被执行。接下来,调用F
的函数延迟会被执行,依此类推,直到执行goroutine中的顶层函数的延迟。在那一点上,程序被终止并报告错误条件,包括panic
的参数值。这种终止序列称为_恐慌_。
panic(42) panic("unreachable") panic(Error("cannot parse"))
recover
函数允许程序管理恐慌的 goroutine 的行为。假设函数 G
延迟调用了函数 D
,而函数 D
中调用了 recover
,并且在执行 G
的 goroutine 中发生了 panic。当延迟函数的执行到达 D
时,D
调用 recover
的返回值将是传递给 panic
调用的值。如果 D
正常返回,没有引发新的 panic
,则恐慌序列停止。在这种情况下,G
和 panic
调用之间调用的函数的状态将被丢弃,正常执行将恢复。然后运行 G
在 D
之前延迟的任何函数,并且 G
的执行通过返回到其调用方而终止。
recover
的返回值在协程没有恐慌或者 recover
没有被延迟函数直接调用时为 nil
。相反,如果协程正在恐慌并且 recover
被延迟函数直接调用,recover
的返回值保证不是 nil
。为了确保这一点,使用 nil
接口值(或者无类型的 nil
)调用 panic
会引发 运行时恐慌。
在下面的示例中,protect
函数调用了函数参数 g
并保护调用者免受 g
引发的运行时恐慌。
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() }
引导
当前的实现在引导过程中提供了几个内置函数。这些函数为了完整性而记录,但不能保证会一直存在于语言中。它们不返回结果。
功能 行为
print 打印所有参数; 参数的格式化是特定于实现的 println 类似于print,但在参数之间打印空格,并在末尾打印换行符
实现限制:print
和 println
不需要接受任意参数类型,但必须支持布尔值、数字和字符串类型的打印。
软件包
Go 程序是通过链接在一起的 包 构建的。一个包又由一个或多个源文件构成,这些文件一起声明了属于该包的常量、类型、变量和函数,并且可以在同一包的所有文件中访问。这些元素可能会被导出,并在另一个包中使用。
源文件组织
每个源文件由一个包声明组成,用于定义它所属的包,然后是可能为空的一组导入声明,用于声明希望使用其内容的包,然后是可能为空的一组函数、类型、变量和常量的声明。
包声明
每个源文件都以包子句开头,并定义文件所属的包。
PackageName 不能是 空白标识符。
包 math
具有相同PackageName的一组文件形成了一个包的实现。一个实现可能要求一个包的所有源文件都位于同一个目录中。
导入声明
导入声明表示包含声明的源文件依赖于_imported_包的功能(§程序初始化和执行),并允许访问该包的导出标识符。导入命名一个标识符(PackageName)用于访问,以及指定要导入的包的ImportPath。
ImportDecl = "import" ( ImportSpec | "(" { ImportSpec ";" } ")" ) . ImportSpec = [ "." | PackageName ] ImportPath . ImportPath = string_lit .
PackageName 用于qualified identifiers中,用于访问导入源文件中包的导出标识符。它在file block中声明。如果省略 PackageName,则默认为导入包的package clause中指定的标识符。如果显式出现句点(.
)而不是名称,则该包中在package block中声明的所有导出标识符将在导入源文件的文件块中声明,并且必须在没有限定符的情况下访问。
ImportPath的解释取决于实现,但通常是编译包的完整文件名的子字符串,并且可能相对于已安装包的存储库。
实现限制: 编译器可以限制ImportPaths为非空字符串,只能使用属于Unicode'sL, M, N, P和S通用类别的字符(没有空格的图形字符),还可以排除字符!"#$%&'()*,:;<=>?[\]^`{|}
和Unicode替换字符U+FFFD。
考虑一个编译的包,其中包含包声明package math
,导出函数Sin
,并将编译的包安装在文件"lib/math"
中。该表说明了在导入包的文件中如何访问Sin
,以及各种类型的导入声明之后。
导入声明 Sin的本地名称
import "lib/math" math.Sin import m "lib/math" m.Sin import . "lib/math" Sin
导入声明声明了导入包和被导入包之间的依赖关系。包不能直接或间接地导入自身,也不能直接导入一个包而不引用其导出的标识符。要仅仅为了其副作用(初始化)而导入一个包,请使用 blank 作为显式包名:
import _ "lib/math"
一个示例包
这里有一个完整的 Go 包,实现了一个并发的素数筛。
包主要
import "fmt"
// 将序列 2, 3, 4, … 发送到通道 'ch'。 func generate(ch chan<- int) { for i := 2; ; i++ { ch <- i // 将 'i' 发送到通道 'ch'。 } }
// 将通道'src'中的值复制到通道'dst'中,删除那些可被'prime'整除的值。 func filter(src <-chan int, dst chan<- int, prime int) { for i := range src { // 遍历从'src'接收到的值。 if i%prime != 0 { dst <- i // 将'i'发送到通道'dst'。 } } }
// 素数筛法:将过滤器进程串联在一起。 func sieve() { ch := make(chan int) // 创建一个新的通道。 go generate(ch) // 将 generate() 作为子进程启动。 for { prime := <-ch fmt.Print(prime, "\n") ch1 := make(chan int) go filter(ch, ch1, prime) ch = ch1 } }
func main() { sieve() }
程序初始化和执行
零值
当为variable分配存储空间时,无论是通过声明还是调用new
,或者创建新值,无论是通过复合文字还是调用make
,如果没有提供显式初始化,变量或值都会被赋予默认值。这样的变量或值的每个元素都被设置为其类型的“零值”:布尔类型为false
,数值类型为0
,字符串类型为""
,指针、函数、接口、切片、通道和映射类型为nil
。此初始化是递归进行的,因此,例如,如果未指定值,则结构体数组的每个元素的字段都将被置零。
这两个简单的声明是等价的:
var i int var i int = 0
之后
type T struct { i int; f float64; next *T } t := new(T)
以下内容保持不变:
t.i == 0 t.f == 0.0 t.next == nil
同样的情况也会在之后成立
var t T
包初始化
在一个包内,包级变量的初始化是逐步进行的,每一步都选择在声明顺序中最早出现且不依赖未初始化变量的变量。
更准确地说,如果包级变量尚未初始化,并且没有初始化表达式,或者其初始化表达式不依赖于未初始化的变量,则认为该变量已准备好进行初始化。初始化过程是通过重复初始化声明顺序中最早准备好初始化的下一个包级变量进行,直到没有变量准备好进行初始化为止。
如果在此过程结束时仍有任何变量未初始化,则这些变量是一个或多个初始化循环的一部分,程序是无效的。
左侧变量声明的左侧有多个变量,右侧由单个(多值)表达式初始化,这些变量一起初始化:如果左侧的任何变量被初始化,所有这些变量都将在同一步骤中初始化。
var x = a var a, b = f() // a 和 b 在 x 初始化之前一起初始化
为了包初始化的目的,blank 变量在声明中被视为与其他变量相同。
在多个文件中声明的变量的声明顺序由文件呈现给编译器的顺序决定:在第一个文件中声明的变量在第二个文件中声明的任何变量之前声明,依此类推。为了确保可重现的初始化行为,建议构建系统以词法文件名顺序呈现属于同一包的多个文件给编译器。
依赖分析不依赖于变量的实际值,只依赖于源代码中对它们的词法引用,进行传递分析。例如,如果变量 x
的初始化表达式引用了一个函数,而该函数的主体又引用了变量 y
,那么 x
就依赖于 y
。具体来说:
- 变量或函数的引用是指代该变量或函数的标识符。
- 对方法
m
的引用是形式为t.m
的method value或method expression,其中t
的(静态)类型不是接口类型,且方法m
在t
的method set中。结果函数值t.m
是否被调用并不重要。 - 如果变量、函数或方法
x
依赖于变量y
,则x
的初始化表达式或主体(对于函数和方法)包含对y
的引用,或者包含对依赖于y
的函数或方法的引用。
例如,给定声明
a = c + b // == 9 b = f() // == 4 c = f() // == 5 d = 3 // == 5 初始化完成后
func f() int { d++ return d }
初始化顺序为 d
, b
, c
, a
。请注意,初始化表达式中子表达式的顺序是无关紧要的:a = c + b
和 a = b + c
在这个例子中会得到相同的初始化顺序。
依赖分析是针对每个包执行的;仅考虑引用当前包中声明的变量、函数和(非接口)方法。如果存在其他隐藏的数据依赖关系,则这些变量之间的初始化顺序是未指定的。
例如,给定声明
var x = I(T{}).ab() // x 依赖于 a 和 b,但是这种依赖是未被检测到的、隐藏的 var _ = sideEffect() // 与 x、a 或 b 无关 var a = b var b = 42
type I interface { ab() []int } type T struct{} func (T) ab() []int { return []int{a, b} }
变量 a
将在 b
之后初始化,但 x
是否在 b
之前、b
和 a
之间或 a
之后初始化,以及 sideEffect()
何时被调用(在 x
初始化之前还是之后)都没有明确定义。
变量也可以使用在包块中声明的名为init
的函数进行初始化,该函数不带参数,也没有结果参数。
func init() { … }
一个包内可以定义多个这样的函数,即使是在单个源文件内也可以。在包块中,init
标识符只能用于声明init
函数,但是标识符本身并没有声明。因此,init
函数在程序中的任何地方都不能被引用。
整个包的初始化是通过为所有包级变量分配初始值,然后按照它们在源代码中出现的顺序调用所有init
函数来完成的,可能涉及多个文件,这些文件是按照提交给编译器的顺序呈现的。
程序初始化
一个完整程序的包是逐步初始化的,每次初始化一个包。如果一个包有导入,那么在初始化包本身之前,导入的包会被初始化。如果多个包导入一个包,那么被导入的包只会被初始化一次。通过构造导入包的方式,可以保证不存在循环初始化依赖。更准确地说:
在给定按导入路径排序的所有软件包列表中,每一步中第一个未初始化的软件包(如果有的话)已经初始化,initialized。重复此步骤,直到所有软件包都已初始化。
包初始化——变量初始化和init
函数的调用——在单个goroutine中顺序进行,一个包一次。init
函数可能会启动其他goroutine,这些goroutine可以与初始化代码并发运行。但是,初始化始终对init
函数进行排序:它不会在前一个init
函数返回之前调用下一个init
函数。
程序执行
通过将一个名为_main package_的单个未导入的包与它传递导入的所有包链接起来,可以创建一个完整的程序。主包必须具有包名称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等效的_运行时恐慌_,其值为实现定义的接口类型runtime.Error
。该类型满足预声明的接口类型error。表示不同运行时错误条件的确切错误值是未指定的。
运行时包
type Error interface { error // and perhaps other methods }
系统考虑
包 unsafe
package unsafe
type ArbitraryType int // 用于表示任意的 Go 类型的简写;它并不是一个真正的类型 type Pointer *ArbitraryType
func Alignof(variable ArbitraryType) uintptr func Offsetof(selector ArbitraryType) uintptr func Sizeof(variable ArbitraryType) uintptr
type IntegerType int // 用于整数类型的简写;它不是一个真正的类型 func Add(ptr Pointer, len IntegerType) Pointer func Slice(ptr *ArbitraryType, len IntegerType) []ArbitraryType func SliceData(slice []ArbitraryType) *ArbitraryType func String(ptr *byte, len IntegerType) string func StringData(str string) *byte
Pointer
是一个指针类型,但Pointer
值可能无法进行解引用。任何uintptr
的指针或值都可以被转换为Pointer
的底层类型,反之亦然。在Pointer
和uintptr
之间进行转换的效果是由实现定义的。
var f float64 bits = *(*uint64)(unsafe.Pointer(&f))
type ptr unsafe.Pointer bits = *(*uint64)(ptr(&f))
var p ptr = nil
函数Alignof
和Sizeof
接受任意类型的表达式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
是 类型参数,或者它是包含可变大小元素或字段的数组或结构类型,则具有 可变大小。否则大小是 常量。如果它们的参数(或者 Offsetof
的结构 s
在选择器表达式 s.f
中的 s
)是常量大小的类型,则对 Alignof
、Offsetof
和 Sizeof
的调用是类型为 uintptr
的编译时 常量表达式。
函数Add
将len
添加到ptr
并返回更新后的指针unsafe.Pointer(uintptr(ptr) + uintptr(len))
。len
参数必须是整数类型或无类型常量。常量len
参数必须能够被int
类型的值所表示;如果它是无类型常量,则被赋予int
类型。仍然适用于Pointer
的有效用法的规则。
Slice
函数返回一个切片,其底层数组从 ptr
开始,长度和容量为 len
。Slice(ptr, len)
等同于
(*[len]ArbitraryType)(unsafe.Pointer(ptr))[:]
除非ptr
为nil
且len
为零,Slice
将返回nil
,作为特殊情况。
len
参数必须是整数类型或无类型常量。常量 len
参数必须是非负的,并且可以由 int
类型的值表示;如果它是无类型常量,则被赋予 int
类型。在运行时,如果 len
是负数,或者如果 ptr
是 nil
而 len
不为零,则会发生运行时恐慌。
函数SliceData
返回指向slice
参数的基础数组的指针。如果切片的容量cap(slice)
不为零,则该指针为&slice[:1][0]
。如果slice
为nil
,则结果为nil
。否则,它是指向未指定内存地址的非nil
指针。
String
函数返回一个 string
值,其底层字节从 ptr
开始,长度为 len
。ptr
和 len
参数的要求与 Slice
函数中相同。如果 len
为零,则结果是空字符串 ""
。由于 Go 字符串是不可变的,因此传递给 String
的字节在之后不能被修改。
StringData
函数返回 str
参数的基础字节的指针。对于空字符串,返回值是未指定的,可能是 nil
。由于 Go 字符串是不可变的,StringData
返回的字节不得被修改。
大小和对齐保证
对于 numeric types,以下大小是有保证的:
类型 大小(字节)
字节,无符号8位整数,有符号8位整数 1 无符号16位整数,有符号16位整数 2 无符号32位整数,有符号32位整数,单精度浮点数 4 无符号64位整数,有符号64位整数,双精度浮点数,复数64位 8 复数128位 16
以下是保证的最小对齐属性:
- 对于任何类型的变量
x
:unsafe.Alignof(x)
至少为 1。2. 对于结构类型的变量x
:unsafe.Alignof(x)
是所有字段x
的值unsafe.Alignof(x.f)
中的最大值,但至少为 1。3. 对于数组类型的变量x
:unsafe.Alignof(x)
与数组元素类型的变量的对齐方式相同。
如果结构体或数组类型不包含任何字段(或元素),其大小为零。两个不同的大小为零的变量可能在内存中具有相同的地址。
附录
类型统一规则
类型统一规则描述了两种类型如何统一,以及如何统一。这些精确的细节对于Go的实现是相关的,会影响错误消息的具体内容(比如编译器是否报告类型推断或其他错误),并且可能解释为什么类型推断在不寻常的代码情况下失败。但总的来说,在编写Go代码时可以忽略这些规则:类型推断旨在大部分情况下“按预期工作”,并且统一规则已经进行了精细调整。
类型统一由匹配模式控制,可以是精确或宽松。当统一递归地遍历复合类型结构时,用于类型元素的匹配模式(元素匹配模式)与匹配模式保持一致,除非两种类型进行可分配性 (≡A
) 的统一:在这种情况下,匹配模式在顶层是宽松的,但对于元素类型则变为精确,反映了可分配的类型不必完全相同。
如果以下任一条件成立,则未绑定类型参数的两种类型完全统一:
如果两种类型都是绑定类型参数,则根据给定的匹配模式进行统一,如果:
- 两个类型参数是相同的。
- 最多只有一个类型参数有已知的类型参数。在这种情况下,类型参数被“合并”:它们都代表相同的类型参数。如果两个类型参数都还没有已知的类型参数,那么为其中一个类型参数推断的未来类型参数也会同时推断给它们两个。
- 两个类型参数都有已知的类型参数,并且根据给定的匹配模式,类型参数统一。
一个单一的绑定类型参数 P
和另一个类型 T
在给定的匹配模式下统一,如果:
最后,如果两种未绑定类型参数松散地统一(并符合元素匹配模式),则:
- 两种类型完全统一。
- 一种类型是定义类型,另一种类型是类型文字,但不是接口,它们的基础类型根据元素匹配模式统一。
- 两种类型都是接口(但不是类型参数),具有相同的type terms,两者都嵌入了预声明类型comparable,相应的方法类型根据元素匹配模式统一,其中一个接口的方法集是另一个接口的方法集的子集,或者两者都不是。
- 只有一种类型是接口(但不是类型参数),两种类型的对应方法根据元素匹配模式统一,接口的方法集是另一种类型的方法集的子集。
- 两种类型具有相同的结构,它们的元素类型根据元素匹配模式统一。