Go编程语言规范
语言版本 go1.26 (2026年1月12日)
介绍
Go是一种通用语言,设计时考虑了系统编程。它具有强类型、垃圾回收和对并发编程的显式支持。程序由包构成,其属性允许高效管理依赖关系。
语法紧凑且易于解析,便于集成开发环境等自动工具进行分析。
符号
语法使用扩展巴科斯范式(EBNF)的变体指定:
产生式是由项和以下运算符构成的表达式,按优先级递增顺序:
小写产生式名称用于标识词法(终结)标记。非终结符使用驼峰命名法。词法标记用双引号""或反引号``括起来。
形式a … b表示从a到b的字符集合作为选择。水平省略号…也用于规范的其他地方,非正式地表示各种枚举或代码片段,这些未进一步指定。字符…(与三个字符...不同)不是Go语言的标记。
形式为[[Go 1.xx](/cn/docs/spec#Language_versions)]的链接表示描述的语言特性(或其某些方面)随语言版本1.xx更改或添加,因此至少需要该语言版本才能构建。详情请参阅附录中的链接部分。
源代码表示
源代码是UTF-8编码的Unicode文本。文本未规范化,因此单个重音码点是distinct的,不同于由重音和字母组合构造的相同字符;这些被视为两个码点。为简单起见,本文档将使用非限定术语字符来指代源代码中的Unicode码点。
每个码点都是distinct的;例如,大写和小写字母是不同的字符。
实现限制:为与其他工具兼容,编译器可能不允许源代码中的NUL字符(U+0000)。
实现限制:为与其他工具兼容,编译器可能忽略源代码中第一个Unicode码点的UTF-8编码字节顺序标记(U+FEFF)。字节顺序标记可能不允许出现在源代码的其他任何地方。
字符
以下术语用于表示特定的Unicode字符类别:
在Unicode标准8.0第4.5节"通用类别"中定义了一组字符类别。Go将所有属于任何字母类别Lu, Ll, Lt, Lm或Lo的字符视为Unicode字母,将属于数字类别Nd的字符视为Unicode数字。
字母和数字
下划线字符_(U+005F)被视为小写字母。
词法元素
注释作为程序文档。有两种形式:
- 行注释以字符序列
//开始,在行尾停止。 - 通用注释以字符序列
/*开始,在第一个后续字符序列*/停止。
注释不能开始于rune字面量或string字面量内部,或注释内部。不包含新行的通用注释表现为空格。任何其他注释表现为换行符。
标记
标记构成Go语言的词汇。有四类:标识符、关键字、运算符和标点符号以及字面量。由空格(U+0020)、水平制表符(U+0009)、回车(U+000D)和换行符(U+000A)形成的空白被忽略,除非它分隔本应合并成单个标记的标记。此外,换行符或文件结束可能触发分号的插入。在将输入分解为标记时,下一个标记是形成有效标记的最长字符序列。
分号
形式语法在许多产生式中使用分号";"作为终止符。Go程序可以使用以下两个规则省略大多数这些分号:
- 当输入被分解为标记时,如果行的最终标记是
- 为允许复杂语句占用单行,分号可在闭合
")"或"}"前省略。
为反映惯用法,本文档中的代码示例使用这些规则省略分号。
标识符
标识符命名程序实体,如变量和类型。标识符是一个或多个字母和数字的序列。标识符中的第一个字符必须是字母。
一些标识符是预声明的。
关键字
以下关键字是保留的,不能用作标识符。
运算符和标点符号
以下字符序列表示运算符(包括赋值运算符)和标点符号[Go 1.18]:
整数字面量
整数字面量是表示整数常量的数字序列。可选前缀设置非十进制基数:0b或0B表示二进制,0、0o或0O表示八进制,0x或0X表示十六进制[Go 1.13]。单个0被视为十进制零。在十六进制字面量中,字母a到f和A到F表示值10到15。
为便于阅读,下划线字符_可出现在基数前缀后或连续数字之间;此类下划线不改变字面量的值。
浮点字面量
浮点字面量是浮点常量的十进制或十六进制表示。
十进制浮点字面量由整数部分(十进制数字)、小数点、小数部分(十进制数字)和指数部分(e或E后跟可选符号和十进制数字)组成。整数部分或小数部分之一可省略;小数点或指数部分之一可省略。指数值exp将尾数(整数和小数部分)按10exp缩放。
十六进制浮点字面量由0x或0X前缀、整数部分(十六进制数字)、基数点、小数部分(十六进制数字)和指数部分(p或P后跟可选符号和十进制数字)组成。整数部分或小数部分之一可省略;基数点也可省略,但指数部分是必需的。(此语法符合IEEE 754-2008 §5.12.3中给出的语法。)指数值exp将尾数(整数和小数部分)按2exp缩放[Go 1.13]。
为便于阅读,下划线字符_可出现在基数前缀后或连续数字之间;此类下划线不改变字面量的值。
虚数字面量
虚数字面量表示复数常量的虚部。它由整数或浮点字面量后跟小写字母i组成。虚数字面量的值是相应整数或浮点字面量的值乘以虚数单位_i_[Go 1.13]
为向后兼容,完全由十进制数字(可能包含下划线)组成的虚数字面量的整数部分被视为十进制整数,即使它以前导0开头。
Rune字面量
Rune字面量表示rune常量,标识Unicode码点的整数值。Rune字面量表示为单引号内的单个或多个字符,如'x'或'\n'。在引号内,除换行符和未转义的单引号外,任何字符都可以出现。单引号字符表示字符本身的Unicode值,而以反斜杠开始的多字符序列以各种格式编码值。
最简单的形式表示引号内的单个字符;由于Go源代码是UTF-8编码的Unicode字符,多个UTF-8编码字节可能表示单个整数值。例如,字面量'a'包含表示字面量a(Unicode U+0061, 值0x61)的单个字节,而'ä'包含两个字节(0xc3 0xa4)表示a-分音符字面量(U+00E4, 值0xe4)。
几种反斜杠转义允许将任意值编码为ASCII文本。有四种方式将整数值表示为数值常量:\x后跟恰好两个十六进制数字;\u后跟恰好四个十六进制数字;\U后跟恰好八个十六进制数字;以及普通反斜杠\后跟恰好三个八进制数字。每种情况下,字面量的值是相应基数中数字表示的值。
尽管这些表示都导致整数,但它们有不同的有效范围。八进制转义必须表示0到255(含)之间的值。十六进制转义通过构造满足此条件。转义\u和\U表示Unicode码点,因此其中一些值是非法的,特别是那些超过0x10FFFF的值和代理半部分。
在反斜杠后,某些单字符转义表示特殊值:
在rune字面量中反斜杠后的未识别字符是非法的。
字符串字面量
字符串字面量表示通过连接字符序列获得的字符串常量。有两种形式:原始字符串字面量和解释字符串字面量。
原始字符串字面量是反引号之间的字符序列,如`foo`。在引号内,除反引号外,任何字符都可以出现。原始字符串字面量的值是引号之间未解释的(隐式UTF-8编码的)字符组成的字符串;特别是,反斜杠没有特殊含义,字符串可以包含换行符。原始字符串值中的回车字符('\r')被丢弃。
解释字符串字面量是双引号之间的字符序列,如"bar"。在引号内,除换行符和未转义的双引号外,任何字符都可以出现。引号之间的文本形成字面量的值,反斜杠转义按rune字面量中的方式解释(除了\'是非法的且\"是合法的),具有相同的限制。三位八进制(\nnn)和两位十六进制(\xnn)转义表示结果字符串的单个字节;所有其他转义表示单个字符的(可能多字节的)UTF-8编码。因此,在字符串字面量中,\377和\xFF表示值0xFF=255的单个字节,而ÿ、\u00FF、\U000000FF和\xc3\xbf表示字符U+00FF的UTF-8编码的两个字节0xc3 0xbf。
这些示例都表示相同的字符串:
如果源代码将字符表示为两个码点,如涉及重音和字母的组合形式,如果放在rune字面量中(它不是单个码点)会出错,如果放在字符串字面量中会显示为两个码点。
常量
有布尔常量、rune常量、整数常量、浮点常量、复数常量和字符串常量。Rune、整数、浮点和复数常量统称为数值常量。
常量值由rune、整数、浮点、虚数或string字面量、表示常量的标识符、常量表达式、结果为常量的转换或某些内置函数(如应用于常量参数的min或max、应用于某些值的unsafe.Sizeof、应用于某些表达式的cap或len、应用于复数常量的real和imag以及应用于数值常量的complex)的结果值表示。布尔真值由预声明常量true和false表示。预声明标识符iota表示整数常量。
一般来说,复数常量是常量表达式的一种形式,在该节中讨论。
数值常量表示任意精度的精确值,不会溢出。因此,没有常量表示IEEE 754负零、无穷大和非数值。
常量可以是类型化或未类型化。字面量常量、true、false、iota以及仅包含未类型化常量操作数的某些常量表达式是未类型化的。
常量可以通过常量声明或转换显式给定类型,或在变量声明、赋值语句中隐式给定类型,或作为表达式中的操作数。如果常量值不能表示为相应类型的值,则出错。如果类型是类型参数,常量将转换为类型参数的非常量值。
未类型化常量有默认类型,这是在需要类型化值的上下文中常量隐式转换成的类型,例如,在i := 0这样的短变量声明中,没有显式类型。未类型化常量的默认类型分别是bool、rune、int、float64、complex128或string,取决于它是布尔、rune、整数、浮点、复数还是字符串常量。
实现限制:尽管数值常量在语言中具有任意精度,但编译器可能使用有限精度的内部表示来实现它们。也就是说,每个实现必须:
- 用至少256位表示整数常量。
- 用至少256位的尾数和至少16位的有符号二进制指数表示浮点常量(包括复数常量的部分)。
- 如果无法精确表示整数常量,则给出错误。
- 如果由于溢出而无法表示浮点或复数常量,则给出错误。
- 如果由于精度限制而无法表示浮点或复数常量,则四舍五入到最接近的可表示常量。
这些要求既适用于字面量常量,也适用于常量表达式的求值结果。
变量
变量是保存值的存储位置。允许值的集合由变量的类型决定。
变量声明或函数参数和结果的函数声明或函数字面量的签名保留命名变量的存储。调用内置函数new或取复合字面量的地址在运行时分配变量的存储。这样的匿名变量通过(可能是隐式的)指针间接引用。
数组、切片和结构体类型的结构化变量有可以单独寻址的元素和字段。每个这样的元素都像变量一样。
变量的静态类型(或简称类型)是其声明中给出的类型、new调用或复合字面量中提供的类型,或结构化变量的元素的类型。接口类型变量还有不同的动态类型,它是运行时分配给变量的值的(非接口)类型(除非值是预声明标识符nil,它没有类型)。动态类型可能在执行期间变化,但存储在接口变量中的值总是可分配给变量的静态类型。
变量的值通过表达式中引用变量来检索;它是分配给变量的最新值。如果变量尚未分配值,则其值为其类型的零值。
类型
类型确定一组值以及特定于这些值的操作和方法。类型可以由类型名表示(如果有),如果是泛型类型,则必须后跟类型参数。类型也可以使用类型字面量指定,它从现有类型组合类型。
语言预声明某些类型名。其他类型通过类型声明或类型参数列表引入。复合类型—数组、结构体、指针、函数、接口、切片、映射和通道类型—可以使用类型字面量构造。
预声明类型、定义类型和类型参数称为命名类型。如果别名声明中给出的类型是命名类型,则别名表示命名类型。
布尔类型
布尔类型表示由预声明常量true和false表示的布尔真值集合。预声明布尔类型是bool;它是一种定义类型。
数值类型
整数、浮点或复数类型分别表示整数、浮点或复数值的集合。它们统称为数值类型。预声明与架构无关的数值类型有:
n位整数的值是n位宽,并使用二进制补码算术表示。
还有一组预声明具有实现特定大小的整数类型:
为避免可移植性问题,所有数值类型都是定义类型,因此除了byte(是uint8的别名)和rune(是int32的别名)外都是distinct的。当不同数值类型在表达式或赋值中混合时,需要显式转换。例如,即使int32和int在特定架构上大小相同,它们也不是相同类型。
字符串类型
字符串类型表示字符串值集合。字符串值是(可能为空的)字节序列。字节数称为字符串的长度,永不为负。字符串是不可变的:一旦创建,就无法更改字符串的内容。预声明字符串类型是string;它是一种定义类型。
字符串s的长度可以使用内置函数len发现。如果字符串是常量,则长度是编译时常量。字符串的字节可以通过整数索引0到len(s)-1访问。取这种元素的地址是非法的;如果s[i]是字符串的第i个字节,&s[i]是无效的。
数组类型
数组是称为元素类型的单一类型的编号元素序列。元素数称为数组的长度,永不为负。
长度是数组类型的一部分;它必须求值为非负常量可表示为int类型的值。数组a的长度可以使用内置函数len发现。元素可以通过整数索引0到len(a)-1寻址。数组类型始终是一维的,但可以组合形成多维类型。
数组类型T不能有类型为T的元素,或包含T作为组件的类型,直接或间接,如果这些包含类型仅是数组或结构体类型。
切片类型
切片是底层数组连续段的描述符,提供对该数组中编号元素序列的访问。切片类型表示其元素类型所有数组的切片的集合。元素数称为切片的长度,永不为负。未初始化切片的值是nil。
切片s的长度可以使用内置函数len发现;与数组不同,它在执行期间可能变化。元素可以通过整数索引0到len(s)-1寻址。给定元素在切片中的索引可能小于其在底层数组中的索引。
切片一旦初始化,总是与保存其元素的底层数组相关联。切片因此与其数组及同一数组的其他切片共享存储;相比之下,不同数组总是表示不同的存储。
切片底层数组可能延伸到切片末尾之外。容量是这种延伸的度量:它是切片长度和切片后数组长度之和;可以通过从原始切片切片新切片来创建长度达到该容量的切片。切片a的容量可以使用内置函数cap(a)发现。
给定元素类型T的新初始化切片值可以使用内置函数make创建,该函数接受切片类型和指定长度和可选容量的参数。使用make创建的切片总是分配一个新的隐藏数组,返回的切片值引用该数组。也就是说,执行
产生与分配数组并切片它相同的切片,因此这两个表达式等价:
与数组一样,切片始终是一维的,但可以组合构造高维对象。对于数组的数组,内部数组在构造上总是相同长度;然而对于切片的切片(或数组的切片),内部长度可能动态变化。此外,内部切片必须单独初始化。
结构体类型
结构体是称为字段的有名元素序列,每个元素都有名称和类型。字段名称可以显式指定(IdentifierList)或隐式指定(EmbeddedField)。在结构体中,非空白字段名称必须是唯一的。
声明了类型但没有显式字段名称的字段称为嵌入字段。嵌入字段必须指定为类型名T或指向非接口类型名*T的指针,且T本身不能是指针类型或类型参数。未限定的类型名充当字段名。
以下声明是非法的,因为结构体类型中的字段名必须是唯一的:
结构体x中嵌入字段的字段或方法 f被称为提升,如果x.f是表示该字段或方法f的合法选择器。
提升的字段表现得像结构体的普通字段,只是它们不能在复合字面量中用作字段名。
给定结构体类型S和类型名T,提升的方法按以下方式包含在结构体的方法集中:
- 如果
S包含嵌入字段T,S和*S的方法集都包含接收器为T的提升方法。*S的方法集还包含接收器为*T的提升方法。 - 如果
S包含嵌入字段*T,S和*S的方法集都包含接收器为T或*T的提升方法。
字段声明后可以跟一个可选的字符串字面量标签,它成为相应字段声明中所有字段的属性。空标签字符串等价于不存在的标签。标签通过反射接口可见,并参与结构体的类型标识,但除此之外被忽略。
结构体类型T不能包含类型为T的字段,或包含T作为组件的类型,直接或间接,如果这些包含类型仅是数组或结构体类型。
指针类型
指针类型表示给定类型变量的所有指针的集合,该类型称为指针的基类型。未初始化指针的值是nil。
函数类型
函数类型表示具有相同参数和结果类型的所有函数的集合。未初始化函数类型变量的值是nil。
在参数或结果列表中,名称(IdentifierList)必须全部存在或全部不存在。如果存在,每个名称代表指定类型的一个项目(参数或结果),且签名中非空白名称必须是唯一的。如果不存在,每个类型代表该类型的一个项目。参数和结果列表总是用括号括起来,但如果有且仅有一个未命名的结果,它可以写成未加括号的形式。
函数签名中的最后一个传入参数可以有前缀为...的类型。具有这种参数的函数称为可变参数函数,可以用零个或多个参数调用该参数。
接口类型
接口类型定义类型集。接口类型的变量可以存储类型集中任何类型的值。这种类型被称为实现接口。未初始化接口类型变量的值是nil。
接口类型由接口元素列表指定。接口元素是方法或类型元素,其中类型元素是一组或多类型项的并集。类型项是单个类型或单个潜在类型。
基本接口
在其最基本的形式中,接口指定方法(可能为空)列表。这种接口定义的类型集是实现所有这些方法的类型的集合,相应的方法集完全由接口指定的方法组成。完全由方法列表定义类型集的接口称为基本接口。
多个类型可能实现接口。例如,如果两个类型S1和S2都有方法集
(其中T代表S1或S2),那么File接口由S1和S2实现,不管S1和S2可能有的其他方法或共享的方法。
接口的每个成员类型都实现该接口。任何给定类型都可以实现几个不同的接口。例如,所有类型都实现空接口,它代表所有(非接口)类型的集合:
为方便起见,预声明类型any是空接口的别名。[Go 1.18]
类似地,考虑这个接口规范,它出现在类型声明中,定义了一个名为Locker的接口:
如果S1和S2也实现
它们也实现Locker接口以及File接口。
嵌入接口
在稍更一般的形式中,接口T可以使用(可能限定的)接口类型名E作为接口元素。这称为在T中嵌入接口E [Go 1.14]。T的类型集是T的显式声明方法和嵌入接口类型集的交集。换句话说,T的类型集是实现T所有显式声明方法和E所有方法的类型集 [Go 1.18]。
通用接口
在其最一般的形式中,接口元素也可以是任意类型项T,或形式为~T的项,指定潜在类型T,或项的并集t<sub>1</sub>|t<sub>2</sub>|…|t<sub>n</sub> [Go 1.18]。结合方法规范,这些元素使接口的类型集得以精确定义如下:
- 空接口的类型集是所有非接口类型的集合。
- 非空接口的类型集是其接口元素类型集的交集。
- 方法规范的类型集是方法集包含该方法的所有非接口类型的集合。
- 非接口类型项的类型集仅由该类型组成的集合。
- 形式为
~T的项的类型集是潜在类型为T的所有类型的集合。 - 项的并集
t<sub>1</sub>|t<sub>2</sub>|…|t<sub>n</sub>的类型集是各项类型集的并集。
"所有非接口类型的集合"不仅指程序中声明的所有(非接口)类型,还包括所有可能程序中的所有可能类型,因此是无限的。同样,给定实现特定方法的所有非接口类型,这些类型的方法集的交集将恰好包含该方法,即使程序中的所有类型总是将该方法与另一个方法配对。
通过构造,接口的类型集从不包含接口类型。
在形式为~T的项中,T的潜在类型必须是T本身,且T不能是接口。
并集元素表示类型集的并集:
形式为T或~T的项中的类型T不能是类型参数,且所有非接口项的类型集必须是两两不相交的(类型集的成对交集必须为空)。给定类型参数P:
实现限制:并集(有多个项)不能包含预声明标识符comparable或指定方法的接口,或嵌入comparable或指定方法的接口。
非基本接口只能用作类型约束,或用作其他用作约束的接口的元素。它们不能是值或变量的类型,或其他非接口类型的组件。
接口类型T不能嵌入直接或间接是T、包含T或嵌入T的类型元素。
实现接口
如果满足以下条件,类型T实现接口I:
T不是接口且是I类型集的元素;或T是接口且T的类型集是I类型集的子集。
如果T实现接口,则T类型的值实现接口。
映射类型
映射是无序的元素组,元素为一种类型(称为元素类型),由另一类型的唯一键索引(称为键类型)。未初始化映射的值是nil。
键类型的操作数必须完全定义比较操作符 ==和!=;因此键类型不能是函数、映射或切片。如果键类型是接口类型,则这些比较操作符必须为动态键值定义;失败将导致运行时恐慌。
映射中元素的数量称为其长度。对于映射m,可以使用内置函数len发现长度,且在执行期间可能变化。在执行期间,可以使用赋值添加元素,使用索引表达式检索元素;可以使用delete和clear内置函数删除元素。
可以使用内置函数make创建新的空映射值,该函数接受映射类型和一个可选容量提示作为参数:
初始容量不限制其大小:映射会增长以适应其中存储的项目数量,nil映射除外。nil映射等效于空映射,只是不能添加元素。
通道类型
通道提供了一种机制,用于并发执行的函数通过指定元素类型的发送和接收值进行通信。未初始化通道的值是nil。
可选的<-操作符指定通道的方向,发送或接收。如果指定了方向,通道是定向的,否则是双向的。通道可以通过赋值或显式转换限制为仅发送或仅接收。
<-操作符与最左边的chan关联:
可以使用内置函数make创建新的初始化通道值,该函数接受通道类型和一个可选容量作为参数:
容量(以元素数量计)设置通道中缓冲区的大小。如果容量为零或省略,通道是无缓冲的,只有当发送方和接收方都准备好时通信才成功。否则,通道是有缓冲的,只要缓冲区未满(发送)或非空(接收),通信就无需阻塞。nil通道永远无法进行通信。
通道可以使用内置函数close关闭。使用接收操作符的多值赋值形式可以报告接收的值是否在通道关闭前发送。
单个通道可以在发送语句、接收操作以及内置函数cap和len的调用中由任意数量的goroutines使用,无需进一步同步。通道充当先进先出队列。例如,如果一个goroutine在通道上发送值,第二个goroutine接收它们,则值按发送顺序接收。
类型和值的属性
值的表示
预声明类型(接口any和error除外)、数组和结构体的值是独立的:每个这样的值包含其所有数据的完整副本,且这些类型的变量存储整个值。例如,数组变量为数组的所有元素提供存储(变量)。相应的零值是特定于值类型的;它们永远不是nil。
非nil指针、函数、切片、映射和通道值包含对基础数据的引用,这些数据可能被多个值共享:
接口值可能是独立的,也可能包含对基础数据的引用,取决于接口的动态类型。预声明标识符nil是值可以包含引用的类型的零值。
当多个值共享基础数据时,更改一个值可能会更改另一个值。例如,更改切片的元素将更改底层数组中所有共享该数组切片的元素。
潜在类型
每个类型T都有一个潜在类型:如果T是预声明的布尔、数值或字符串类型之一,或者是类型字面量,则相应的潜在类型是T本身。否则,T的潜在类型是T声明中引用类型的潜在类型。对于总是接口的类型参数,该类型是它的类型约束的潜在类型。
string、A1、A2、B1和B2的潜在类型是string。[]B1、B3和B4的潜在类型是[]B1。P的潜在类型是interface{}。
类型标识
两种类型要么是相同的("相同"),要么是不同的。
命名类型总是与其他任何类型不同。否则,如果它们的潜在类型字面量结构等价,则两种类型是相同的;也就是说,它们具有相同的字面量结构,相应的组件具有相同的类型。详细来说:
- 如果两种数组类型具有相同的元素类型且长度相同,则它们是相同的。
- 如果两种切片类型具有相同的元素类型,则它们是相同的。
- 如果两种结构体类型具有相同顺序的字段,且对应字段对具有相同的名称、相同的类型、相同的标签,并且要么都是嵌入的,要么都不是嵌入的,则它们是相同的。来自不同包的未导出字段名称总是不同的。
- 如果两种指针类型具有相同的基类型,则它们是相同的。
- 如果两种函数类型具有相同数量的参数和结果值,对应的参数和结果类型是相同的,且要么都是可变参数,要么都不是,则它们是相同的。参数和结果名称不需要匹配。
- 如果两种接口类型定义相同的类型集,则它们是相同的。
- 如果两种映射类型具有相同的键和元素类型,则它们是相同的。
- 如果两种通道类型具有相同的元素类型且方向相同,则它们是相同的。
- 如果两种实例化类型的定义类型和所有类型参数都是相同的,则它们是相同的。
给定声明
以下类型是相同的:
A0、A1和[]stringA2和struct{ a, b int }A3和intA4、func(int, float64) *[]string和A5B0和C0D0[int, string]和E0[]int和[]intstruct{ a, b *B5 }和struct{ a, b *B5 }func(x int, y float64) *[]string、func(int, float64) (result *[]string)和A5
B0和B1不同,因为它们是由不同的类型定义创建的新类型;func(int, float64) *B0和func(x int, y float64) *[]string不同,因为B0不同于[]string;并且P1和P2不同,因为它们是不同类型的参数。D0[int, string]和struct{ x int; y string }不同,因为前者是实例化的定义类型,而后者是类型字面量(但它们仍然是可分配的)。
可分配性
如果满足以下条件之一,则类型V的值x是可分配给类型T的变量("x可分配给T"):
V和T是相同的。V和T具有相同的潜在类型,但不是类型参数,且V或T中至少有一个不是命名类型。V和T是具有相同元素类型的通道类型,V是双向通道,且V或T中至少有一个不是命名类型。T是接口类型,但不是类型参数,且x实现T。x是预声明标识符nil,且T是指针、函数、切片、映射、通道或接口类型,但不是类型参数。x是常量可表示为T类型的值。
此外,如果x的类型V或T是类型参数,则x可分配给类型为T的变量,如果满足以下条件之一:
x是预声明标识符nil,T是类型参数,且x可分配给T类型集中的每个类型。V不是命名类型,T是类型参数,且x可分配给T类型集中的每个类型。V是类型参数且T不是命名类型,且V类型集中的每个类型的值都可分配给T。
可表示性
如果满足以下条件之一,则常量 x可由T类型的值表示,其中T不是类型参数:
x属于由T确定的值集。T是浮点类型且x可以舍入到T的精度而不会溢出。舍入使用IEEE 754舍入到最近偶数规则,但IEEE负零进一步简化为无符号零。注意,常量值永远不会导致IEEE负零、NaN或无穷大。T是复数类型,且x的组成部分real(x)和imag(x)可由T的组成部分类型(float32或float64)的值表示。
如果T是类型参数,则x可由T类型的值表示,如果x可由T类型集中的每个类型的值表示。
方法集
类型的方法集决定了可以在该类型的操作数上调用的方法。每种类型都有一个关联的(可能为空的)方法集:
- 定义类型
T的方法集包括所有接收器类型为T的方法。 - 指向定义类型
T的指针(其中T既不是指针也不是接口)的方法集包括所有接收器为*T或T声明的方法。 - 接口类型的方法集是接口类型集中每种类型的方法集的交集(结果方法集通常只是接口中声明的方法集)。
进一步规则适用于包含嵌入字段的结构体(和指向结构体的指针),如结构体类型部分所述。任何其他类型都有空方法集。
块
块是可能为空的声明和语句序列,位于匹配的大括号内。
除了源代码中的显式块外,还有隐式块:
- 宇宙块包含所有Go源代码。
- 每个包都有一个包块,包含该包的所有Go源代码。
- 每个文件都有一个文件块,包含该文件中的所有Go源代码。
- 每个"if"、"for"和"switch"语句被认为在其自己的隐式块中。
- "switch"或"select"语句中的每个子句都充当一个隐式块。
块嵌套并影响作用域。
声明和作用域
声明将非空白标识符绑定到常量、类型、类型参数、变量、函数、标签或包。程序中每个标识符都必须声明。在同一个块中,没有标识符可以声明两次,且没有标识符可以在文件块和包块中同时声明。
空白标识符可以像其他标识符一样在声明中使用,但它不引入绑定,因此不被声明。在包块中,标识符init只能用于init函数声明,且与空白标识符一样,它不引入新的绑定。
声明标识符的作用域是源代码中标识符表示指定常量、类型、变量、函数、标签或包的范围。
Go使用块进行词法作用域:
- 预声明标识符的作用域是宇宙块。
- 在顶层(函数外部)声明的常量、类型、变量或函数(但不是方法)的标识符的作用域是包块。
- 导入包的包名的作用域是包含导入声明的文件的文件块。
- 表示方法接收器、函数参数或结果变量的标识符的作用域是函数体。
- 表示函数的类型参数或方法接收者声明的类型参数标识符的作用域从函数名后开始,到函数体结束时结束。
- 表示类型的类型参数的标识符的作用域从类型名后开始,到TypeSpec结束时结束。
- 在函数内声明的常量或变量标识符的作用域从ConstSpec或VarSpec(短变量声明的ShortVarDecl)结束时开始,到最内层包含块结束时结束。
- 在函数内声明的类型标识符的作用域从TypeSpec中的标识符开始,到最内层包含块结束时结束。
在块中声明的标识符可以在内部块中重新声明。当内部声明的标识符在作用域时,它表示内部声明。
包子句不是声明;包名不出现在任何作用域中。它的目的是标识属于同一个包的文件,并指定导入声明的默认包名。
标签作用域
标签由标签语句声明,并在"break"、"continue"和"goto"语句中使用。定义了从未使用的标签是非法的。与其他标识符不同,标签不是块作用域,且不与非标签标识符冲突。标签的作用域是声明它的函数体,不包括任何嵌套函数的体。
空白标识符
空白标识符由下划线字符_表示。它在声明、操作数和赋值语句中作为匿名占位符而不是常规(非空白)标识符使用,具有特殊含义。
预声明标识符
以下标识符在宇宙块中隐式声明 [Go 1.18] [Go 1.21]:
导出标识符
标识符可以导出,以允许从另一个包访问它。如果满足以下两个条件,则标识符是导出的:
所有其他标识符未导出。
标识符唯一性
给定一组标识符,如果一个标识符与该组中的其他标识符不同,则称该标识符为唯一。如果拼写不同,或出现在不同包中且未导出,则两个标识符不同。否则,它们是相同的。
常量声明
常量声明将标识符列表(常量名)绑定到常量表达式的值列表。标识符数量必须与表达式数量相等,且左边的第n个标识符绑定到右边的第n个表达式的值。
如果类型存在,所有常量采用指定的类型,表达式必须可分配给该类型,该类型不能是类型参数。如果省略类型,常量采用对应表达式的类型。如果表达式值是未类型化的常量,则声明的常量保持未类型化,且常量标识符表示常量值。例如,如果表达式是浮点字面量,即使字面量的分数部分为零,常量标识符也表示浮点常量。
在括号内的const声明列表中,除了第一个ConstSpec外,任何ConstSpec都可以省略表达式列表。空列表等价于文本替换前一个非空表达式列表及其类型(如果有的话)。因此,省略表达式列表等价于重复前一个列表。标识符数量必须等于前一个列表中的表达式数量。结合iota常量生成器,此机制允许轻量级顺序值声明:
Iota
在常量声明中,预声明标识符iota表示连续的无类型整数常量。其值是相应ConstSpec在该常量声明中的索引,从零开始。它可用于构造一组相关的常量:
根据定义,在同一个ConstSpec中多次使用iota具有相同的值:
最后一个例子利用了最后一个非空表达式列表的隐式重复。
类型声明
类型声明将标识符(类型名)绑定到类型。类型声明有两种形式:别名声明和类型定义。
别名声明
别名声明将标识符绑定到给定类型 [Go 1.9]。
在标识符的作用域内,它作为给定类型的别名。
如果别名声明指定了类型参数 [Go 1.24],则类型名表示泛型别名。泛型别名在使用时必须被实例化。
在别名声明中,给定类型不能是类型参数。
类型定义
类型定义创建具有相同潜在类型和操作的新定义类型,并将标识符(类型名)绑定到它。
新类型被称为定义类型。它与任何其他类型都不同,包括创建它的类型。
定义类型可以有与其关联的方法。它不从给定类型继承任何方法,但接口类型或复合类型元素的方法集保持不变:
类型定义可用于定义不同的布尔、数值或字符串类型,并与它们关联方法:
如果类型定义指定了类型参数,则类型名表示泛型类型。泛型类型在使用时必须被实例化。
在类型定义中,给定类型不能是类型参数。
泛型类型也可以有与其关联的方法。在这种情况下,方法接收器必须声明与泛型类型定义中相同数量的类型参数。
类型参数声明
类型参数列表声明泛型函数或类型声明的类型参数。类型参数列表看起来像普通的函数参数列表,除了类型参数名称都必须存在且列表用方括号而不是括号包围 [Go 1.18]。
列表中所有非空白名称必须是唯一的。每个名称声明一个类型参数,它是新的不同命名类型,作为声明中(尚未)未知类型的占位符。类型参数在泛型函数或类型被实例化时被替换为类型参数。
就像每个普通函数参数都有参数类型一样,每个类型参数都有相应的(元)类型,这被称为其类型约束。
当泛型类型的类型参数列表声明单个类型参数P且约束C使得文本P C形成有效表达式时,会产生解析歧义:
在这些罕见情况下,类型参数列表与表达式无法区分,类型声明被解析为数组类型声明。为了消除歧义,将约束嵌入接口或使用尾随逗号:
类型参数也可以由泛型类型的方法声明的接收器规范声明。
在泛型类型T的类型参数列表中,类型约束不能(直接,或间接通过另一个泛型类型的类型参数列表)引用T。
类型约束
类型约束 是一种接口,它定义了相应类型参数允许的类型参数集合,并控制该类型参数值支持的操作 [Go 1.18]。
如果约束是形如 interface{E} 的接口字面量,其中 E 是嵌入的类型元素(不是方法),在类型参数列表中,为了方便可以省略外围的 interface{ … }:
预声明的接口类型 comparable 表示所有严格可比较的非接口类型的集合 [Go 1.18]。
尽管非类型参数的接口是可比较的,但它们不是严格可比较的,因此它们不实现 comparable。但是,它们满足 comparable。
comparable 接口和(直接或间接)嵌入了 comparable 的接口只能用作类型约束。它们不能作为值或变量的类型,也不能作为其他非接口类型的组成部分。
满足类型约束
如果类型参数 T 是 C 定义的类型集合中的元素,则称类型参数 T 满足类型约束 C;换句话说,如果 T 实现了 C。作为例外,严格可比较的类型约束也可以通过可比较的(不一定严格可比较)类型参数满足 [Go 1.20]。更精确地说:
类型 T 满足 约束 C,如果
由于约束满足规则中的例外,比较类型参数类型的操作数可能在运行时 panic(即使可比较的类型参数始终是严格可比较的)。
变量声明
变量声明会创建一个或多个变量,将相应的标识符与它们绑定,并为每个变量提供类型和初始值。
如果提供了表达式列表,则按照赋值语句的规则初始化变量。否则,每个变量将初始化为它的零值。
如果存在类型,则每个变量都将被赋予该类型。否则,每个变量将获得相应初始化值的类型。如果该值是无类型常量,则首先将其隐式转换为它的默认类型;如果它是无类型的布尔值,则首先隐式转换为 bool 类型。不能使用预声明标识符 nil 来初始化没有显式类型的变量。
实现限制:如果变量从未被使用,编译器可以禁止在函数体内声明变量。
短变量声明
短变量声明 使用如下语法:
它是省略了类型的常规变量声明的简写形式:
与常规变量声明不同,短变量声明可以_重新声明_变量,前提是它们最初已在同一块中(如果块是函数体,则为参数列表)以相同类型声明,并且至少有一个非空变量是新的。因此,重新声明只能出现在多变量短声明中。重新声明不会引入新变量;它只是为原始变量分配新值。:= 左侧的非空变量名必须是唯一的。
短变量声明只能出现在函数内部。在某些上下文中,例如 "if"、"for" 或 "switch" 语句的初始化部分,它们可用于声明本地临时变量。
函数声明
函数声明将标识符(即_函数名_)与函数绑定。
如果函数的签名声明了结果参数,则函数体的语句列表必须以终止语句结束。
如果函数声明指定了类型参数,则函数名表示一个_泛型函数_。泛型函数在使用前必须实例化。
没有类型参数的函数声明可以省略函数体。这种声明提供了在 Go 外部实现的函数的签名,例如汇编程序。
方法声明
方法是具有_接收者_的函数。方法声明将标识符(即_方法名_)与方法绑定,并将方法与接收者的_基本类型_关联。
接收者通过位于方法名之前的额外参数部分指定。该参数部分必须声明一个非可变参数,即接收者。其类型必须是已定义的类型 T 或指向已定义类型 T 的指针,可能后跟用方括号括起来的类型参数名称列表 [P1, P2, …]。T 被称为接收者_基本类型_。接收者的基本类型不能是指针或接口类型,并且它必须在同一包中定义。方法被称为_绑定_到其接收者的基本类型,方法名仅在类型 T 或 *T 的选择器中可见。
非空接收者标识符在方法签名中必须是唯一的。如果接收者的值在方法体内未被引用,则可以在声明中省略其标识符。这通常也适用于函数和方法的参数。
对于基本类型,绑定到它的非空方法名必须是唯一的。如果基本类型是结构体类型,则非空方法和字段名必须不同。
给定已定义的类型 Point,声明
将方法 Length 和 Scale 与接收者类型 *Point 绑定到基本类型 Point。
如果接收者的基本类型是泛型类型,则接收者规范必须声明相应的方法使用的类型参数。这使得接收者的类型参数对方法可用。从语法上讲,这种类型参数声明看起来像接收者基本类型的实例化:类型参数必须是表示正在声明的类型参数的标识符,每个接收者基本类型的类型参数对应一个。类型参数名不需要与接收者基本类型定义中的相应参数名匹配,所有非空参数名在接收者参数部分和方法签名中必须是唯一的。接收者类型参数约束由接收者基本类型定义隐含:相应的类型参数具有相应的约束。
如果接收者类型由(指向)别名表示,则别名不能是泛型的,也不能表示实例化的泛型类型,无论是直接还是通过另一个别名间接表示,也不考虑指针间接。
表达式
表达式通过将运算符和函数应用于操作数来指定值的计算。
操作数
操作数表示表达式中的基本值。操作数可以是字面量、(可能限定的)非空标识符,表示常量、变量或函数,或者括号内的表达式。
表示泛型函数的操作数名称后面可以跟一个类型参数列表;结果操作数是实例化的函数。
实现限制:如果操作数的类型是具有空类型集合的类型参数,编译器无需报告错误。具有此类类型参数的函数无法实例化;任何尝试都将在实例化位置导致错误。
限定标识符
限定标识符 是带有包名前缀的标识符。包名和标识符都不能是空的。
限定标识符访问不同包中的标识符,该包必须导入。该标识符必须是导出的,并在该包的包块中声明。
复合字面量
复合字面量每次求值时都会为结构体、数组、切片和映射构造新值。它们由字面量的类型后跟一个用大括号括起来的元素列表组成。每个元素前面可以有相应的键。
除非 LiteralType 是类型参数,否则其底层类型必须是结构体、数组、切片或映射类型(语法强制此约束,除非类型作为 TypeName 给出)。如果 LiteralType 是类型参数,则其类型集合中的所有类型必须具有相同的底层类型,且必须是有效的复合字面量类型。元素和键的类型必须是 可赋值的到类型 T 的相应字段、元素和键类型;没有额外的转换。对于结构体字面量,键解释为字段名;对于数组和切片字面量,键解释为索引;对于映射字面量,键解释为键。对于映射字面量,所有元素必须有键。指定具有相同字段名或常量键值的多个元素是错误的。对于非常数映射键,请参阅求值顺序部分。
对于结构体字面量,适用以下规则:
- 键必须是结构体类型中声明的字段名。
- 不包含任何键的元素列表必须按字段声明的顺序列出每个结构体字段的元素。
- 如果任何元素有键,则每个元素都必须有键。
- 包含键的元素列表不需要为每个结构体字段提供元素。省略的字段将获取该字段的零值。
- 字面量可以省略元素列表;这样的字面量求值为其类型的零值。
- 为属于不同包的结构体的非导出字段指定元素是错误的。
给定声明
可以编写
对于数组和切片字面量,适用以下规则:
- 每个元素都有一个关联的整数索引,标记其在数组中的位置。
- 带键的元素使用键作为其索引。键必须是非负常量,可表示为
int类型的值;如果键有类型,则必须是整数类型。 - 不带键的元素使用前一个元素的索引加一。如果第一个元素没有键,则其索引为零。
取地址复合字面量会生成一个指向用字面量值初始化的唯一变量的指针。
注意,切片或映射类型的零值与同类型的初始化但空值不同。因此,对空切片或映射复合字面量取地址的效果与使用new分配新切片或映射值的效果不同。
数组字面量的长度是字面量类型中指定的长度。如果字面量中提供的元素少于长度,则缺失的元素将设置为数组元素类型的零值。提供超出数组索引范围的索引值是错误的。符号 ... 指定数组长度等于最大元素索引加一。
切片字面量描述整个底层数组字面量。因此,切片字面量的长度和容量是最大元素索引加一。切片字面量具有以下形式
并且是应用于数组的切片操作的简写:
在数组、切片或映射类型 T 的复合字面量中,如果元素或映射键本身是复合字面量,则可以省略相应的字面量类型(如果它与 T 的元素或键类型相同)。类似地,如果元素或键是复合字面量的地址,则可以省略 &T(当元素或键类型为 *T 时)。
当使用 LiteralType 的 TypeName 形式的复合字面量出现在 "if"、"for" 或 "switch" 语句块的关键字和左大括号之间,并且复合字面量未用括号、方括号或大括号括起来时,会出现解析歧义。在这种罕见情况下,字面量的左大括号被错误地解析为引入语句块的大括号。为解决此歧义,复合字面量必须出现在括号内。
有效数组、切片和映射字面量的示例:
函数字面量
函数字面量表示匿名函数。函数字面量不能声明类型参数。
函数字面量可以赋值给变量或直接调用。
函数字面量是_闭包_:它们可以引用周围函数中定义的变量。这些变量在周围函数和函数字面量之间共享,并且只要它们可访问,就会一直存在。
基本表达式
基本表达式是一元和二元表达式的操作数。
选择器
表示值 x(或有时是 *x;见下文)的字段或方法 f。标识符 f 称为(字段或方法)选择器;它不能是空标识符。选择器表达式的类型是 f 的类型。如果 x 是包名,请参阅限定标识符部分。
选择器 f 可以表示类型 T 的字段或方法 f,也可以引用 T 的嵌套嵌入字段的字段或方法 f。为到达 f 而遍历的嵌入字段的数量称为其在 T 中的_深度_。在 T 中声明的字段或方法 f 的深度为零。在 T 的嵌入字段 A 中声明的字段或方法 f 的深度是 f 在 A 中的深度加一。
选择器适用以下规则:
- 对于类型为
T或*T的值x,其中T不是指针或接口类型,x.f表示T中深度最浅的字段或方法。如果深度最浅的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会导致运行时恐慌。
例如,给定声明:
可以编写:
但以下无效:
方法表达式
如果 M 在类型 T 的方法集中,则 T.M 是一个函数,可以像普通函数一样调用,参数与 M 相同,但前缀一个额外参数,即方法的接收者。
考虑一个结构体类型 T,有两个方法 Mv(接收者类型为 T)和 Mp(接收者类型为 *T)。
表达式
生成一个等价于 Mv 的函数,但显式接收者作为第一个参数,其签名为
该函数可以显式接收者正常调用,因此以下五次调用等价:
类似地,表达式
生成一个表示 Mp 的函数值,签名为
对于值接收者的方法,可以派生一个显式指针接收者的函数,因此
生成一个表示 Mv 的函数值,签名为
这样的函数通过接收者间接传递值,以作为接收者传递给底层方法;方法不会覆盖函数调用中传递地址的值。
最后一种情况,为指针接收者方法派生值接收者函数,是非法的,因为指针接收者方法不在值类型的方法集中。
从方法派生的函数值使用函数调用语法调用;接收者作为调用的第一个参数提供。即,给定 f := T.Mv,f 以 f(t, 7) 调用,而非 t.f(7)。要构造绑定接收者的函数,使用函数字面量或方法值。
从接口类型的方法派生函数值是合法的。结果函数显式接收该接口类型。
方法值
如果表达式 x 具有静态类型 T,且 M 在类型 T 的方法集中,则 x.M 称为_方法值_。方法值 x.M 是一个函数值,可以像 x.M 的方法调用一样使用相同参数调用。表达式 x 在方法值求值期间被求值并保存;保存的副本随后作为接收者用于任何调用,调用可能在之后执行。
类型 T 可以是接口或非接口类型。
如方法表达式讨论中所述,考虑一个结构体类型 T,有两个方法 Mv(接收者类型为 T)和 Mp(接收者类型为 *T)。
表达式
生成一个类型为
的函数值。以下两次调用等价:
类似地,表达式
生成一个类型为
的函数值。
与选择器类似,使用指针引用值接收者的非接口方法会自动解引用该指针:pt.Mv 等价于 (*pt).Mv。
与方法调用类似,使用可寻址值引用指针接收者的非接口方法会自动取该值的地址:t.Mp 等价于 (&t).Mp。
尽管上述示例使用非接口类型,但从接口类型的值创建方法值也是合法的。
索引表达式
形如
的基本表达式表示由 x 索引的数组、指向数组的指针、切片、字符串或映射 a 的元素。值 x 分别称为_索引_或_映射键_。适用以下规则:
如果 a 不是映射也不是类型参数:
- 索引
x必须是无类型常量,或其类型必须是整数或类型参数(其类型集合仅包含整数类型) - 常量索引必须非负且可表示为
int类型的值 - 无类型常量索引将赋予类型
int - 索引
x在0 <= x < len(a)范围内为_在范围内_,否则为_超出范围_
对于数组类型 A 的 a:
对于指向数组类型的指针的 a:
a[x]是(*a)[x]的简写
对于切片类型 S 的 a:
- 如果
x在运行时超出范围,发生运行时恐慌 a[x]是指索引x的切片元素,其类型为S的元素类型
对于字符串类型的 a:
对于映射类型 M 的 a:
对于类型参数类型 P 的 a:
- 索引表达式
a[x]必须对P类型集合中所有类型的值有效。 P类型集合中所有类型的元素类型必须相同。在此上下文中,字符串类型的元素类型是byte。- 如果
P的类型集合中有映射类型,则该类型集合中的所有类型必须都是映射类型,且相应的键类型必须全部相同。 a[x]是指索引x的数组、切片或字符串元素,或键为x的映射元素,其类型为P实例化时的(相同)元素类型。- 如果
P的类型集合包含字符串类型,则a[x]不能被赋值。
否则 a[x] 非法。
在赋值语句或特殊形式初始化中使用的映射 a(类型为 map [K]V)的索引表达式
会生成一个额外的无类型布尔值。如果键 x 存在于映射中,ok 为 true,否则为 false。
向 nil 映射的元素赋值会导致运行时恐慌。
切片表达式
切片表达式从字符串、数组、指向数组的指针或切片操作数构造子字符串或切片。有两种变体:指定上下界的基本形式,以及还指定容量界限的完整形式。
如果操作数类型是类型参数,除非其类型集合包含字符串类型,否则类型集合中的所有类型必须具有相同的底层类型,且切片表达式必须对那种类型的操作数有效。如果类型集合包含字符串类型,它也可以包含底层类型为 []byte 的字节切片。此时,切片表达式必须对 string 类型的操作数有效。
基本切片表达式
对于字符串、数组、指向数组的指针或切片 a,基本表达式
构造子字符串或切片。索引 low 和 high 选择操作数 a 中出现在结果中的元素。结果从索引 0 开始,长度等于 high - low。对数组 a 切片后
切片 s 具有类型 []int,长度 3,容量 4,元素为
为方便起见,可以省略任意索引。缺失的 low 索引默认为 0;缺失的 high 索引默认为切片操作数的长度:
如果 a 是指向数组的指针,a[low : high] 是 (*a)[low : high] 的简写。
对于数组或字符串,索引在 0 <= low <= high <= len(a) 范围内为_在范围内_,否则为_超出范围_。对于切片,上界是切片容量 cap(a) 而非长度。常量索引必须非负且可表示为 int 类型的值;对于数组或常量字符串,常量索引也必须在范围内。如果两个索引都是常量,必须满足 low <= high。如果索引在运行时超出范围,发生运行时恐慌。
除了无类型字符串,如果切片操作数是字符串或切片,切片操作的结果是操作数类型的非常量值。对于无类型字符串操作数,结果是 string 类型的非常量值。如果切片操作数是数组,它必须是可寻址的,且切片操作的结果是数组元素类型的切片。
如果有效切片表达式的切片操作数是 nil 切片,结果是 nil 切片。否则,如果结果是切片,它与操作数共享底层数组。
完整切片表达式
对于数组、指向数组的指针或切片 a(但非字符串),基本表达式
构造与基本切片表达式 a[low : high] 类型相同、长度和元素相同的切片。此外,它通过设置 max - low 控制结果切片的容量。只能省略第一个索引;它默认为 0。对数组 a 切片后
切片 t 具有类型 []int,长度 2,容量 4,元素为
对于基本切片表达式,如果 a 是指向数组的指针,a[low : high : max] 是 (*a)[low : high : max] 的简写。如果切片操作数是数组,它必须是可寻址的。
索引在 0 <= low <= high <= max <= cap(a) 范围内为_在范围内_,否则为_超出范围_。常量索引必须非负且可表示为 int 类型的值;对于数组,常量索引也必须在范围内。如果多个索引是常量,存在的常量必须彼此在范围内。如果索引在运行时超出范围,发生运行时恐慌。
类型断言
对于接口类型(但不是类型参数)的表达式 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。
在赋值语句或特殊形式初始化中使用的类型断言
会生成一个额外的无类型布尔值。如果断言成立,ok 的值为 true;否则为 false,且 v 的值为类型 T 的零值。在这种情况下,不会发生运行时恐慌。
调用
给定一个函数类型 F 的表达式 f,
用参数 a1, a2, … an 调用 f。除一个特殊情况外,参数必须是可赋值给 F 的参数类型的单值表达式,并在函数调用前求值。表达式的类型是 F 的结果类型。方法调用类似,但方法本身指定为方法接收者类型值上的选择器。
如果 f 表示泛型函数,则必须实例化后才能调用或用作函数值。
如果 f 的类型是类型参数,其类型集合中的所有类型必须具有相同的底层类型,且必须是函数类型,函数调用必须对那种类型有效。
在函数调用中,函数值和参数按常规顺序求值。求值后,为函数的变量(包括参数和结果)分配新存储。然后,调用参数被_传递_给函数,这意味着它们被赋值给相应的函数参数,被调函数开始执行。函数返回时,返回参数传回调用者。
调用 nil 函数值会导致运行时恐慌。
作为特殊情况,如果函数或方法 g 的返回值数量与另一个函数或方法 f 的参数数量相同,且可单独赋值给 f 的参数,则调用 f(g(_parameters_of_g_)) 会将 g 的返回值按顺序传递给 f 的参数后调用 f。f 的调用除 g 的调用外不能有其他参数,且 g 必须至少有一个返回值。如果 f 有最终的 ... 参数,它将被分配 g 的常规参数赋值后剩余的返回值。
方法调用 x.m() 在(x 的类型的)方法集包含 m 且参数列表可赋值给 m 的参数列表时有效。如果 x 是可寻址的且 &x 的方法集包含 m,则 x.m() 是 (&x).m() 的简写:
没有独立的方法类型,也没有方法字面量。
向 ... 参数传递参数
如果 f 是可变参数且最终参数 p 的类型为 ...T,则 f 内部 p 的类型等价于 []T。如果 f 调用时没有为 p 提供实际参数,传递给 p 的值是 nil。否则,传递的值是类型 []T 的新切片,具有新底层数组,其连续元素为实际参数(都必须可赋值给 T)。因此切片的长度和容量是绑定到 p 的参数数量,每次调用可能不同。
给定函数和调用
在 Greeting 内,who 在第一次调用时为 nil,第二次调用时为 []string{"Joe", "Anna", "Eileen"}。
如果最终参数可赋值给切片类型 []T 且后跟 ...,它将原样作为 ...T 参数的值传递。此时不创建新切片。
给定切片 s 和调用
在 Greeting 内,who 将与 s 具有相同值和底层数组。
实例化
泛型函数或类型通过将_类型参数_替换为_类型实参_来_实例化_ [Go 1.18]。实例化分两步:
实例化类型产生新的非泛型命名类型;实例化函数产生新的非泛型函数。
使用泛型函数时,类型实参可以显式提供,也可以部分或完全从使用函数的上下文中推断。只要可推断,如果函数:
则可以完全省略类型实参列表。
在所有其他情况下,必须提供(可能部分)类型实参列表。如果类型实参列表缺失或部分,所有缺失的类型实参必须能从使用函数的上下文中推断。
部分类型实参列表不能为空;至少第一个实参必须存在。列表是完整类型实参列表的前缀,剩余实参待推断。通俗地说,类型实参可从"右到左"省略。
对于泛型类型,所有类型实参必须始终显式提供。
类型推断
泛型函数的使用中,如果类型实参能从使用函数的上下文中_推断_(包括函数类型参数的约束),则可以省略部分或全部类型实参。如果可推断缺失的类型实参且实例化在推断的类型实参下成功,则类型推断成功;否则类型推断失败,程序无效。
类型推断利用成对类型间的类型关系进行推断:例如,函数参数必须可赋值给对应的函数参数;这建立了参数类型与参数类型的关系。如果这两个类型之一包含类型参数,类型推断会寻找替换类型参数的实参,使可赋值关系成立。类似地,类型推断利用类型实参必须满足对应类型参数约束的事实。
每对匹配类型对应一个包含一个或多个类型参数的_类型方程_(可能来自多个泛型函数)。推断缺失类型实参意味着求解所得类型方程组对相应类型参数的值。
例如,给定
程序有效时,类型 Slice 的变量 s 必须可赋值给函数参数类型 S。为降低复杂性,类型推断忽略赋值方向性,因此 Slice 和 S 的类型关系可表示为(对称的)类型方程 Slice ≡<sub>A</sub> S(或 S ≡<sub>A</sub> Slice),其中 ≡<sub>A</sub> 中的 <sub>A</sub> 表示左右类型必须按可赋值规则匹配(详见类型统一部分)。类似地,类型参数 S 必须满足约束 ~[]E。这可表示为 S ≡<sub>C</sub> ~[]E,其中 X ≡<sub>C</sub> Y 表示 "X 满足约束 Y"。这些观察导致两个方程
现在可求解类型参数 S 和 E。由 (1) 编译器可推断 S 的类型实参是 Slice。类似地,因为 Slice 的底层类型是 []int 且 []int 必须匹配约束中的 []E,编译器可推断 E 必须是 int。因此,对这两个方程,类型推断得出
给定类型方程组,待求解的类型参数是需要实例化且未提供显式类型实参的函数的类型参数。这些类型参数称为_绑定_类型参数。例如,在 dedup 示例中,类型参数 S 和 E 绑定到 dedup。泛型函数调用的参数可以是泛型函数本身。该函数的类型参数包含在绑定类型参数集合中。函数参数的类型可能包含其他函数的类型参数(如包含函数调用的泛型函数)。这些类型参数可能出现在类型方程中,但在该上下文中不绑定。类型方程始终只针对绑定类型参数求解。
类型推断支持泛型函数调用和泛型函数赋值给(显式函数类型的)变量。这包括将泛型函数作为参数传递给其他(可能也是泛型的)函数,以及将泛型函数作为结果返回。类型推断对每种情况的特定方程组操作。方程如下(为清晰省略类型实参列表):
-
对函数调用
f(a<sub>0</sub>, a<sub>1</sub>, …),其中f或函数参数a<sub>i</sub>是泛型函数:
每对对应的函数参数(a<sub>i</sub>, p<sub>i</sub>)(其中a<sub>i</sub>不是无类型常量)生成方程typeof(p<sub>i</sub>) ≡<sub>A</sub> typeof(a<sub>i</sub>)。
如果a<sub>i</sub>是无类型常量c<sub>j</sub>且typeof(p<sub>i</sub>)是绑定类型参数P<sub>k</sub>,则对(c<sub>j</sub>, P<sub>k</sub>)单独收集,不纳入类型方程。 -
对泛型函数
f赋值给函数类型(非泛型)变量v的v = f:
typeof(v) ≡<sub>A</sub> typeof(f)。 -
对返回语句
return …, f, …,其中f是泛型函数作为结果返回给函数类型(非泛型)结果变量r:
typeof(r) ≡<sub>A</sub> typeof(f)。
此外,每个类型参数 P<sub>k</sub> 和对应约束 C<sub>k</sub> 生成类型方程 P<sub>k</sub> ≡<sub>C</sub> C<sub>k</sub>。
类型推断优先处理从类型操作数获得的信息,再考虑无类型常量。因此推断分两个阶段:
-
使用类型统一求解类型方程对绑定类型参数的值。如果统一失败,类型推断失败。
-
对每个尚未推断类型实参且收集到一个或多个对
(c<sub>j</sub>, P<sub>k</sub>)的绑定类型参数P<sub>k</sub>,确定所有对中常量c<sub>j</sub>的常量种类(方法与常量表达式相同)。P<sub>k</sub>的类型实参是确定常量种类的默认类型。如果因冲突常量种类无法确定常量种类,类型推断失败。
如果两阶段后未找到所有类型实参,类型推断失败。
如果两阶段成功,类型推断为每个绑定类型参数确定类型实参:
类型实参 A<sub>k</sub> 可以是复合类型,包含其他绑定类型参数 P<sub>k</sub> 作为元素类型(或仅为另一个绑定类型参数)。在重复简化过程中,每个类型实参中的绑定类型参数替换为对应类型实参,直到每个类型实参不含绑定类型参数。
如果类型实参通过绑定类型参数包含对自身的循环引用,简化及类型推断失败。否则,类型推断成功。
类型统一
类型推断通过_类型统一_求解类型方程。类型统一递归比较方程的左右类型(任一或两者可能是或包含绑定类型参数),寻找这些类型参数的实参,使左右类型匹配(根据上下文完全相同或可赋值兼容)。为此,类型推断维护绑定类型参数到推断类型实参的映射;该映射在类型统一过程中查询和更新。初始时,绑定类型参数已知但映射为空。类型统一过程中,如果推断新类型实参 A,则将类型参数到实参的映射 P ➞ A 添加到映射。相反,比较类型时,已知类型实参(已有映射条目的类型实参)取代对应类型参数。随着类型推断进行,映射逐渐填充,直到考虑所有方程或统一失败。如果无统一步骤失败且映射有每个类型参数的条目,则类型推断成功。
例如,给定含绑定类型参数 P 的类型方程
类型推断从空映射开始。统一首先比较左右类型的顶层结构。两者都是相同长度的数组;如果元素类型统一,则它们统一。两个元素类型都是结构体;如果字段数量、名称相同且字段类型统一,则它们统一。P 的类型实参尚不知(无映射条目),因此统一 P 和 string 将映射 P ➞ string 添加到映射。统一 list 字段类型需要统一 []P 和 []string,从而统一 P 和 string。由于此时 P 的类型实参已知(有 P 的映射条目),其类型实参 string 取代 P。由于 string 与 string 相同,此统一步骤也成功。方程左右类型的统一现在完成。由于只有一个类型方程,无统一步骤失败且映射完全填充,类型推断成功。
统一根据两种类型必须相同、可赋值兼容或仅结构相等,组合使用_精确_和_宽松_统一。详细的类型统一规则在附录中说明。
对于形如 X ≡<sub>A</sub> Y 的方程(其中 X 和 Y 是涉及赋值(包括参数传递和返回语句)的类型),顶层类型结构可宽松统一,但元素类型必须精确统一,匹配赋值规则。
对于形如 P ≡<sub>C</sub> C 的方程(其中 P 是类型参数,C 是其对应约束),统一规则更复杂:
- 如果
C的类型集合中所有类型有相同底层类型U,且P有已知类型实参A,则U和A必须宽松统一。 - 类似地,如果
C的类型集合中所有类型是相同元素类型且无冲突通道方向的通道类型,且P有已知类型实参A,则C的类型集合中最严格的通道类型与A必须宽松统一。 - 如果
P无已知类型实参且C恰好包含一个不是底层(波浪线)类型的类型项T,则统一将映射P ➞ T添加到映射。 - 如果
C无上述类型U且P有已知类型实参A,则A必须具有C的所有方法(如果有),且对应方法类型必须精确统一。
求解类型约束的类型方程时,求解一个方程可能推断额外类型实参,这些实参可能使依赖它们的方程可求解。只要推断新类型实参,类型推断就重复类型统一。
运算符
运算符将操作数组合成表达式。
比较运算在其他部分讨论。对于其他二元运算符,除非运算涉及移位或无类型常量,否则操作数类型必须相同。仅涉及常量的运算,参见常量表达式部分。
除了移位运算,如果一个操作数是无类型常量而另一个不是,常量将隐式转换为另一个操作数的类型。
移位表达式中的右操作数必须具有整数类型 [Go 1.13],或为可表示为 uint 类型值的无类型常量。如果非常量移位表达式的左操作数是无类型常量,它将首先隐式转换为移位表达式被其左操作数单独替换时所采用的类型。
运算符优先级
一元运算符具有最高优先级。由于 ++ 和 -- 运算符形成语句而非表达式,它们不属于运算符层次结构。因此,语句 *p++ 等同于 (*p)++。
二元运算符有五个优先级等级。乘法运算符绑定最强,其次是加法运算符、比较运算符、&&(逻辑与),最后是 ||(逻辑或):
相同优先级的二元运算符从左到右结合。例如,x / y * z 等同于 (x / y) * z。
算术运算符
算术运算符应用于数值并产生与第一个操作数相同类型的结果。四种标准算术运算符(+, -, *, /)适用于整数、浮点数和复数类型;+ 也适用于字符串。位逻辑和移位运算符仅适用于整数。
如果操作数类型是类型参数,运算符必须适用于该类型集合中的每种类型。操作数表示为类型参数实例化时的类型实参值,运算以该类型实参的精度计算。例如,给定函数:
乘积 x * y 和加法 s += x * y 分别以 float32 或 float64 精度计算,取决于 F 的类型实参。
整数运算符
对于两个整数值 x 和 y,整数商 q = x / y 和余数 r = x % y 满足以下关系:
其中 x / y 向零截断("截断除法")。
此规则的唯一例外是当被除数 x 是 x 的整数类型的最小负值时,商 q = x / -1 等于 x(且 r = 0),这是由于二进制补码整数溢出:
如果除数是常量,它必须不为零。如果除数在运行时为零,发生运行时恐慌。如果被除数非负且除数是 2 的常数次幂,除法可用右移替代,计算余数可用位与操作替代:
移位运算符将左操作数按右操作数指定的移位计数移位,右操作数必须非负。如果移位计数在运行时为负,发生运行时恐慌。如果左操作数是有符号整数,移位运算符实现算术移位;如果是无符号整数,实现逻辑移位。移位计数无上界。移位行为如同左操作数按移位计数 n 次每次移 1 位。因此,x << 1 等同于 x*2,x >> 1 等同于 x/2 但向负无穷截断。
对于整数操作数,一元运算符 +, -, 和 ^ 定义如下:
整数溢出
对于无符号整数值,运算 +, -, *, 和 << 按模 2n 计算,其中 n 是无符号整数类型的位宽。通俗地说,这些无符号整数运算在溢出时丢弃高位,程序可依赖"回绕"行为。
对于有符号整数,运算 +, -, *, /, 和 << 可能合法溢出,结果值由有符号整数表示、运算及其操作数确定性定义。溢出不会导致运行时恐慌。编译器不能假设溢出不会发生来优化代码。例如,不能假设 x < x + 1 始终为真。
浮点运算符
对于浮点数和复数,+x 等同于 x,而 -x 是 x 的取负。浮点或复数除以零的结果超出 IEEE 754 标准未指定;是否发生运行时恐慌取决于实现。
实现可将多个浮点运算合并为单个融合运算(可能跨语句),产生与单独执行并舍入指令获得值不同的结果。显式浮点类型转换舍入到目标类型精度,防止丢弃该舍入的融合。
例如,某些架构提供"融合乘加"(FMA)指令,计算 x*y + z 时不舍入中间结果 x*y。以下示例显示 Go 实现何时可使用该指令:
字符串连接
字符串可用 + 运算符或 += 赋值运算符连接:
字符串加法通过连接操作数创建新字符串。
比较运算符
比较运算符比较两个操作数并产生无类型布尔值。
任何比较中,第一个操作数必须可赋值给第二个操作数的类型,或反之。
相等运算符 == 和 != 适用于_可比较_类型的操作数。排序运算符 <, <=, >, 和 <= 适用于_有序_类型的操作数。这些术语和比较结果定义如下:
- 布尔类型可比较。两个布尔值相等当且仅当它们同为
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 的比较也允许,遵循上述通用规则。
类型_严格可比较_当且仅当它可比较且不是接口类型也不由接口类型组成。具体:
- 布尔、数值、字符串、指针和通道类型严格可比较。
- 结构体类型严格可比较当且仅当所有字段类型严格可比较。
- 数组类型严格可比较当且仅当数组元素类型严格可比较。
- 类型参数严格可比较当且仅当类型集合中所有类型严格可比较。
逻辑运算符
逻辑运算符应用于布尔值并产生与操作数相同类型的结果。先求值左操作数,然后根据条件需要求值右操作数。
地址运算符
对于类型 T 的操作数 x,地址运算 &x 生成指向 x 的类型 *T 的指针。操作数必须_可寻址_,即变量、指针间接或切片索引操作;或可寻址结构体操作数的字段选择器;或可寻址数组的数组索引操作。作为地址性要求的例外,x 也可以是(可能带括号的)复合字面量。如果 x 的求值会导致运行时恐慌,则 &x 的求值也会。
对于指针类型 *T 的操作数 x,指针间接 *x 表示 x 指向的类型 T 的变量。如果 x 为 nil,尝试求值 *x 将导致运行时恐慌。
接收运算符
对于通道类型的操作数 ch,接收运算 <-ch 的值是从通道 ch 接收的值。通道方向必须允许接收操作,接收运算的类型是通道的元素类型。表达式阻塞直到值可用。从 nil 通道接收永远阻塞。从关闭的通道接收总能立即进行,在接收所有先前发送的值后产生元素类型的零值。
如果操作数类型是类型参数,其类型集合中所有类型必须是允许接收操作的通道类型,且必须具有相同元素类型(即接收运算的类型)。
在赋值语句或特殊形式初始化中使用的接收表达式
会产生额外的无类型布尔结果,报告通信是否成功。如果接收的值成功发送到通道,ok 为 true;如果通道关闭且为空产生零值,则为 false。
转换
转换将表达式的类型更改为转换指定的类型。转换可能字面出现在源码中,也可能由表达式出现的上下文_隐含_。
_显式_转换是形如 T(x) 的表达式,其中 T 是类型,x 是可为类型 T 转换的表达式。
如果类型以运算符 * 或 <- 开头,或类型以关键字 func 开头且无结果列表,必要时必须加括号以避免歧义:
常量值 x 可转换为类型 T,如果 x 可表示为 T 的值。作为特殊情况,整数常量 x 可显式转换为字符串类型,使用与非常量 x 相同规则。
常量转换为非类型参数类型产生类型化常量。
常量转换为类型参数产生该类型的_非常量_值,值表示为类型参数实例化时的类型实参值。例如,给定函数:
转换 P(1.1) 产生类型 P 的非常量值,值 1.1 根据 f 的类型实参表示为 float32 或 float64。因此,如果 f 用 float32 类型实例化,表达式 P(1.1) + 1.2 的数值将以对应非常量 float32 加法的相同精度计算。
非常量值 x 可在以下任一情况下转换为类型 T:
x可赋值给T。- 忽略结构体标签(见下文),
x的类型和T都不是类型参数但有相同的底层类型。 - 忽略结构体标签(见下文),
x的类型和T都是非命名类型的指针类型,且它们的指针基类型都不是类型参数但有相同底层类型。 x的类型和T都是整数或浮点类型。x的类型和T都是复数类型。x是整数或字节或符文切片,T是字符串类型。x是字符串,T是字节或符文切片。x是切片,T是数组 [Go 1.20] 或指向数组的指针 [Go 1.17],且切片和数组类型有相同元素类型。
此外,如果 T 或 x 的类型 V 是类型参数,x 也可在以下任一条件下转换为类型 T:
V和T都是类型参数,且V类型集合中每种类型的值可转换为T类型集合中每种类型。- 只有
V是类型参数,且V类型集合中每种类型的值可转换为T。 - 只有
T是类型参数,且x可转换为T类型集合中每种类型。
结构体标签在比较结构体类型以进行转换时忽略:
特定规则适用于数值类型之间或与字符串类型之间的(非常量)转换。这些转换可能更改 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)不使用。
在所有涉及浮点或复数值的非常量转换中,如果结果类型不能表示该值,转换成功但结果值实现依赖。
与字符串类型之间的转换
-
将字节切片转换为字符串类型产生字符串,其连续字节是切片的元素。
-
将符文切片转换为字符串类型产生字符串,即各个符文值转换为字符串的连接。
-
将字符串类型的值转换为字节切片类型产生非
nil切片,其连续元素是字符串的字节。结果切片的容量实现依赖,可能大于切片长度。 -
将字符串类型的值转换为符文切片类型产生包含字符串各个 Unicode 代码点的切片。结果切片的容量实现依赖,可能大于切片长度。
-
最后,出于历史原因,整数值可转换为字符串类型。这种转换产生包含给定整数值的(可能多字节)UTF-8 表示的字符串。超出有效 Unicode 代码点范围的值转换为
"\uFFFD"。注意:这种形式的转换可能最终被语言移除。
go vet工具将某些整数到字符串的转换标记为潜在错误。应改用库函数如utf8.AppendRune或utf8.EncodeRune。
切片到数组或数组指针的转换
将切片转换为数组产生包含切片底层数组元素的数组。类似地,将切片转换为数组指针产生指向切片底层数组的指针。两种情况下,如果切片的长度小于数组长度,发生运行时恐慌。
常量表达式
常量表达式只能包含常量操作数并在编译时求值。
无类型布尔、数值和字符串常量可在任何可使用布尔、数值或字符串类型操作数的地方用作操作数。
常量比较始终产生无类型布尔常量。如果常量移位表达式的左操作数是无类型常量,结果是无类型整数常量;否则是左操作数同类型的常量,左操作数必须是整数类型。
对无类型常量的任何其他运算产生同种类的无类型常量;即布尔、整数、浮点、复数或字符串常量。如果二元运算(移位除外)的无类型操作数种类不同,结果取列表中后出现的操作数种类:整数、符文、浮点、复数。例如,无类型整数常量除以无类型复数常量产生无类型复数常量。
对无类型整数、符文或浮点常量应用内置函数 complex 产生无类型复数常量。
常量表达式始终精确求值;中间值和常量本身可能需要比语言任何预声明类型支持的显著更大的精度。以下是合法声明:
常量除法或取余操作的除数不能为零:
_类型化_常量的值必须始终能由常量类型的值精确表示。以下常量表达式非法:
无类型常量使用一元位补码运算符 ^ 的掩码匹配非常量规则:无符号常量掩码全为 1,有符号和无类型常量掩码为 -1。
实现限制:编译器计算无类型浮点或复数常量表达式时可能使用舍入;参见常量部分的实现限制。这种舍入可能导致浮点常量表达式在整数上下文中无效,即使无限精度计算时为整数值,反之亦然。
求值顺序
在包级别,初始化依赖决定变量声明中各个初始化表达式的求值顺序。否则,求值表达式、赋值或返回语句的操作数时,所有函数调用、方法调用、接收操作和二元逻辑运算按词法从左到右顺序求值。
例如,在(函数局部)赋值
函数调用和通信按顺序 f(), h()(若 z 求值为假), i(), j(), <-c, g(), 和 k() 发生。但与 x 的求值和索引及 y 和 z 的求值相比,这些事件的顺序未指定,仅词法要求除外。例如,g 的参数求值前不能调用 g。
在包级别,初始化依赖覆盖各个初始化表达式的从左到右规则,但不覆盖每个表达式内部的操作数:
函数调用顺序为 u(), sqr(), v(), f(), v(), 和 g()。
表达式内的浮点运算根据运算符结合性求值。显式括号通过覆盖默认结合性影响求值。在表达式 x + (y + z) 中,加法 y + z 在加 x 前执行。
语句
语句控制执行。
终止语句
终止语句 中断块中的常规控制流。以下语句是终止语句:
-
对内置函数
panic的调用。 -
以终止语句结尾的块。
-
"if" 语句,其中:
- 存在 "else" 分支,且
- 两个分支都是终止语句。
-
"for" 语句,其中:
- 没有引用该 "for" 语句的 "break" 语句,且
- 循环条件不存在,且
- "for" 语句不使用 range 子句。
-
"switch" 语句,其中:
- 没有引用该 "switch" 语句的 "break" 语句,
- 有默认情况,且
- 每个情况(包括默认情况)的语句列表以终止语句结尾,或带标签的"fallthrough" 语句结尾。
-
"select" 语句,其中:
- 没有引用该 "select" 语句的 "break" 语句,且
- 每个情况(包括存在的默认情况)的语句列表以终止语句结尾。
-
标记终止语句的标记语句。
其他语句都不是终止语句。
如果语句列表非空且其最终非空语句是终止语句,则称语句列表以终止语句结尾。
空语句
空语句不执行任何操作。
标记语句
标记语句可以是 goto、break 或 continue 语句的目标。
表达式语句
除特定内置函数外,函数和方法调用及接收操作可出现在语句上下文中。此类语句可加括号。
以下内置函数不允许出现在语句上下文中:
发送语句
发送语句在通道上发送值。通道表达式必须是通道类型,通道方向必须允许发送操作,且待发送值的类型必须可赋值给通道的元素类型。
通信开始前先求值通道和值表达式。通信阻塞直到发送可继续。无缓冲通道的发送可继续,如果有接收者准备就绪。缓冲通道的发送可继续,如果缓冲区有空间。关闭的通道发送会导致运行时恐慌。nil 通道发送永远阻塞。
如果通道表达式的类型是类型参数,其类型集合中所有类型必须是允许发送操作的通道类型,它们必须具有相同元素类型,且待发送值的类型必须可赋值给该元素类型。
自增自减语句
"++" 和 "--" 语句将其操作数递增或递减无类型常量 1。与赋值类似,操作数必须可寻址或映射索引表达式。
以下赋值语句在语义上等价:
赋值语句
赋值 用表达式指定的新值替换变量中存储的当前值。赋值语句可将单个值赋给单个变量,或多个值赋给匹配数量的变量。
每个左侧操作数必须可寻址、映射索引表达式,或(仅用于 = 赋值)空标识符。操作数可加括号。
赋值运算 x op= y(其中 op 是二元算术运算符)等价于 x = x op (y),但 x 仅求值一次。op= 构造是单个标记。在赋值运算中,左右两侧表达式列表都必须恰好包含一个单值表达式,且左侧表达式不能是空标识符。
元组赋值将多值操作的各个元素赋给变量列表。有两种形式。第一种形式中,右侧操作数是单个多值表达式,如函数调用、通道或映射操作,或类型断言。左侧操作数数量必须匹配值的数量。例如,若 f 是返回两个值的函数,
将第一个值赋给 x,第二个值赋给 y。第二种形式中,左侧操作数数量必须等于右侧表达式数量,每个表达式必须是单值的,第 n 个右侧表达式赋给左侧第 n 个操作数:
空标识符提供忽略赋值右侧值的方式:
赋值分两个阶段进行。首先,左侧的索引表达式和指针间接(包括选择器中的隐式指针间接)以及右侧的表达式全部按常规顺序求值。其次,赋值按从左到右的顺序执行。
赋值时,每个值必须可赋值给赋值的操作数类型,有以下特殊情况:
将值赋给变量时,只替换变量中存储的数据。如果值包含引用,赋值只复制引用而不复制引用的数据(如切片的底层数组)。
If 语句
"If" 语句根据布尔表达式的值指定两个分支的条件执行。如果表达式求值为真,执行 "if" 分支;否则执行存在的 "else" 分支。
表达式前可有简单语句,在表达式求值前执行。
Switch 语句
"Switch" 语句提供多路执行。将表达式或类型与 "switch" 内的 "cases" 比较以确定执行哪个分支。
有两种形式:表达式 switch 和类型 switch。在表达式 switch 中,cases 包含与 switch 表达式的值比较的表达式。在类型 switch 中,cases 包含与特殊标注的 switch 表达式的类型比较的类型。switch 表达式在 switch 语句中只求值一次。
表达式 switch
在表达式 switch 中,switch 表达式求值后,case 表达式(不必是常量)从左到右、从上到下求值;第一个等于 switch 表达式的 case 触发关联 case 语句的执行;其他 case 被跳过。若无 case 匹配且有 "default" case,则执行其语句。最多一个 default case,且可出现在 "switch" 语句的任何位置。缺失的 switch 表达式等价于布尔值 true。
如果 switch 表达式求值为无类型常量,先隐式转换为默认类型。预声明的无类型值 nil 不能用作 switch 表达式。switch 表达式类型必须可比较。
如果 case 表达式是无类型的,先隐式转换为 switch 表达式的类型。对于每个(可能转换的)case 表达式 x 和 switch 表达式的值 t,必须满足 x == t 是有效的比较。
换句话说,switch 表达式被视为声明并初始化无显式类型的临时变量 t;将 t 的值与每个 case 表达式 x 进行相等测试。
在 case 或 default 子句中,最后一个非空语句可以是(可能标记)"fallthrough" 语句,表示控制应从此子句末尾流到下一个子句的第一条语句。否则控制流到 "switch" 语句末尾。"fallthrough" 语句可出现在表达式 switch 除最后一个子句外的任何子句的最后语句。
switch 表达式前可有简单语句,在表达式求值前执行。
实现限制:编译器可能禁止多个 case 表达式求值为同一常量。例如,当前编译器禁止 case 表达式中出现重复的整数、浮点或字符串常量。
类型 switch
类型 switch 比较类型而非值。其他方面类似表达式 switch。它用特殊 switch 表达式标记,该表达式具有类型断言的形式,但用关键字 type 而非实际类型:
cases 将实际类型 T 与表达式 x 的动态类型比较。与类型断言类似,x 必须是接口类型,但不能是类型参数,且每个 case 中列出的非接口类型 T 必须实现 x 的类型。type switch 中 case 列出的类型必须全部不同。
TypeSwitchGuard 可包含短变量声明。使用此形式时,变量在 TypeSwitchCase 的每个子句的隐式块末尾声明。在恰好列出一个类型的 case 子句中,变量具有该类型;否则变量具有 TypeSwitchGuard 中表达式的类型。
case 可用预声明标识符 nil 代替类型;当 TypeSwitchGuard 中的表达式为 nil 接口值时选择该 case。最多一个 nil case。
给定类型为 interface{} 的表达式 x,以下类型 switch:
可重写为:
类型参数或泛型类型可用作 case 中的类型。如果实例化后该类型与其他 switch 条目重复,则选择第一个匹配的 case。
TypeSwitchGuard 前可有简单语句,在 guard 求值前执行。
类型 switch 中不允许 "fallthrough" 语句。
For 语句
"For" 语句指定块的重复执行。有三种形式:迭代可由单个条件、"for" 子句或 "range" 子句控制。
单条件 For 语句
最简单的形式中,"for" 语句指定块重复执行,只要布尔条件求值为真。每次迭代前求值条件。如果条件缺失,等价于布尔值 true。
带 ForClause 的 For 语句
带 ForClause 的 "for" 语句也由其条件控制,但额外指定 init 和 post 语句,如赋值、自增或自减语句。init 语句可以是短变量声明,但 post 语句不能。
若非空,init 语句执行一次,在首次迭代前求值条件;post 语句执行于每次块执行后(且仅在块执行时)。ForClause 的任何元素可为空,但分号要求除非仅有条件。如果条件缺失,等价于布尔值 true。
每个迭代有独立的声明变量(或变量) [Go 1.22]。首次迭代使用的变量由 init 语句声明。后续迭代使用的变量在执行 post 语句前隐式声明并初始化为前次迭代变量的值。
输出
在 [Go 1.22] 前,迭代共享一组变量而非独立变量。此时上述示例输出
带 range 子句的 For 语句
带 "range" 子句的 "for" 语句遍历数组、切片、字符串或映射的所有条目、通道上接收的值、从零到上限的整数值 [Go 1.22] 或传递给迭代器函数的 yield 函数的值 [Go 1.23]。对每个条目,如果存在,将_迭代值_赋给相应的_迭代变量_,然后执行块。
"range" 子句右侧的表达式称为 range 表达式,可以是数组、指向数组的指针、切片、字符串、允许接收操作的通道、整数或具有特定签名的函数(见下文)。与赋值类似,如果左侧存在操作数,必须是可寻址或映射索引表达式;它们表示迭代变量。如果 range 表达式是函数,迭代变量的最大数量取决于函数签名。如果 range 表达式是通道或整数,最多允许一个迭代变量;否则最多允许两个。如果最后一个迭代变量是空标识符,range 子句等价于不包含该标识符的相同子句。
Range 表达式 x 在循环开始前求值,有一个例外:如果最多一个迭代变量存在且 x 或 len(x) 是常量,则不求值 range 表达式。
每次迭代时,如果存在相应的迭代变量,按以下方式生成迭代值:
- 对于数组、指向数组的指针或切片值
a,索引迭代值按递增顺序从元素索引 0 开始生成。如果最多一个迭代变量存在,range 循环从 0 到len(a)-1生成迭代值,且不对数组或切片本身进行索引。对于nil切片,迭代次数为 0。 - 对于字符串值,"range" 子句从字节索引 0 开始迭代字符串中的 Unicode 代码点。连续迭代中,索引值将是字符串中连续 UTF-8 编码代码点的第一个字节的索引,第二个值(类型为
rune)将是相应代码点的值。如果迭代遇到无效 UTF-8 序列,第二个值将是0xFFFD(Unicode 替换字符),下一次迭代将在字符串中前进一个字节。 - 映射的迭代顺序未指定且不保证连续迭代相同。如果迭代中移除尚未到达的映射条目,不会生成对应的迭代值。如果迭代中创建映射条目,该条目可能在迭代中生成或跳过。选择可能因创建的每个条目及连续迭代而异。如果映射为
nil,迭代次数为 0。 - 对于通道,迭代值是通道上发送的连续值,直到通道关闭。如果通道为
nil,range 表达式永远阻塞。 - 对于整数值
n(其中n是整数类型或无类型整数常量),按递增顺序生成 0 到n-1的迭代值。如果n是整数类型,迭代值具有相同类型。否则,n的类型按赋值给迭代变量确定。具体地:如果迭代变量已存在,迭代值的类型是迭代变量的类型(必须是整数类型)。否则,如果迭代变量由 "range" 子句声明或不存在,迭代值的类型是n的默认类型。如果n≤ 0,循环不执行任何迭代。 - 对于函数
f,迭代通过将f与新的合成yield函数作为参数调用进行。如果yield在f返回前被调用,yield的参数成为执行循环体一次的迭代值。每次循环迭代后,yield返回true并可再次调用继续循环。只要循环体不终止,"range" 子句将持续为每次yield调用生成迭代值,直到f返回。如果循环体终止(如通过break语句),yield返回false且不得再次调用。
迭代变量可用短变量声明的形式由 "range" 子句声明(:=)。此时它们的作用域是 "for" 语句的块,且每个迭代有独立的新变量 [Go 1.22](见 "for" 语句带 ForClause 部分)。变量具有相应迭代值的类型。
如果迭代变量未由 "range" 子句显式声明,则必须已存在。此时迭代值按赋值语句的方式赋给相应变量。
如果 range 表达式的类型是类型参数,其类型集合中所有类型必须具有相同底层类型,且 range 表达式必须对该类型有效;或者,如果类型集合包含通道类型,则必须只包含具有相同元素类型的通道类型,且所有通道类型必须允许接收操作。
Go 语句
"Go" 语句在同一地址空间中作为独立并发线程(或 goroutine)启动函数调用的执行。
表达式必须是函数或方法调用;不能加括号。内置函数调用受表达式语句的限制。
函数值和参数在调用 goroutine 中按常规方式求值,但不同于常规调用,程序执行不等待被调用函数完成。相反,函数在新 goroutine 中独立执行。函数终止时其 goroutine 也终止。如果函数有返回值,函数完成时返回值被丢弃。
Select 语句
"select" 语句从一组可能的发送或接收操作中选择将进行的操作。它类似"switch"语句,但所有 case 都引用通信操作。
带 RecvStmt 的 case 可将 RecvExpr 的结果赋给一个或两个变量,可用短变量声明声明。RecvExpr 必须是(可能加括号)接收操作。最多一个 default case,可出现在 case 列表的任何位置。
"select" 语句的执行按以下步骤进行:
- 对于语句中的所有 case,接收操作的通道操作数和发送语句的通道及右侧表达式按源码顺序只求值一次,在 "select" 语句进入时。结果是接收或发送的通道集合及对应的发送值。求值中的任何副作用都会发生,无论选择哪种通信操作进行。带短变量声明或赋值的 RecvStmt 左侧表达式尚未求值。
- 如果有一个或多个通信操作可进行,则通过均匀伪随机选择选择其中可进行的一个。否则如果有 default case,则选择 default case。如果没有 default case,"select" 语句阻塞直到至少一个通信操作可进行。
- 除非选择的 case 是 default case,否则执行相应的通信操作。
- 如果选择的 case 是带短变量声明或赋值的 RecvStmt,则求值左侧表达式并赋接收值(或多个值)。
- 执行选择的 case 的语句列表。
由于 nil 通道上的通信永远无法进行,只有 nil 通道且无 default case 的 select 永远阻塞。
返回语句
函数 F 中的 "return" 语句终止 F 的执行,并可选择提供多个结果值。任何由 F 延迟的函数在 F 返回调用者前执行。
在无结果类型的函数中,"return" 语句不得指定结果值。
有结果类型的函数返回值有三种方式:
-
返回值可在 "return" 语句中显式列出。每个表达式必须单值且可赋值给函数结果类型的对应元素。
-
"return" 语句中的表达式列表可以是多值函数的单个调用。效果如同该函数返回的每个值都赋给具有相应值的临时变量,然后 "return" 语句列出这些变量,此时适用前一情况的规则。
-
如果函数的结果类型指定了结果参数的名称,则表达式列表可为空。结果参数充当普通局部变量,函数可按需赋给它们值。"return" 语句返回这些变量的值。
无论声明方式如何,所有结果值在函数入口时都初始化为类型的零值。指定结果的 "return" 语句在延迟函数执行前设置结果参数。
实现限制:如果结果参数同名且不同实体(常量、类型或变量)在 return 位置作用域内,则编译器可能禁止 "return" 语句中的空表达式列表。
Break 语句
"break" 语句终止同一函数内最内层"for"、"switch"或"select"语句的执行。
如果有标签,必须是包含的 "for"、"switch" 或 select 语句的标签,且终止该语句的执行。
Continue 语句
"continue" 语句通过将控制推进到循环块末尾开始最内层包含"for" 循环的下一次迭代。"for" 循环必须在同一函数内。
如果有标签,必须是包含的 "for" 语句的标签,且推进该语句的执行。
Goto 语句
"goto" 语句在同一函数内将控制转移到对应标签的语句。
执行 "goto" 语句不得导致任何变量进入作用域,而这些变量在 goto 位置尚未在作用域内。例如,以下示例:
错误,因为跳转到标签 L 跳过了 v 的创建。
"goto" 语句在块外不得跳转到该块内的标签。例如,以下示例:
错误,因为标签 L1 在 "for" 语句的块内,但 "goto" 在块外。
Fallthrough 语句
"fallthrough" 语句将控制转移到表达式 "switch" 语句的下一个 case 子句的第一条语句。只能作为此类子句的最后一个非空语句使用。
Defer 语句
"defer" 语句调用函数,其执行延迟到周围函数返回时,无论周围函数通过执行返回语句、到达函数体末尾或因相应 goroutine panicking而返回。
表达式必须是函数或方法调用;不能加括号。内置函数调用受表达式语句的限制。
每次执行 "defer" 语句时,函数值和调用参数按常规方式求值并保存,但不立即调用函数。相反,延迟函数在周围函数返回前立即调用,按延迟顺序的逆序调用。即,如果周围函数通过显式返回语句返回,则延迟函数在结果参数由返回语句设置后但在函数返回调用者前执行。如果延迟函数求值为 nil,函数调用时执行panic,而非 "defer" 语句执行时。
例如,如果延迟函数是函数字面量且周围函数有命名结果参数在字面量作用域内,延迟函数可在结果参数返回前访问和修改结果参数。如果延迟函数有返回值,函数完成时返回值被丢弃。(另见处理 panic部分。)
内置函数
内置函数预声明。它们像其他函数一样调用,但其中一些函数接受类型而非表达式作为第一个参数。
内置函数没有标准 Go 类型,因此只能出现在调用表达式中;不能用作函数值。
切片追加和复制
内置函数 append 和 copy 辅助切片常用操作。对于这两个函数,结果与参数引用的内存是否重叠无关。
可变参数函数 append 将零个或多个值 x 追加到类型 S 的切片 s 并返回结果切片(也是类型 S)。值 x 传递给类型 ...E 的参数,其中 E 是 S 的元素类型,适用相应的参数传递规则。作为特殊情况,append 还接受可赋值给 []byte 类型的第一个参数,以及字符串类型的第二个参数后跟 ...。这种形式追加字符串的字节。
如果 S 是类型参数,其类型集合中所有类型必须具有相同底层切片类型 []E。
如果 s 的容量不足以容纳附加值,append 会分配新底层数组,其大小足够容纳现有切片元素和附加值。否则 append 重用底层数组。
函数 copy 将源切片 src 的元素复制到目标切片 dst 并返回复制元素的数量。两个参数必须具有相同元素类型 E 且必须可赋值给类型 []E 的切片。复制的元素数量是 len(src) 和 len(dst) 的最小值。作为特殊情况,copy 还接受可赋值给 []byte 类型的目标参数和字符串类型的源参数。这种形式将字符串的字节复制到字节切片。
如果一个或两个参数的类型是类型参数,其相应类型集合中所有类型必须具有相同底层切片类型 []E。
示例:
清除
内置函数 clear 接受映射、切片或类型参数类型的参数,并删除或清零所有元素 [Go 1.21]。
如果 clear 的参数类型是类型参数,其类型集合中所有类型必须是映射或切片,且 clear 执行与实际类型参数对应的操作。
如果映射或切片为 nil,clear 是无操作。
关闭
对于通道 ch,内置函数 close(ch) 记录通道上不再发送值。如果 ch 是只接收通道,则错误。向关闭的通道发送或关闭 nil 通道也会导致运行时恐慌。调用 close 后,任何先前发送的值接收后,接收操作将返回通道类型的零值而不阻塞。多值接收操作返回接收值及通道是否关闭的指示。
如果 close 的参数类型是类型参数,其类型集合中所有类型必须是具有相同元素类型的通道。如果这些通道中有只接收通道,则错误。
复数操作
三个函数用于构造和分解复数。内置函数 complex 从浮点实部和虚部构造复数,而 real 和 imag 提取复数的实部和虚部。
参数和返回值的类型对应。对于 complex,两个参数必须是相同浮点类型,返回值是相应复数类型:float32 参数返回 complex64,float64 参数返回 complex128。如果参数求值为无类型常量,则先隐式转换为另一个参数的类型。如果两个参数都求值为无类型常量,它们必须是非复数或其虚部为零,函数返回无类型复数常量。
对于 real 和 imag,参数必须是复数类型,返回值是相应浮点类型:complex64 参数返回 float32,complex128 参数返回 float64。如果参数求值为无类型常量,它必须是复数,函数返回无类型浮点常量。
real 和 imag 函数共同构成 complex 的逆,对于复数类型 Z 的值 z,z == Z(complex(real(z), imag(z)))。
如果这些函数的所有操作数都是常量,返回值是常量。
类型参数类型的参数不允许。
映射元素删除
内置函数 delete 从映射 m 中删除键为 k 的元素。值 k 必须可赋值给 m 的键类型。
如果 m 的类型是类型参数,其类型集合中所有类型必须是映射,且它们必须具有相同键类型。
如果映射 m 为 nil 或元素 m[k] 不存在,delete 是无操作。
长度和容量
内置函数 len 和 cap 接受各种类型的参数并返回 int 类型的结果。实现保证结果始终适合 int。
如果参数类型是类型参数 P,则调用 len(e)(或 cap(e))对 P 类型集合中的每种类型都必须有效。结果是 P 被实例化时对应类型参数的参数的长度(或容量)。
切片的容量是底层数组中已分配空间可容纳的元素数量。任何时候满足:
nil 切片、映射或通道的长度为 0。nil 切片或通道的容量为 0。
如果 s 是字符串常量,表达式 len(s) 是常量。如果 s 的类型是数组或指向数组的指针,并且表达式 s 不包含通道接收操作或(非常量)函数调用,那么表达式 len(s) 和 cap(s) 也是常量;在这种情况下,s 不会被求值。否则,调用 len 和 cap 不是常量,并且 s 会被求值。
创建切片、映射和通道
内建函数 make 接受一个类型 T,该类型必须是切片、映射或通道类型,或者是一个类型参数,后面可选择跟随特定类型的表达式列表。它返回类型为 T 的值(不是 *T)。内存的初始化在初始值部分有描述。
如果第一个参数是类型参数,其类型集的所有类型必须具有相同的基础类型,该基础类型必须是切片或映射类型,或者,如果是通道类型,则必须是相同元素类型的通道类型,并且通道方向不能冲突。
每个大小参数 n 和 m 必须是整数类型,其类型集只包含整数类型,或者是一个未类型化的常量。常量大小参数必须是非负数,并且可以由类型 int 的值表示;如果是未类型化的常量,则其类型被指定为 int。如果 n 和 m 都提供且为常量,则 n 必须不大于 m。对于切片和通道,如果 n 在运行时是负数或大于 m,则会发生运行时恐慌。
使用映射类型和大小提示 n 调用 make 将创建初始空间可容纳 n 个映射元素的映射。具体行为依赖于实现。
最小值和最大值
内建函数 min 和 max 计算一组固定数量有序类型参数的最小值(或最大值)。必须至少有一个参数 [Go 1.21]。
与运算符相同的类型规则适用:对于有序参数 x 和 y,如果 x + y 有效,则 min(x, y) 有效,且 min(x, y) 的类型与 x + y 的类型相同(对于 max 也类似)。如果所有参数都是常量,则结果为常量。
对于数值参数,假设所有 NaN 相等,min 和 max 是可交换和可结合的:
对于浮点参数负零、NaN 和无穷大,适用以下规则:
对于字符串参数,min 的结果是字典序最小(或 max 最大)的第一个参数:
分配
内建函数 new 接受一个类型 T,在运行时为该类型的变量分配存储空间,并返回一个指向该变量的类型为 *T 的指针值。变量按初始值部分描述进行初始化。
例如
为类型 S 的变量分配存储空间,将其初始化(a=0,b=0.0),并返回一个包含该位置地址的 *S 类型的值。
处理恐慌
两个内建函数 panic 和 recover 协助报告和处理运行时恐慌和程序定义的错误条件。
在执行函数 F 时,显式调用 panic 或运行时恐慌会终止 F 的执行。然后,F 延迟执行的所有函数照常执行。接下来,F 的调用者延迟执行的所有函数被执行,依此类推,直到执行 goroutine 中的顶层函数的延迟函数。此时,程序终止,并报告错误条件,包括传递给 panic 的参数值。这个终止序列称为 panicking。
recover 函数允许程序管理 panic 的 goroutine 的行为。假设函数 G 延迟执行一个调用 recover 的函数 D,并且在与 G 执行的相同 goroutine 中的某个函数中发生 panic。当延迟函数的执行到达 D 时,D 的 recover 调用的返回值将是传递给 panic 调用的值。如果 D 正常返回,没有启动新的 panic,则 panicking 序列停止。在这种情况下,丢弃 G 和 panic 调用之间的函数状态,恢复正常执行。然后运行 G 在 D 之前延迟的所有函数,G 的执行通过返回其调用者而终止。
如果 goroutine 没有发生 panic,或者 recover 不是由延迟函数直接调用,则 recover 的返回值为 nil。相反,如果 goroutine 正在 panic,且 recover 是由延迟函数直接调用的,则 recover 的返回值保证不为 nil。为了确保这一点,使用 nil 接口值(或未类型化的 nil)调用 panic 会导致运行时恐慌。
以下示例中的 protect 函数调用函数参数 g,并保护调用者免受由 g 引起的运行时恐慌。
启动
当前实现提供了几个在启动期间有用的内建函数。这些函数为了完整性而记录,但不保证会保留在语言中。它们不返回结果。
实现限制:print 和 println 不需要接受任意参数类型,但必须支持布尔、数值和字符串类型的打印。
包
Go 程序通过连接多个_包_来构建。一个包由多个源文件组成,这些源文件共同声明了属于该包的常量、类型、变量和函数,并且这些元素在同一个包的所有文件中都是可访问的。这些元素可以被导出并在其他包中使用。
源文件组织
每个源文件由定义其所属包的包子句组成,后跟一组可能为空的导入声明,声明希望使用的包的内容,然后是一组可能为空的函数、类型、变量和常量的声明。
包子句
每个源文件以包子句开始,定义该文件所属的包。
PackageName 不能是空标识符。
共享相同 PackageName 的一组文件形成包的实现。实现可能要求包的所有源文件都位于同一目录中。
导入声明
导入声明表明包含该声明的源文件依赖于_导入_包的功能(§程序初始化和执行),并启用对该包的导出标识符的访问。导入声明了一个用于访问的标识符(PackageName)和一个指定要导入包的 ImportPath。
PackageName 在限定标识符中用于在导入源文件中访问包的导出标识符。它在文件块中声明。如果省略 PackageName,则默认使用导入包的包子句中指定的标识符。如果显式句点(.)代替名称出现,则该包包块中声明的所有导出标识符将在导入源文件的文件块中声明,并且必须无修饰符访问。
ImportPath 的解释依赖于实现,但通常是编译包完整文件名的子字符串,并且可能相对于已安装包的存储库。
实现限制:编译器可能限制 ImportPath 为仅使用属于 Unicode's L, M, N, P, 和 S 通用类别(无空格的图形字符)的字符,并且可能排除字符 !"#$%&'()*,:;<=>?[\]^`{|} 和 Unicode 替换字符 U+FFFD。
考虑一个包含包子句 package math 的已编译包,它导出了函数 Sin,并在 "lib/math" 标识的文件中安装了编译包。下表说明了如何在不同类型的导入声明后,在导入包的文件中访问 Sin。
导入声明声明了导入包和导入包之间的依赖关系。包直接或间接导入自身是非法的,或者直接导入一个包而不引用其任何导出标识符也是非法的。为了仅为了其副作用(初始化)导入包,请使用空标识符作为显式包名:
示例包
以下是一个实现并发质数筛选的完整 Go 包。
程序初始化和执行
零值
当为变量分配存储空间时,无论是通过声明还是调用 new,或者创建新值,无论是通过复合字面量还是调用 make,并且没有提供显式初始化,则变量或值将给定默认值。这种变量或值的每个元素都设置为对应类型的_零值_:布尔类型为 false,数值类型为 0,字符串为 "",指针、函数、接口、切片、通道和映射为 nil。这种初始化是递归进行的,因此,例如,如果没有指定值,则结构体数组中的每个元素的字段都将被清零。
以下两个简单声明是等价的:
在
之后,以下成立:
以下也成立
包初始化
在包内,包级变量的初始化逐步进行,每个步骤选择_声明顺序_中最早且对未初始化变量没有依赖关系的变量。
更准确地说,如果包级变量尚未初始化,并且没有初始化表达式或其初始化表达式对未初始化变量没有_依赖_,则认为该变量_已准备好进行初始化_。初始化通过重复初始化声明顺序中最早且准备好初始化的下一个包级变量来进行,直到没有变量准备好初始化。
如果此过程结束时仍有变量未初始化,则这些变量属于一个或多个初始化循环,并且程序无效。
由右侧单个(多值)表达式初始化的变量声明左侧的多个变量将一起初始化:如果左侧的任何变量已初始化,则所有这些变量将在同一步骤中初始化。
出于包初始化的目的,空变量在声明中被视为任何其他变量。
多文件声明的变量声明顺序由文件呈现给编译器的顺序决定:第一个文件中声明的变量在第二个文件中声明的任何变量之前声明,依此类推。为了确保可重复的初始化行为,构建系统应鼓励按字典文件名顺序向编译器呈现属于同一包的多个文件。
依赖分析不依赖于变量的实际值,仅依赖于源中对它们的词法_引用_,进行传递性分析。例如,如果变量 x 的初始化表达式引用了一个其主体引用变量 y 的函数,则 x 依赖于 y。具体来说:
- 对变量或函数的引用是表示该变量或函数的标识符。
- 对方法
m的引用是形式为t.m的方法值或方法表达式,其中t的(静态)类型不是接口类型,并且方法m在t的方法集中。无论结果函数值t.m是否被调用都无关紧要。 - 如果变量、函数或方法
x的初始化表达式或主体(对于函数和方法)包含对y或依赖于y的函数或方法的引用,则x依赖于变量y。
例如,给定以下声明
初始化顺序是 d, b, c, a。注意初始化表达式中子表达式的顺序是无关紧要的:a = c + b 和 a = b + c 在这个例子中导致相同的初始化顺序。
依赖分析按包进行;仅考虑引用当前包中声明的变量、函数和(非接口)方法的引用。如果变量之间存在其他隐藏的数据依赖关系,则这些变量之间的初始化顺序是未指定的。
例如,给定以下声明
变量 a 将在 b 之后初始化,但 x 是在 b 之前、b 和 a 之间还是 a 之后初始化,以及 sideEffect() 被调用的时刻(在 x 初始化之前或之后)是未指定的。
变量也可以使用在包块中声明的名为 init 的函数进行初始化,这些函数没有参数且没有结果参数。
每个包可以定义多个这样的函数,甚至可以在单个源文件中定义多个。在包块中,init 标识符只能用于声明 init 函数,而标识符本身不会被声明。因此 init 函数在程序的任何地方都不能被引用。
整个包的初始化通过为其所有包级变量分配初始值,然后按照它们在源中出现的顺序调用所有 init 函数来完成,可能涉及多个文件,如呈现给编译器的顺序。
程序初始化
完整程序的包逐步初始化,一次一个包。如果一个包有导入,则在初始化该包本身之前先初始化导入的包。如果多个包导入同一个包,则导入的包只初始化一次。通过构造,包的导入保证了不存在循环初始化依赖关系。更精确地说:
给定所有包按导入路径排序的列表,在每个步骤中,列表中第一个未初始化的包,其所有导入的包(如果有)已经初始化,则被初始化。重复此步骤,直到所有包都初始化。
包初始化——变量初始化和 init 函数的调用——在单个 goroutine 中顺序发生,一次一个包。init 函数可以启动其他 goroutines,这些 goroutines 可以与初始化代码并发运行。然而,初始化总是按顺序执行 init 函数:在调用下一个 init 函数之前,前一个 init 函数必须返回。
程序执行
完整程序通过将一个单一的、未导入的包(称为 main 包)与它导入的所有包(传递性地)链接来创建。main 包必须有包名 main 并声明一个不带参数且不返回值的 main 函数。
程序执行从初始化程序开始,然后调用 main 包中的 main 函数。当该函数调用返回时,程序退出。它不会等待其他(非 main)goroutines 完成。
错误
预声明类型 error 定义为
它是表示错误条件的常规接口,nil 值表示没有错误。例如,从文件读取数据的函数可能定义为:
运行时恐慌
执行错误,如尝试索引超出数组边界,会触发一个_运行时恐慌_,相当于调用内置函数 panic,其参数为实现定义的接口类型 runtime.Error。该类型满足预声明接口类型 error。表示不同运行时错误条件的精确错误值是未指定的。
系统考虑
unsafe 包
内置包 unsafe 对编译器可见,并通过导入路径 "unsafe" 访问,提供低级编程设施,包括违反类型系统的操作。使用 unsafe 的包必须手动检查类型安全性,并且可能不可移植。该包提供以下接口:
Pointer 是指针类型,但 Pointer 值不能被解引用。任何指针或底层类型为 uintptr 的值都可以转换为底层类型为 Pointer 的类型,反之亦然。如果相应类型是类型参数,则它们各自类型集中的所有类型必须具有相同的基础类型,分别为 uintptr 和 Pointer。Pointer 和 uintptr 之间转换的效果是依赖于实现的。
函数 Alignof 和 Sizeof 接受任何类型的表达式 x,并分别返回假设变量 v 的对其或大小,就像 v 通过 var v = x 声明一样。
函数 Offsetof 接受一个(可能加括号的)选择器 s.f,表示结构体 s 或 *s 的字段 f,并返回字段相对于结构体地址的字节偏移量。如果 f 是嵌入字段,则它必须可以通过结构体的字段无指针间接访问。对于具有字段 f 的结构体 s:
计算机架构可能要求内存地址_对齐_;即变量的地址是某个因子的倍数,该变量的类型的_对齐_。函数 Alignof 接受表示任何类型变量的表达式,并返回(变量类型的)对齐字节数。对于变量 x:
(变量)类型 T 具有_可变大小_,如果 T 是类型参数,或者它是包含可变大小元素或字段的数组或结构体类型。否则大小是_常量_。如果其参数(或选择器表达式 s.f 中的结构体 s 对于 Offsetof)是常量大小类型,则对 Alignof、Offsetof 和 Sizeof 的调用是类型 uintptr 的编译时常量表达式。
函数 Add 将 len 加到 ptr 上并返回更新后的指针 unsafe.Pointer(uintptr(ptr) + uintptr(len)) [Go 1.17]。len 参数必须是整数类型或未类型化的常量。常量 len 参数必须可由类型 int 的值表示;如果是未类型化的常量,则其类型为 int。Pointer 的有效使用规则仍然适用。
函数 Slice 返回一个切片,其基础数组从 ptr 开始,其长度和容量为 len。Slice(ptr, len) 等价于
除了一个特殊情况,如果 ptr 是 nil 且 len 为零,Slice 返回 nil [Go 1.17]。
len 参数必须是整数类型或未类型化的常量。常量 len 参数必须是非负数,并且可由类型 int 的值表示;如果是未类型化的常量,则其类型为 int。在运行时,如果 len 为负数,或者 ptr 是 nil 且 len 不为零,则会发生运行时恐慌 [Go 1.17]。
函数 SliceData 返回 slice 参数的基础数组的指针。如果切片的容量 cap(slice) 不为零,则该指针为 &slice[:1][0]。如果 slice 为 nil,则结果为 nil。否则它是对未指定内存地址的非 nil 指针 [Go 1.20]。
函数 String 返回一个 string 值,其基础字节从 ptr 开始,其长度为 len。ptr 和 len 参数的要求与 Slice 函数相同。如果 len 为零,则结果为空字符串 ""。由于 Go 字符串是不可变的,传递给 String 的字节之后不能被修改 [Go 1.20]。
函数 StringData 返回 str 参数的基础字节的指针。对于空字符串,返回值是未指定的,可能为 nil。由于 Go 字符串是不可变的,StringData 返回的字节不能被修改 [Go 1.20]。
大小和对齐保证
对于数值类型,以下大小是保证的:
以下最小对齐属性是保证的:
- 对于任何类型的变量
x:unsafe.Alignof(x)至少为 1。 - 对于结构体类型的变量
x:unsafe.Alignof(x)是x的每个字段f的unsafe.Alignof(x.f)值的最大值,但至少为 1。 - 对于数组类型的变量
x:unsafe.Alignof(x)与数组元素类型变量的对齐方式相同。
如果结构体或数组类型不包含任何大小大于零的字段(或元素),则其大小为零。两个不同的零大小变量可能在内存中具有相同的地址。
附录
语言版本
Go 1 兼容性保证确保根据 Go 1 规范编写的程序在该规范的整个生命周期内将继续正确编译和运行,且无需更改。更一般地,随着语言调整和添加功能,兼容性保证确保适用于特定 Go 语言版本的 Go 程序将继续适用于任何后续版本。
例如,使用 0b 前缀表示二进制整数字面值的能力是在 Go 1.13 中引入的,如整数字面值部分中的 [Go 1.13] 所示。如果编译器使用的隐含或必需的语言版本早于 Go 1.13,则包含 0b1011 等整数字面值的源代码将被拒绝。
下表描述了 Go 1 之后引入的功能所需的最低语言版本。
Go 1.9
- 可以使用别名声明为类型声明别名名称。
Go 1.13
- 整数字面值可以使用前缀
0b、0B、0o和0O分别表示二进制和八进制字面值。 - 十六进制浮点字面值可以使用前缀
0x和0X编写。 - 虚部后缀
i可以用于任何(二进制、十进制、十六进制)整数或浮点字面值,而不仅仅是十进制字面值。 - 任何数字字面值的数字可以使用下划线
_分隔(分组)。 - 移位操作中的移位计数可以是带符号的整数类型。
Go 1.14
- 通过不同的嵌入接口多次嵌入方法不是错误。
Go 1.17
Go 1.18
1.18 版本为语言添加了多态函数和类型(“泛型”)。具体来说:
- 运算符和标点符号集合包括新符号
~。 - 函数和类型声明可以声明类型参数。
- 接口类型可以嵌入任意类型(不仅仅是接口的类型名)以及联合和
~T类型元素。 - 预声明类型集合包括新类型
any和comparable。
Go 1.20
- 如果切片和数组元素类型匹配,并且数组长度不超过切片长度,则切片可以转换为数组。
- 内置包
unsafe包括新的函数SliceData、String和StringData。 - 可比较类型(如普通接口)可以满足
comparable约束,即使类型参数不是严格可比较的。
Go 1.21
Go 1.22
- 在"for" 语句中,每次迭代都有自己的迭代变量集,而不是在每次迭代中共享相同的变量。
- 带有"range" 子句的 "for" 语句可以迭代从零到上限的整数值。
Go 1.23
- 带有"range" 子句的 "for" 语句接受一个迭代器函数作为范围表达式。
Go 1.24
类型统一规则
类型统一规则描述了两种类型是否以及如何统一。具体细节与 Go 实现相关,影响错误消息的具体内容(例如,编译器报告类型推断错误还是其他错误),并可能解释为什么在异常代码情况下类型推断失败。但在编写 Go 代码时,这些规则大多可以忽略:类型推断设计为基本“按预期工作”,统一规则也相应进行了微调。
类型统一由_匹配模式_控制,可能是_精确_或_宽松_。当统一递归下降到复合类型结构时,用于类型元素的匹配模式(元素匹配模式)与匹配模式相同,除非两种类型因可赋值性(≡<sub>A</sub>)而统一:在这种情况下,匹配模式在顶层是_宽松_的,但对于元素类型则变为_精确_,反映出类型不必完全相同即可赋值的事实。
如果满足以下任一条件,则两种非绑定类型参数的类型完全统一:
- 两种类型相同。
- 两种类型具有相同的结构,且其元素类型完全统一。
- 恰好一种类型是未绑定的类型参数,且其类型集中的所有类型根据
≡<sub>A</sub>的统一规则(顶层宽松统一,元素类型精确统一)与另一种类型统一。
如果两种类型都是绑定类型参数,则它们根据给定的匹配模式统一,如果:
- 两种类型参数相同。
- 最多一种类型参数具有已知类型参数。在这种情况下,类型参数被_合并_:它们都代表相同的类型参数。如果两种类型参数都还没有已知类型参数,则为一个类型参数推断的未来类型参数同时推断为两种类型参数。
- 两种类型参数都具有已知类型参数,且类型参数根据给定的匹配模式统一。
单个绑定类型参数 P 和另一种类型 T 根据给定的匹配模式统一,如果:
P没有已知类型参数。在这种情况下,T被推断为P的类型参数。P确实有已知类型参数A,A和T根据给定的匹配模式统一,并且满足以下条件之一:
最后,如果满足以下条件,则两种非绑定类型参数的类型松散统一(并根据元素匹配模式):
- 两种类型完全统一。
- 一种类型是定义类型,另一种类型是类型字面值,但不是接口,且其基础类型根据元素匹配模式统一。
- 两种类型都是接口(但不是类型参数),具有相同的类型项,都嵌入或不嵌入预声明类型comparable,对应方法类型完全统一,且一种接口的方法集是另一种接口方法集的子集。
- 只有一种类型是接口(但不是类型参数),两种类型的对应方法根据元素匹配模式统一,且接口的方法集是另一种类型方法集的子集。
- 两种类型具有相同的结构,且其元素类型根据元素匹配模式统一。