Go编程语言规范[译]
本文由cwittlut独立翻译自 The Go Programming Language Specification (https://go.dev/ref/spec),原文采用 Creative Commons Attribution 3.0 协议,文档内代码采用 BSD 协议 (https://go.dev/LICENSE)。 本文采用 Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International 许可协议,文档内代码继用相同协议,如果你需要发布本文(或衍生品),也需要注明本文原始链接 (https://bitbili.net/golang_spec.html) 及译者 cwittlut (原 bekcpear)。
- 对应英文原版 为 2022 年 03 月 10 日 版本: https://go.dev/ref/spec , :github:
golang/go@fe75fe3c7ae99713ed4e452ea8a4fcb589517dd9:doc/go_spec.html
- 本文完整翻译了官方英文版,且后续会尽全力同步更新
Important
本文适用于支持泛型的 Go 1.18 及以上版本;
对于之前的文档,请移步: Go 1.17 编程语言规范 【译】
本文唯一原始链接为 https://bitbili.net/golang_spec.html
源码存放在我的 Github 上: :github:
bekcpear/mypelicanconfandarticles@master:/content/Tech/gospec.rst
翻译中针对可能有歧义/不明确/翻译后不易于理解的单词将直接使用原词汇
为了行文工整,代码块内可能使用英文表述
因为学习语言需要,所以翻译
有些翻译可能比较迷糊,我会在进一步学习后完善它们
文中实参仅代表 argument;参数仅代表 parameter,有时候也会译为形参
目前翻译还是可能存在错误,如发现请及时联系我或在下方留言
介绍
这是一篇 Go 编程语言的参考手册。访问 go.dev 以获取更多信息及其它文档。
Go 是一个在设计时便考虑到系统编程的通用语言。它是强类型的、带垃圾回收的且明确支持了并发编程。程序是由包来构建的,包的特性允许对其依赖进行高效管理。
语言语法紧凑且易于解释,便于如集成开发环境(IDE)这样子的自动化工具分析。
标记法
标记法语法指定使用扩展巴科斯-瑙尔范式(EBNF):
|
|
:ruby:产生式|Productions
是由 :ruby:术语|terms
和如下操作符所构建的表达式(操作符排列按优先级递增的顺序):
|
|
小写字母的产生式名是用来标记一个词汇记号(组)的。 :ruby:非终结符|Non-terminals
是以驼峰命名法命名的。词汇记号( :ruby:终结符|terminals
)都是使用双引号 "" 或者反引号 `` 包裹起来的。
a … b
这样子的格式表示从 a
连续到 b
的字符集。水平省略号 …
也会用在其它一些地方非正式地表示枚举或者不再进一步说明的代码片段。 字符 …
(与三个单独字符 ...
不同)并不是 Go 语言里的 token。
Note
译注: :ruby:扩展巴科斯-瑙尔范式|extended Backus-Naur form
是一种 :ruby:元语法|metasyntax
符号标记法,可以用于表示 :ruby:上下文无关文法|Context-free grammar
。
针对本文简单说明,其产生式规则由非终结符和终结符所构成,左侧是一个非终结符,右侧则是该非终结符所代表的终结符和非终结符。终结符包括字母数字字符、标点符号和空格字符,其不可再分;非终结符最终指代某种序列组合的多个终结符。
本文用到的上述未说明的范式符号说明: =
定义; ,
级联; .
表示表达式终结; " .. "
表示除双引号外的终结符; \
.. `表示除反引号外的终结符;
? .. ?` 表示特殊序列,用于解释 EBNF 标准以外的文本。
又注:根据维基百科 extended Backus-Naur form 上说明来看,原文的 EBNF 格式并不规范,所以我对原文表达式进行最小程度修改。更详细的 EBNF 说明可以下载 ISO/IEC 14977:1996 PDF 压缩档 查看。
段落名若为中文且在语法标记块中使用英文书写的,均会在段落名上一并附上英文。
源代码表示
源代码是以 UTF-8 编码的 Unicode 文本。该文本并不是规范化的,所以一个单一的带重音符(附加符)的码位和由重音符(附加符)和字母所组成的相同字符不同,该相同字符结构被看成两个码位。为了简便,本文档使用非正规的术语——字符——指代源文本中的 Unicode 码位。
Note
译注: 这里的 规范化 的含义是指,文字处理软件为了对 Unicode 字符串做比较、搜寻和排序操作而不得不考虑其等价性才做的正规化处理,参考维基百科 Unicode 等價性 。
每一个码位都是不同的,比如大写和小写的字母就是不同的字符。
实现限制:为了保证与其它工具的兼容性,编译器可能会不允许源文本中存在 NUL 字符(U+0000)。
实现限制:为了保证与其它工具的兼容性,如果一个 UTF-8 编码的字节顺序标记(U+FEFF)为源文本的第一个 Unicode 码位,编译器可能会忽略它。字节顺序标记也可能会被不允许出现在源中的任何其它位置。
字符
如下术语用于表示特定的 Unicode 字符类:
|
|
在 The Unicode Standard 8.0 中, 4.5 节 “General Category” 定义了一套字符类别。 Go 语言把类别 Lu, Ll, Lt, Lm 或 Lo 中的字符看作 Unicode 字母,把数字类别 Nd 中的字符看作 Unicode 数字。
Note
译注: Lu 为大写字母, Ll 为小写字母, Lt 为标题字母, Lm 为修饰字母, Lo 为其它字母, Nd 为十进制数字,可以在 Compart 上查到对应分类包含哪些字符。
但是在这里我有一个疑惑,里面明明很多字母和数字是不能用在标识符中的,为什么这里统统包含了进来,并且下文也没有额外的说明?
暂时不去深究,就先以通常认知来对待
字母和数字
下划线字符 _ (U+005F) 被认为是一个字母。
|
|
词法元素
注释
注释作为程序的文档,有两种格式:
- 行内注释从字符序列
//
开始并在一行末尾结束。 - 通用注释从字符序列
/*
开始并在遇到的第一个字符序列*/
时结束。
注释不能开始于 rune 或 字符串 字面值或另一个注释的内部。不包含新行的通用注释就像一个空格。任何其它的注释就像一空白行。
Tokens
Tokens 组成了 Go 语言的词汇表。有四个分类: 标识符 、 关键字 、 运算符和标点 以及 字面值 。 空白 是由空格(U+0020)、水平制表(U+0009)、回车(U+000D)和新行(U+000A)所组成的,空白一般会被忽略,除非它分隔了组合在一起会形成单一 token 的 tokens. 并且,新行或者文件结尾可能会触发 分号 的插入。当把输入的内容区分为 tokens 时,每一个 token 都是可组成有效 token 的最长字符序列。
分号
正式的语法使用分号 ;
作为一定数量的产生式的终结符。 Go 程序可以依据如下两条规则来省略大部分这样子的分号:
- 输入内容被分为 tokens 时,当每一行最后一个 token 为以下 token 时,一个分号会自动插入到其后面:
- 为了使复杂的语句可以占据在单一一行上,分号也可以在关闭的
)
或者}
前被省略。
为了反应出惯用的使用习惯,本文档中的代码示例将参照这些规则来省略掉分号。
:ruby:标识符|Identifiers
标识符用于命名程序中的实体——比如变量和类型。它是一个或者多个字母和数字的序列组合。标识符的第一个字符必须是一个字母。
|
|
有一些标识符已经被 预先声明 了。
关键字
如下关键字是保留的,不可以用作标识符。
|
|
运算符和标点
如下的字符序列用于代表 运算符 (包括了 赋值运算符 )和标点:
|
|
:ruby:整数字面值|Integer literals
整数字面值是用来代表整数 常量 的数字序列。可用一个可选前缀来设置非十进制数: 0b
或 0B
代表二进制, 0
, 0o
, 0O
代表八进制, 0x
或 0X
代表十六进制。单独的 0
被视作十进制零。在十六进制数字面值中,字母 a 到 f 以及 A 到 F 代表数字值 10 到 15 。
为了可读性,下划线字符 _
可以出现在基本前缀之后或者连续的数字之间;这样的下划线不改变字面值的值。
|
|
:ruby:浮点数字面值|Floating-point literals
浮点数字面值是浮点数 常量 的十进制或十六进制表示。
十进制的浮点数字面值由一个整数部分(十进制数字),一个小数点,一个小数部分(十进制数字)和一个指数部分( e
或 E
后紧跟着带或者不带符号且为十进制的数字)。整数部分和小数部分其中之一可以省略;小数点和指数部分其中之一可以省略。指数值 exp 以 10^exp
来缩放 :ruby:有效数字|mantissa
(整数和小数部分)。
Note
译注: “An exponent value exp scales the mantissa (integer and fractional part) by 10^exp
.” 这里的 “mantissa” 存在争议,目前 IEEE 使用的是 “significand” 一词,维基百科 Talk:Significand 整理了相关讨论。
十六进制浮点数字面值由一个 0x 或 0X 前缀,一个整数部分(十六进制数字),一个小数点,一个小数部分(十六进制数字)和一个指数部分( p
或 P
后紧跟着带或者不带符号且为十六进制的数字)。整数部分和小数部分其中之一可以省略;小数点也可以省略,但是指数部分是必须的。(这个语法匹配 IEEE 754-2008 §5.12.3 章所说的。)指数值 exp 以 2^exp
来缩放有效数字(整数和小数部分)。
为了可读性,下划线字符 _
可以出现在基本前缀之后或是连续的数字之间;这样的下划线不会改变字面值的值。
|
|
:ruby:虚数字面值|Imaginary literals
虚数字面值表示复数 常量 的虚部。它由 整数 或者 浮点数 字面值紧跟着一个小写的字母 i
组成。这个虚数字面值的值为对应整数或者浮点数字面值的值乘以虚数单位 i 。
|
|
考虑到向后兼容,完全由十进制数字(可能存在下划线)组成的虚数字面值的整数部分被作为十进制整数,即使其以 0 开头也不例外。
|
|
:ruby:Rune 字面值|Rune literals
Rune 字面值代表了一个 rune 常量 ,一个确定了 Unicode 码位的整数值。 Rune 字面值是由一个或者多个字符以单引号包裹来表示的,就像 'x'
或 '\\n'
。在引号内,除了新行和未被转义的单引号外的任何字符都可能出现。被单引的字符表示的是该字符的 Unicode 值,不过以反斜杠开头的多字符序列会以不同的格式来编码 Unicode 值。
这是在引号内代表单一字符的最简单的形式;因为 Go 源文件是使用 UTF-8 编码的 Unicode 字符,多个 UTF-8 编码的字节可以表示为一个单一整数值。比如: 'a'
用一个字节代表了字面值 a
, Unicode U+0061,值 0x61
;但 'ä'
用了两个字节( 0xc3 0xa4
)代表了字面值 a 分音符
, Unicode U+00E4,值 0xe4
。
几个反斜杠转义允许任意值被编码为 ASCII 文本。有四种方法将整数值表达为数值常量: \\x
紧跟着两个十六进制数; \\u
紧跟着四个十六进制数; \\U
紧跟着八个十六进制数;一个单独的反斜杠 \\
紧跟着三个八进制数。每一种情况下的字面值的值都是对应基础上该数所表示的值。
虽然这些表示的最终都是一个整数,但它们有不同的有效范围。八进制转义必须表示 0 到 255 之间的值。十六进制转义满足条件的要求会因为构造不同而不同。 \\u
和 \\U
代表了 Unicode 码位,所以在这里面有一些值是非法的,尤其是那些超过 0x10FFFF
的和代理了一半的(译注:查阅「 UTF-16 代理对」进行深入阅读)。
在反斜杠后,某些单字符的转义代表了特殊的值:
|
|
所有其它以反斜杠开头的序列在 rune 字面值中都是非法的。
|
|
:ruby:字符串字面值|String literals
字符串字面值代表了通过串联字符序列而获得的字符串 常量 。它有两种形式: :ruby:原始|raw
字符串字面值和 :ruby:解释型|interpreted
字符串字面值。
原始字符串字面值是在反引号之间的字符序列,就像 \
foo`。 除了反引号外的任何字符都可以出现在该引号内。 原始字符串字面值的值是由在引号内未被解释过的(隐式 UTF-8 编码的)字符所组成的字符串; 尤其是,反斜杠在这里没有特殊意义,且字符串可以包含新行(LF)。 原始字符串字面值中的回车字符(
’\r’` )会被从原始字符串值中所丢弃。
Note
译注: 经测试,手动输入的 '\\r'
字符是可以正常显示为 '\\r'
的(毕竟反斜杠在这里无意义),那么理解下来,丢弃的是键盘键入的回车(CR,比如 Windows 上)。
解释型字符串字面值是在双引号之间的字符序列,就像 "bar"
。除了新行和未被转义的双引号之外的所有字符都可以出现在该引号内。引号之间的文本组成了字符串字面值的值,反斜杠转义以及限制都和 rune 字面值一样(不同的是,在解释型字符串字面值中, \\'
是非法的, \\"
是合法的)。三个数字的八进制数( \\nnn
)和两个数字的十六进制数( \\xnn
)的转义代表着所生成字符串的独立字节;所有其它的转义代表了单独字符的 UTF-8 编码(可能是多字节的)。因此字符串字面值内的 \\377
和 \\xFF
代表着值为 0xFF=255
的单一字节,而 ÿ
, \\u00FF
, \\U000000FF
和 \\xc3\\xbf
代表着字符 U+00FF 以 UTF-8 编码的双字节 0xc3 0xbf
。
|
|
以下这些例子都代表着相同的字符串:
|
|
当源代码以两个码位来代表一个字符,比如包含一个重音符和一个字母的组合形式,如果是在 rune 字面值中的话会使得结果出错(因为其并不是一个单一码位),而如果是在字符串字面值中的话则会显示为两个码位。
常量
常量有 布尔值常量 、 rune 常量 、 整数常量 、 浮点数常量 、 复数常量 和 字符串常量 。 Rune、整数、浮点数和复数常量统称为数值常量。
一个常量的值是由如下所表示的: rune 、 整数 、 浮点数 、 虚数 或 字符串 字面值;表示常量的标识符; 常量表达式 ;结果为常量的 变量转换 ;或者一些内置函数所生成的值,这些内置函数比如应用于 某些值 的 unsafe.Sizeof
,应用于 一些表达式 的 cap
或 len
,应用于复数常量的 real
和 imag
以及应用于数值常量的 complex
。布尔值是由预先声明的常量 true
和 false
所代表的。预先声明的标识符 iota 表示一个整数常量。
通常,复数常量是 常量表达式 的一种形式,会在该节讨论。
数值常量代表任意精度的确切值,而且不会溢出。因此,没有常量表示 IEEE-754 负零,无穷,以及非数字值。
Note
译注:上面后半句应该是指的数值常量,可能没有表示清楚,因为字符串常量肯定就是非数字值。
常量可以是 类型化的 也可以是非类型化的。字面值常量, true
, false
, iota
以及一些仅包含非类型化的恒定操作数的 常量表达式 是非类型化的。
常量可以通过 常量声明 或 变量转换 被显示地赋予一个类型,也可以在 变量声明 或 赋值 中,或作为一个操作数在 表达式 中使用时隐式地被赋予一个类型。如果常量的值不能按照所对应的类型来表示的话,就会出错。 如果类型是一个类型形参,那么常量会被转化为该类型形参的一个非常量值。
Note
译注, 【2018 年 5 月版的内容】 比如, 3.0
可以作为任何整数类型或任何浮点数类型,而 2147483648.0
(相当于 1<<31
)可以作为 float32
, float64
或 uint32
类型,但不能是 int32
或 string
。
一个非类型化的常量有一个 默认类型 ,当在上下文中需要请求该常量为一个类型化的值时,这个 默认类型 便指向该常量隐式转换后的类型,比如像 i := 0
这样子的 短变量声明 就没有显示的类型。非类型化的常量的默认类型分别是 bool
, rune
, int
, float64
, complex128
或 string
,取决于它是否是一个布尔值、 rune、整数、浮点数、复数或字符串常量。
实现限制:虽然数值常量在这个语言中可以是任意精度的,但编译器可能会使用精度受限的内部表示法来实现它。也就是说,每一种实现必须:
- 使用最少 256 位来表示整数。
- 使用最少 256 位来表示浮点数常量(包括复数常量的对应部分)的小数部分,使用最少 16 位表示其带符号的二进制指数部分。
- 当无法表示一个整数常量的精度时,需要给出错误。
- 当因为溢出而无法表示一个浮点数或复数常量时,需要给出错误。
- 当因为精度限制而无法表示一个浮点数或复数常量时,约到最接近的可表示的常量。
这些要求也适用于字面值常量,以及 常量表达式 的求值结果。
变量
变量是用来放置 值 的存储位置。可允许的值的集是由变量 类型 所确定的。
变量声明 和对于函数参数及其结果而言的 函数声明 或 函数字面值 的签名都为命名的变量保留存储空间。调用内置函数 new
或获取 复合字面值 的地址会在运行时为变量分配存储空间。这样子的一个匿名变量是通过(可能隐式的) 指针间接 引用到的。
结构化的 数组 、 分片 和 结构体 类型变量存在可以独立 寻址 的元素和字段。每一个这样子的元素就像一个变量。
变量的 静态类型 (或者就叫 类型 )是其声明时确定好的类型,或由 new
调用或复合字面值所提供的类型,或结构化变量的元素类型。接口类型的变量还有一个独特的 动态 类型,该类型是在运行时所分配给变量的值的(非接口)类型(除非那个值是预声明的标识符 nil
,它是没有类型的)。动态类型可能会在执行过程中变化,但存储在接口变量中的值始终 可分配 为接口变量的静态类型。
|
|
变量的值是通过引用 表达式 中的变量来检索的;它总是那个最后 赋 给变量的值。如果一个变量还没有被分配到值,那么它的值是其对应类型的 零值 。
类型
类型确定了一个值集(连同特定于这些值的操作和方法)。 类型可以由 类型名 表示(如果它有的话),如果该类型是泛型,那么其后必须紧跟一个 类型实参 。 类型也可以由已知类型组成的 类型字面值 指定。
|
|
语言本身 预先声明 了一些特定的类型名。其它的命名类型则使用 类型声明 或者 类型形参列表 引入。 复合类型 ——数组、结构体、指针、函数、接口、分片、映射和信道类型——可以由类型字面值构成。
预先声明的类型、定义好的类型以及类型形参都被称为 命名类型 。如果在别名声明中给出的类型是命名类型,那么该别名表示命名类型。
布尔类型
布尔类型 代表以预先声明的常量 true
和 false
所表示的布尔真值的集合。预先声明的布尔类型为 bool
,这是一个 定义类型 。
数字类型
整数 、 浮点数 或 复数 类型分别代表整数、浮点数或复数值的集合。 它们被统称为 数字类型 。 预先声明的架构无关的数字类型有:
|
|
一个 n 位整数的值是 n 位宽的,是使用 补码 来表示的。
Note
译注:也就是 uint8 就是 8 位宽, int8 也是 8 位宽, int16 就是 16 位宽,以此类推;关于原码、反码和补码,这里有一篇比较: 知乎-原码、反码、补码的产生、应用以及优缺点有哪些?
以下是根据实现不同而有特定大小的预先声明的整数类型:
|
|
为了避免移植性问题,除了 byte
( unit8
的别名)和 rune
( int32
的别名)外的所有数字类型都是截然不同的 定义类型 。当不同的数字类型混合在一个表达式或赋值里时,是需要显示的转换的。比如, int32
和 int
并不是相同的类型,就算在一个特定的架构上它们可能有相同的大小,也是如此。
字符串类型
字符串类型 代表了字符串值的集合。一个字符串值是字节的序列(可能为空)。字节的个数被称为该字符串的长度,并且不能为负。字符串是不可变的:一旦创建好了是不可能去修改其内容的。预先声明的字符串类型是 string
;它是一个 定义类型 。
字符串 s
的长度可以使用内置函数 len 来发现。如果字符串是一个常量,那么长度是一个编译时常量。一个字符串的字节可以通过从 0
索引 到 len(s) - 1
的整数来访问。获取这样子的一个元素的地址是非法的;如果 s[i]
是一个字符串的第 i
个字节,那么 &s[i]
是无效的。
:ruby:数组类型|Array types
数组是单一类型元素的有序序列,该单一类型称为元素类型。元素的个数被称为数组长度,并且不能为负值。
|
|
长度是数组类型的一部分;它必须为一个可以被 int
类型的值所代表的非负 常量 。数组的长度 a
可以使用内置函数 len 来发现。元素可以被从 0
索引 到 len(a) - 1
的整数所寻址到。数组类型总是一维的,但可以被组合以形成多维类型。
|
|
:ruby:分片类型|Slice types
分片是针对一个底层数组的连续段的描述符,它提供了对该数组内有序序列元素的访问。分片类型表示其元素类型的数组的所有分片的集合。元素的数量被称为分片长度,且不能为负。未初始化的分片的值为 nil
。
Note
译注, 在这里 Go Specification 的描述为:
The value of an uninitialized slice is nil.
而 :ruby:《Go 语言圣经》|The GO Programming Language
里说:
The zero-value mechanism ensures that a variable always holds a well-defined value of its type; in Go there is no such thing as an uninitialized variable."
于是我对如下两段代码:
|
|
使用如下命令:
|
|
分别生成对应的汇编文件后对比,发现两个文件内容除文件名外其它一致,均对变量 a 所对应的栈地址写零了;因此这里的描述实际上修正为, “未被显式初始化过的分片会被隐式地初始化为其零值 nil” 更恰当,下同。
|
|
分片 s
的长度可以被内置函数 len 来发现;和数组不同的是,这个长度可能会在执行过程中改变。元素可以被从 0
索引 到 len(s) - 1
的整数所寻址到。一个给定元素的分片索引可能比其底层数组的相同元素的索引要小。
分片一旦初始化便始终关联到存放其元素的底层数组。因此分片会与其数组和相同数组的其它分片共享存储区;相比之下,不同的数组总是代表不同的存储区域。
分片底层的数组可以延伸超过分片的末端。 容量 便是对这个范围的测量:它是分片长度和数组内除了该分片以外的长度的和;不大于其容量长度的分片可以从原始分片 再分片 新的来创建。分片 a
的容量可以使用内置函数 cap(a) 来找到。
对于给定元素类型 T
的新的初始化好的分片值可以使用的内置函数 make 来制作, 这个内置函数需要获取分片类型、指定的长度和可选的容量作为参数。使用 make
创建的分片总是分配一个新的隐藏的数组给返回的分片值去引用。也就是,执行
|
|
就像分配个数组然后 再分片 它一样来产生相同的分片,所以如下两个表达式是相等的:
|
|
如同数组一样,分片总是一维的但可以通过组合来构造高维的对象。数组间组合时,被构造的内部数组总是拥有相同的长度;但分片与分片(或数组与分片)组合时,内部的长度可能是动态变化的。此外,内部分片必须单独初始化。
:ruby:结构体类型|Struct types
结构体是命名元素的一个序列,这些元素被称为字段,每一个都有一个名字和一个类型。字段名可以被显式指定(IdentifierList)也可以被隐式指定(EmbeddedField)。在结构体中,非 空白 字段名必须是 唯一的 。
|
|
一个声明了类型但没有显式的字段名的字段就是 嵌入字段 。嵌入字段必须指定为一个类型名 T
或者为一个到非接口类型的指针名 *T
, 并且 T
不是一个指针类型。这个非限定的类型名就被当作字段名。
|
|
以下声明是非法的,因为在一个结构体类型中,字段名必须是唯一的:
|
|
在结构体 x
中,一个嵌入字段的字段或 方法 f
被称作 promoted ,前提是 x.f
是一个表示那个字段或方法 f
的合法 选择器 。
除了不能在结构体的 复合字面值 中作为字段名外, promoted 字段和结构体的普通字段一样。
给定一个结构体类型 S
和一个 定义类型 T
, promoted 方法包含在这个结构体的方法集中的情况分为:
- 如果
S
包含一个嵌入字段T
,那么S
和*S
的 方法集 都包括了接收器为T
的 promoted 方法。*S
的方法集还包括了接收器为*T
的 promoted 方法。 - 如果
S
包含了一个嵌入字段*T
,那么S
和*S
的 方法集 都包括了接收器为T
或*T
的 promoted 方法。
字段声明可以紧跟着一个可选的字符串字面值 标签 ,在对应的字段声明中,它将成为针对所有这个字段的属性。空的标签字符串等于没有标签。标签可以通过 反射接口 被可视化,并且可以参与到结构体的 类型一致性 中,但其它情况下都是被忽略的。
|
|
:ruby:指针类型|Pointer types
指针类型表示指向一给定类型的 变量 的所有指针的集合,这个给定类型称为该指针的 基础类型 。未初始化的指针的值为 nil
。
|
|
:ruby:函数类型|Function types
函数类型表示具有相同参数和结果类型的所有函数的集合。函数类型的未初始化的变量的值为 nil
。
|
|
在参数或结果的列表中,名字(IdentifierList)要么全部存在,要么全部不存在。如果存在,每个名字代表特定类型的一个条目(参数或者结果),签名中的名字是非 空白 的,且必须是 唯一的 。如果不存在,每个类型代表该类型的一个条目。参数和结果列表总是括起来的,除非只有一个未命名的结果(可以写为不使用括号括起来的类型)。
函数签名中最后的进入参数可以是以 ...
为前缀的类型。带这样一个参数的函数被称为 variadic (可变),它可以携带针对该形参的零或多个实参来调用。
|
|
:ruby:接口类型|Interface types
一个接口类型定义了一个 类型集 。 一个接口类型的变量可以储存该接口的类型集中任意类型的值。 这样的一个类型被称为 实现了这个接口 。 接口类型的未初始化的变量的值为 nil
。
|
|
接口类型是由一个 接口元素 列表所指定的。接口元素可以是一个 方法 也可以是一个 类型元素 ,其中类型元素是一个或多个 类型术语 的并集。类型术语可以是一个单一类型也可以是一个单一潜在类型。
基本接口
在最基本的格式下,接口指定了一个(可能为空的)方法列表。 由这样的接口所定义的类型集是实现了所有这些方法的类型的集合,其对应的 方法集 完全由这个接口所指定的方法组成。那些类型集可以完全由一列方法定义的接口被称为 基本接口 。
|
|
|
|
多个类型可以实现一个相同的接口。比如,如果两个类型 S1
和 S2
有方法集
|
|
(其中 T
代表 S1
或 S2
)那么 File
接口就被 S1
和 S2
实现了,不管 S1
和 S2
是否有其它的(或共享的)方法。
接口类型集中的每一个类型都实现了这个接口。一个给定的类型可能会实现完全不同的接口。例如,所有类型都实现 空接口 ,它代表所有类型的集合:
|
|
为了方便,用预先声明的类型 any
作为空接口的一个别名。
类似的,来看这个出现在 类型声明 中定义了一个叫做 Locker
接口的接口规格:
|
|
如果 S1
和 S2
也实现了
|
|
和 File
接口一样,它们也实现了 Locker
接口。
嵌入接口
以稍微常规点的格式来说,接口 T
可以使用(可能是限定的)接口类型名 E
作为其接口元素。 这被叫作在 T
中的 嵌入 接口 E
。 T
的类型集是 T
显示声明的方法的类型集与 T
嵌入接口的类型集的交集。换句话说, T
的类型集是实现了所有 T
与 E
显示声明的方法的类型的集合。
|
|
|
|
泛型接口
以最通用的格式来说,接口元素也可以是一个任意的类型术语 T
,或者一个指定潜在类型为 T
的格式为 ~T
的术语,或者术语 t1|t2|…|tn
的并集。这些元素与方法规格一起启用了一个接口的类型集的精确定义,如下:
- 空接口的类型集是所有非接口类型的集合
- 非空接口的类型集是其接口元素类型集的交集
- 方法规格的类型集是方法集中包含该方法的类型的集合
- 非接口类型术语的类型集就是组成该类型的集合
- 格式为
~T
的术语的类型集是潜在类型为T
的类型的集合 - 术语
t1|t2|…|tn
并集 的类型集是这些术语的类型集的并集
通过构建,接口的类型集永远不会包含接口类型。
|
|
在格式为 ~T
的术语中, T
的潜在类型必须是它自身且 T
不能是一个接口。
|
|
:ruby:并集元素|Union elements
表示其所有类型集的并集:
|
|
在并集中,术语不能是一个 类型形参 ,且所有非接口术语的类型集必须是成对不相交的(类型集成对的交集必须为空)。给定一个类型形参 P
:
|
|
实现限制:一个(术语数量大于 1 的)并集不能包含 预先声明的标识符 comparable
或指定了方法的接口,或者嵌入的 comparable
或指定了方法的接口。
非 基本接口 只可以作为类型约束来使用,或作为其它作为约束来使用的接口的元素。 这些接口不能作为值或变量的类型,也不能作为其它非接口类型的组件。
Note
译注,这里的基本接口包含了上述的基本接口以及仅嵌入了基本接口的嵌入接口。 来源见 the commit msg of 30501bb
|
|
一个接口类型 T
不能嵌入任何递归地包含或嵌入 T
的类型元素。
|
|
实现接口
如果
T
不是一个接口但是I
的类型集的元素;或者T
是一个接口且T
的类型集是I
的类型集的子集,
那么类型 T
就实现了接口 I
。
如果 T
实现了一个接口,那么类型 T
的值就实现了这个接口。
:ruby:映射类型|Map types
映射是由一种类型的元素所组成的无序组,这个类型被称为元素类型, 其元素被一组另一种类型的唯一 键 索引,这个类型被称为键类型。 一个未初始化的映射的值为 nil
。
|
|
键类型的操作数必须有完全定义的 比较运算符 ==
和 !=
;因此键类型不能为一个函数、映射或分片。如果键类型是一个接口类型,那么比较运算符必须针对其动态键值做完全定义;失败会导致一个 run-time panic 。
|
|
映射元素的数目被称为其长度。对于一个映射 m
,长度可以使用内置函数 len 来找到并且可能会在执行过程中改变。元素可以在执行过程中使用 赋值 来进行添加,可以使用 索引表达式 来获取;可以使用内置函数 delete 来移除。
一个新的、空的映射值的创建使用的是内置函数 make ,其获取映射类型和一个可选的容量提示作为实参:
|
|
初始化的容量不会限制其大小:映射会增长以适合其存储项目的数量,除了 nil
映射。 nil
映射相当于空映射,但是 nil
映射不能添加元素。
:ruby:信道类型|Channel types
信道针对 并行执行函数 提供了一个 发送 和 接收 指定类型的值的机制。未初始化的信道的值为 nil
。
|
|
可选的 <-
运算符指定了信道的 方向 、 发送 或 接收 。如果方向被指定了,那么这个信道是 定向的 ,否则就是 双向的 。通过 赋值 或显示的 转换 ,信道可以被限制为仅能发送或仅能接收。
|
|
<-
与最左的 chan
关联的一些可能性:
|
|
一个新的,初始化的信道值的创建可以使用内置的函数 make ,它获取信道类型和可选的 容量 作为实参:
|
|
容量(元素的数量)确定了信道中缓冲区的大小。如果容量为零或没有写,那么信道就是无缓冲的,这种情况下,只有在接收端和发送端都准备好的情况下,通信才会成功。不然信道就是有缓冲的,这种情况下只要不阻塞,通信便会成功;阻塞是指缓冲区满了(对于发送端而言)或者缓冲区空了(对于接收端而言)。 一个 nil
的信道是不能用于通信的。
信道可以使用内置函数 close 来关闭。 接收运算符 的多值分配形式报告了在信道关闭前接收到的值是否已经被发送了。
单个信道可以被不需要进一步同步的任意数量的 goroutines 用在 发送语句 , 接收运算符 和对内置函数 cap 及 len 的调用上。信道是一个先进先出的队列。举例,如果一个 goroutine 在信道上发送了值,第二个 goroutine 接收了这些值,那么这些值是按照发送的顺序被接收的。
类型和值的属性
潜在类型
每个类型 T
都有一个 潜在类型 : 如果 T
是预先声明的布尔值、数值或者字符串类型之一,或一个类型字面值,那对应的潜在类型就是 T
自己。否则,其潜在类型就是 T
声明时指定的那个类型的潜在类型。 对于类型形参而言,则是其 类型约束 的潜在类型,其总是一个接口。
|
|
string
, A1
, A2
, B1
和 B2
的潜在类型是 string
。 []B1
, B3
和 B4
的潜在类型是 []B1
。 P
的潜在类型为 interface{}
。
核心类型
每个非接口类型 T
都有一个 核心类型 ,它和 T
的 潜在类型 是相同的。
如果以下条件之一满足,那么接口 T
就就有核心类型:
所有其它接口都没有核心类型。
根据满足条件,接口的核心类型要么是:
- 类型
U
;要么是 - 类型
chan E
(如果T
仅包含双向信道), 或者类型chan<- E
或<-chan E
(取决于现存定向信道的方向)
根据定义,核心类型永远不会是 定义类型 、 类型形参 或 接口类型 。
带核心类型的接口的例子:
|
|
不带核心类型的接口的例子:
|
|
类型一致性
两个类型,要么是 一致的 要么是 不同的 。
命名类型 和其它类型总是不同的。除此以外,如果两个类型所对应的 潜在类型 字面值是结构一致的——也就是说它们拥有相同的字面值结构并且对应的组成部分拥有一致的类型——那么它们便是一致的。详细来说:
- 如果两个数组类型有一致的元素类型和相同的数组长度,那么它们便是一致的。
- 如果两个分片类型有一致的元素类型,那么它们便是一致的。
- 如果两个结构体有相同的字段序列,并且对应的字段有相同的名字、一致的类型和一致的标签,那么它们便是一致的。(不同包的 非暴露的 字段名总是不同的)
- 如果两个指针类型有一致的基础类型,那么它们便是一致的。
- 如果两个函数类型有相同的参数数量和结果值,并且对应的参数和结果类型是一致的,并且两者要么都是 variadic 要么都不是,那么它们便是一致的。(参数和结果名不是必须匹配的)
- 如果两个接口类型定义了相同的类型集,那么它们便是一致的。
- 如果两个映射类型有一致的键类型和值类型,那么它们便是一致的。
- 如果两个信道类型有一致的值类型和相同的方向,那么它们便是一致的。
- 如果两个 实例化 过的类型的定义类型和所有的类型实参都是一致的,那么它们便是一致的。
给出声明
|
|
这些类型是一致的
|
|
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
是一个双向信道,且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 round-to-even 规则,但是 IEEE 负零会被进一步简化到一个无符号的零。 (注:这种常量值不会出现 IEEE 负零、 NaN 或者无穷。)T
是一个复数类型并且x
的 组成real(x)
和imag(x)
是可以被T
的组成类型(float32
或者float64
)所表示的。
如果 T
是一个类型形参,那么如果 x
可以被 T
类型集中的每一个类型的值所表示,那么 x
就可以被 T
的值所表示。
|
|
方法集
类型的 方法集 确定了该类型的 操作数 所可以 调用 的方法。每一个类型都有一个(可能为空的)方法集与之关联:
- 定义类型
T
的方法集是由接收器类型T
所声明的所有 方法 组成的。 - 指向定义类型
T
的指针(T
既不能是指针也不能是接口) 的方法集是由接收器类型*T
或T
所声明的所有方法组成的。 - 接口类型 的方法集是该接口的 类型集 中每个类型的方法集的交集(最终的方法集往往是该接口中所声明的方法的集合)。
应用于包含嵌入字段的结构体(及其指针)的更多规则,会在 结构体类型 一节描述。任何其它类型都有一个空的方法集。
在方法集中,每个方法都必须有一个 唯一的 非 空白 的 方法名 。
:ruby:块|Blocks
块 是在一对花括号内的声明和语句序列,这个序列可能是空的。
|
|
源代码中除了显式的块外,还有隐式的块:
- 包围所有 Go 原始文本的 宇宙块 。
- 每个 包 有一个包含针对该包的所有 Go 原始文本的 包块 。
- 每个文件有一个包含在该文件中所有 Go 原始文本的 文件块 。
- 每个 “if” , “for” 和 “switch” 语句都被认为是在其自己的隐式块中。
- 每个在 “switch” 或 “select” 语句中的子句都作为一个隐式的块。
块是嵌套的并影响着 作用域 。
声明和作用域
声明 绑定了非 空白 的标识符到 常量 、 类型 、 类型形参 、 变量 、 函数 、 标签 或 包 。 程序中的每个标识符都必须要声明。同一个块中不能定义一个标识符两次,并且没有标识符可以同时在文件块和包块中定义。
空白标识符 可以像其它标识符一样在声明中使用,但它不会引出一个绑定,因此不被声明。在包块中,标识符 init
只能用于 init 函数 声明,且和空白标识符一样,它不会引出一个新的绑定。
|
|
声明的标识符的 作用域 是该标识符表示特定常量、类型、变量、函数、标记或包时所处的原始文本的范围。
Go 使用 块 来定作用域:
- 预先声明的标识符 的作用域为宇宙块。
- 表示一个常量、类型、变量或函数(但不是方法)的在最上层(在任何函数外)定义的标识符的作用域为包块。
- 导入的包的包名的作用域为包含导入声明在内的文件的文件块。
- 表示一个方法接收器、函数参数或结果变量的标识符的作用域为函数主体。
- 表示一个函数的或被方法接收器所声明的 类型形参的标识符的作用域是这个函数体及函数的所有形参列表。
- 表示一个类型的类型形参的标识符的作用域起于这个类型的名字,终于类型规格(TypeSpec)。
- 在函数内定义的常量或变量标识符的作用域起始于 ConstSpec 或 VarSpec(对短变量来说为 ShortVarDecl)的尾端,结束于包含着它的最内的块。
- 在函数内定义的类型标识符的作用域起于 TypeSpec 的标识符,终于包含着它的最内的块。
在块中声明的标识符可以在其内的块中重新声明。当内部声明的标识符在作用域内时,它表示内部声明所声明的实体。
包子句 不是一个声明;包名不会在任何作用域中出现。它的目的是确定一个文件属于相同的 包 和针对导入声明指定默认的包名。
标签作用域
标签是由 标签语句 所声明的,它用在 “break” 、 “continue” 和 “goto” 语句中。定义一个不去用的标签是非法的。与其它标识符相对比,标签不按块分作用域,也不和那些不是标签的标识符冲突。标记的作用域是声明时所在的函数的主体,不过要排除所有嵌套函数的主体。
空白标识符
空白标识符 由下划线字符 _
所代表。它充当一个匿名的占位符替代通常的(非空白的)标识符,并且作为 操作数 在 声明 和 赋值 中有特殊的意义。
预声明的标识符
以下的标识符是在 宇宙块 中被隐式地定义的:
|
|
暴露的标识符
标识符可以被 暴露 用来允许从另一个包访问到它。一个标识符将会被暴露如果同时满足:
所有其它的标识符是不暴露的。
标识符的唯一性
给定一组标识符,如果一个标识符与在该集合中的所有其它都 不同 ,那么其便被称为是 唯一的 。如果两个标识符拼写不同,或它们处于不同的 包 并且没有被暴露,那么它们便是不同的。否则,它们便是相同的。
常量声明
常量声明绑定了一个标识符的列表(常量的名字)到 常量表达式 列表的值。标识符的数量必须等于表达式的数量,并且左侧第 n 个标识符绑定到了右侧第 n 个表达式的值。
|
|
如果类型提供了,那么所有常量需采用该指定类型,并且表达式必须 可分配 到该类型,且该类型不能是一个类型形参。 如果类型省略了,常量为对应表达式的独立的类型。如果表达式的值为非类型化的 常量 ,那么声明的常量保持为非类型化的,常量标识符表示着该常量的值。 比如,如果一个表达式为浮点数字面值,那么即使字面值的小数部分为零,常量标识符依旧表示一个浮点数常量。
|
|
在括起来的 const
声明列表中,除了第一个常量声明外,其它的表达式列可以省略。这样的一个空列表相当于第一个前面的非空表达式列表及其类型(如果有的话)的文本替换。省略表达式的列表就因此相当于重复之前的列表。标识符的数量必须等于之前列表的表达式的数量。这个机制结合 iota 常量生成器允许了连续值的轻量声明:
|
|
Iota
在一个 常量声明 中,预先声明的标识符 iota
代表连续的非类型化的整数 常量 。它的值从零开始,是在常量声明中各自的 ConstSpec 的索引。其可以用于构造一组相关的常量:
|
|
定义上,在同一个 ConstSpec 中使用的多个 iota
都拥有相同的值:
|
|
最后一个例子利用了上一个非空表达式列表的 隐式重复 。
:ruby:类型声明|Type declarations
一个类型声明绑定了一个标识符(也就是 类型名 )到一个 类型 。类型声明有两种形式:别名声明和类型定义。
|
|
:ruby:别名声明|Alias declarations
别名声明绑定了一个标识符到一个给定的类型。
|
|
在标识符的 作用域 内,它充当了该类型的 别名 。
|
|
:ruby:类型定义|Type definitions
类型定义创建一个新的,不同的类型,其具有与给定类型相同的 潜在类型 和操作,并将标识符和 类型名 绑定到它。
|
|
新类型被称为 定义类型 。它和其它任何的类型(包括那个给定类型)都是 不同的 。
|
|
定义类型可能具有与之关联的 方法 。它不会继承任何绑定到给定类型的方法,但接口类型或者复合类型元素的 方法集 是保持不变的:
|
|
类型声明可以用于定义不同的布尔、数值或字符串类型,并关联方法给它:
|
|
如果类型定义指定了 类型形参 ,那么这个类型名代表一个 泛型 。 泛型在它们使用时必须被 实例化 。
|
|
在一个类型定义中,给定的类型不能是一个类型形参。
|
|
泛型也可能有与之关联的 方法 。 在这种情况下,方法接收器声明的类型形参数量必须与当前泛型定义中的数量相等。
|
|
:ruby:类型形参声明|Type parameter declarations
类型形参列表在一个泛型函数中或类型定义中声明了 类型形参 。类型形参列表看起来就像一个寻常的 函数形参列表 ,除了类型形参名都必须已经存在且这个列表是闭合在方括号中而不是花括号。
|
|
列表中所有非空白的名字都必须是唯一的。 每个名字都声明了一个类型形参,每个类型形参都是一个新的且不同的 命名类型 ,用来作为声明中(至今的)未知类型的占位符。
|
|
就像每个寻常的函数形参都有一个形参类型一样,每个类型形参也有一个对应的被称为其 类型约束 的(元)类型。
当泛型的类型形参列表声明了一个带类型约束 C
的单一类型形参 P
时 (这样的 P C
形成了有效的表达式:
|
|
),那么会发生 :ruby:解析歧义|parsing ambiguity
。
在这种罕见的情况下,类型形参声明很难与表达式进行区分, 导致该类型声明被解析为一个数组类型声明。 为了解决这种歧义,可以将该约束嵌入到一个 接口 中,或在尾部添上逗号:
|
|
类型形参也可以被与泛型相关联的 方法声明 的接收器规格所声明。
:ruby:类型约束|Type constraints
类型约束是一个 接口 ,这个接口为对应的类型形参定义了一组允许的类型实参,并控制着被该类型形参的值所支持的操作。
|
|
如果约束是一个 interface{E}
形式的接口字面值,其中 E
是一个嵌入类型元素(非方法),那么为了方便在类型参数列表里的封闭的 interface{ … }
可以被省略:
|
|
预先声明的 接口类型 comparable
表示所有 可比较的 的非接口类型的集合。确切来说,如果:
T
不是一个接口类型且T
支持==
或!=
操作;或者T
是一个接口类型但T
类型集 内的每一个类型都实现了comparable
,
那么类型 T
就实现了 comparable
。
尽管非类型形参的接口可以 被比较 (可能会导致一个 run-time panic),但它们并没有实现 comparable
。
|
|
comparable
这个接口以及那些(直接或间接)嵌入了 comparable
的接口仅用作类型约束。它们不能作为值或者变量的类型,或其它及非接口类型的组件。
:ruby:变量声明|Variable declarations
一个变量声明创建一个或多个变量,给它们绑定对应的标识符,并且给每个分一个类型和一个初始化的值。
|
|
如果给出了表达式列表,那么变量会根据 赋值 规则由表达式来初始化。否则,每个变量都被初始化为其 零值 。
如果类型提供了,那么每个变量都会指定为那个类型。否则,每个变量的类型会被给定为赋值中对应的初始化值的类型。如果那个值是非类型化的常量,它会先隐式地 转换 为它的 默认类型 ;如果它是一个非类型化的布尔值,那么它会先隐式地转换为类型 bool
。预先声明的值 nil
不能用于初始化没有明确类型的变量。
|
|
实现限制:当在 函数实体 中定义的变量没有被使用时,编译器可以认定它为非法的。
短变量声明
短变量声明 使用如下语法:
|
|
这是如下这种带初始化表达式而不带类型的 变量声明 的速记法:
|
|
和普通的变量声明不同,短变量声明可以 重复声明 一个变量,这个变量是在同一个块(或者参数列表——如果该块是一个函数实体的话)内之前已经声明过的,且变量类型不能改变,但是重复声明语句最少要存在一个新的非 空白 变量。因此,重复声明仅能出现在多变量短声明中。重复声明不会引进新的变量;它仅赋一个新的值到原变量。
|
|
短变量声明只能出现在函数内。在一些针对诸如 “if” 、 “for” 或 “switch” 这样的初始化器的上下文中,也可以用于声明本地临时变量。
:ruby:函数声明|Function declarations
函数声明绑定一个标识符(也就是 函数名 )到一个函数。
|
|
如果函数的 签名 声明了结果参数,那么函数体语句列表必须以 终止语句 结尾。
|
|
如果函数声明指定了 类型形参 ,那么这个函数名表示一个 泛型函数 。在被调用或作为值使用前,泛型函数必须被 实例化 。
|
|
不带类型形参的函数声明可以省略其实体。这样的声明为在 Go 外部实现(比如汇编程序)的函数提供了签名。
|
|
:ruby:方法声明|Method declarations
方法是带 接收器 的 函数 。一个方法声明绑定了一个标识符(也就是 方法名 )为一个方法,并与接收器的 基础类型 关联。
|
|
接收器是使用在方法名之前额外的参数段来指定的。这个参数段必须声明一个单一非 variadic 形参作为接收器。其类型必须是一个 定义类型 T
或到定义类型 T
的指针,后面可能跟着一列由方括号包裹的类型形参名 [P1, P2, …]
。 T
被称为接收器的 基本类型 。 接收器的基本类型不能是一个指针或者接口类型,并且它必须在和方法相同的包中被声明。 这个方法就被称为 绑定到了 这个基本类型,且方法名只有通过类型 T
或 *T
的 选择器 才可见。
Note
译注:方法的基础类型不能是接口,这边不要混淆,接口是一组方法签名的集合,也就是可以定义一个固定类型为一个接口类型,这个固定类型实现了对应接口类型所声明的方法。
一个非 空白 接收器标识符在方法签名中必须是 唯一的 。如果接收器的值在方法实体内没有被引用,那么其标识符在声明时是可以省略的。一般来说这也同样适用于函数和方法的参数。
对一个基础类型来说,绑定到它的非空白的方法名必须是唯一的。如果基础类型为 结构体类型 。那么非空白的方法和字段名必须是不同的。
给定一个定义类型 Point
,其声明
|
|
绑定了方法 Length
和 Scale
,接收器类型为 *Point
,对应基础类型 Point
。
如果接收器的基本类型是一个 泛型 ,那么 :ruby:接收器规格|the receiver specification
必须声明对应的类型形参以供该方法使用。这使得接收器类型形参对该方法可用。
从语句构成上看,类型形参声明像接收器基本类型的 实例化 : 类型实参必须为标识符(这个标识符表示被声明的类型形参),接收器基本类型的每个类型形参各一个。 类型形参名无需匹配接收器基本类型定义中对应的形参名, 但所有在接收器形参段和方法签名中的非空白的形参名都必须是唯一的。 接收器类型形参的约束是被接收器基本类型定义所隐含的:对应的类型形参有对应的约束。
|
|
表达式
表达式将运算符和函数应用于操作数来规定值的计算。
:ruby:操作数|Operands
操作数表示表达式中基本的值。一个操作数可能是一个字面值;可能是一个(可能为 限定的 )表示 常量 、 变量 或 函数 的非 空白 标识符或者一个圆括号括起来的表达式。
|
|
表示 泛型函数 的操作数名可能会紧跟一个 类型实参 列表; 产生的操作数是一个 实例化过的 函数。
实现限制:当操作数类型是带空 类型集 的 类型形参 时,编译器不必报告错误。 带这样类型形参的函数无法被 实例化 ;任何尝试都会导致在实例化处报错。
:ruby:限定标识符|Qualified identifiers
限定标识符 是由包名前缀所限定的标识符。包名和标识符都不能为 空白 。
|
|
限定标识符可以在不同的包内访问一个标识符,该标识符对应的包必须已经被 导入 。标识符则必须已经在那个包被 暴露 并在 包块 中被声明。
|
|
:ruby:复合字面值|Composite literals
复合字面值在每次被求值时创建一个新的复合值。 复合字面值由字面值类型和紧跟着的花括号绑定的元素列表所组成。每个元素可以选择前缀一个对应的键。
|
|
LiteralType 的 核心类型 T
必须是结构体、数组、分片或者映射类型 (文法强制执行此约束,当类型是 TypeName 时除外)。元素和键的类型必须 可分配 给类型 T
所对应的字段、元素和键类型;这里没有额外的转换。 该键被解释为结构体字面值的字段名,数组和分片字面值的索引,映射字面值的键。 对于映射字面值而言,每个元素都要有一个键。给多个元素指定相同的字段名或者不变的键值会出错。 对于非常量映射键,可以查阅 求值顺序 。
对结构体字面值来说,应用如下规则:
- 键必须是在结构体类型中声明的字段。
- 不包含任何键的元素列表必须对每个结构体字段(字段声明的顺序)列出一个元素。
- 只要一个元素有键,那么每个元素都必须要有键。
- 包含键的元素列表不需要针对每个结构体字段有一个元素。省略的字段会获得一个零值。
- 字面值可以省略元素列表;这样子的字面值相当于其类型的零值。
- 针对属于不同包的结构体的非暴露字段来指定一个元素是错误的。
给定一个声明
|
|
你可以写
|
|
对数组和分片字面值来说,应用如下规则:
- 数组中的每个元素有一个关联的标记其位置的整数索引。
- 带键的元素使用该键作为其索引。这个键必须是可被类型
int
所表示的一个非负常量; 且如果其被类型化了的话,则必须是 整数类型 。 - 不带键的元素使用之前元素的索引加一。如果第一个元素没有键,则其索引为零。
一个复合变量的 寻址 生成了一个到由字面值值初始化的唯一 变量 的指针。
|
|
注意的是,分片和映射类型的零值不同于同类型的初始化过但为空的值。所以,获取空的分片或映射复合字面值的地址与使用 new 来分配一个新的分片或映射的效果不同。
|
|
数组字面值的长度是字面值类型所指定的长度。在字面值中,如果少于其长度的元素被提供了,那么缺漏的元素会被设置为数组元素类型的零值。提供其索引值超出了数组索引范围的元素是错误的。符号 ...
指定一个数组长度等于其最大元素索引加一。
|
|
分片字面值描述了整个底层数组字面值。因此一个分片字面值的长度和容量为其最大元素索引加一。分片字面值的格式为
|
|
以及针对应用到数组的分片操作的速记为
|
|
在数组、分片或者映射类型 T
的复合字面值中,如果元素或映射的键本身为复合字面值,当其字面值类型和 T
的元素或键类型一致时,该字面值类型可以省略。类似的,如果元素或键本身为复合字面值的地址,当元素或键的类型为 *T
时,该元素或键可以省略 &T
。
|
|
当一个使用 LiteralType 的 TypeName 形式的复合字面值表现为一个在 关键字 和 “if” 、 “for” 或 “switch” 语句块的左花括号之间的操作数,并且该复合字面值不被圆括号、方括号或花括号所包围时,会出现一个解析歧义。在这样子一个罕见的情况下,复合字面值的左花括号错误地被解析为语句块的引入。为了解决这样子的歧义,这个复合字段必须在圆括号内。
|
|
有效的数组、分片和映射字面值的例子:
|
|
:ruby:函数字面值|Function literals
函数字面值代表一个匿名 函数 。函数字面值不能声明类型形参。
|
|
函数字面值可以被赋给一个变量或者直接调用。
|
|
函数字面值是 闭包 :它们可以引用外层函数定义的变量。然后这些变量就在外层函数和函数字面值间共享了,并且只要能被访问就可以一直存活。
:ruby:主表达式|Primary expressions
主表达式是一元表达式和二元表达式的操作数。
|
|
选择器
|
|
表示了值 x
(或者有时候为 *x
;见下文)的字段或方法 f
。标识符 f
被称为(字段或方法) 选择器 ,它一定不能为 空白标识符 。选择器表达式的类型为 f
的类型。如果 x
是一个包名,看 限定标识符 一节。
选择器 f
可以表示一个类型 T
的一个字段或方法 f
,或者可以指嵌套在 T
中的 嵌入字段 的字段或方法 f
。遍历以达到 f
所经历的嵌入字段数被称为 f
在 T
中的 深度 。在 T
中声明的字段或者方法 f
的深度为零。在 T
中的嵌入字段 A
中声明的字段或者方法 f
的深度为 A
中 f
的深度加一。
以下规则应用于选择器:
- 对于为类型
T
或*T
的值x
(T
既不是指针类型也不是接口类型),x.f
表示在T
中最浅深度的字段或者方法f
。如果不是恰好 一个f
在最浅深度的话,那么这个选择器表达式就是非法的。 - 对于为接口类型
I
的值x
,x.f
表示动态值x
的名为f
的实际的方法。如果在I
的 方法集 中没有名为f
的方法,那么这个选择器表达式就是非法的。 - 作为例外,如果
x
的类型为一个 定义的 指针类型并且(*x).f
是一个有效的表示一个字段(但不是方法)的选择器表达式,那么x.f
是(*x).f
的速记。 - 在所有其它情况中,
x.f
是非法的。 - 如果
x
是指针类型并且值为nil
并且x.f
表示一个结构体字段,那么,给x.y
赋值或求值会导致一个 run-time panic 。 - 如果
x
是接口类型并且值为nil
,那么 调用 或 求值 方法x.y
会导致一个 run-time panic 。
Note
这边好好熟悉,规则 2 应该要结合方法声明/调用那节一起看
举例,给定声明:
|
|
你可以写:
|
|
但下述是无效的:
|
|
方法表达式
如果 M
在类型 T
的 方法集 中,那么 T.M
是一个函数,该函数可以携带和 M
同样的实参像普通函数一样调用,不过会给其前缀一个额外的实参作为该方法的接收器。
|
|
考虑有两个方法的结构体类型 T
,方法一是接收器为类型 T
的 Mv
,其二是接收器为类型 *T
的 Mp
。
|
|
表达式
|
|
产生一个等同于 Mv
但带一个明确的接收器作为其第一个实参的函数;它的签名为
|
|
这个函数可以在带一个明确的接收器情况下被正常地调用,所以以下五种调用是等同的:
|
|
类似的,表达式
|
|
产生一个签名为如下的代表 Mp
的函数值
|
|
对于一个带值接收器的方法,可以推导出一个带明确指针接收器的函数,所以
|
|
产生一个签名为如下的代表 Mv
的函数值
|
|
这样的一个函数通过接收器创建一个值间接地将其作为接收器传递给底层函数;这个方法在函数调用中不会覆盖那个地址被传递的值。
最后一种情况——值接收器的函数对指针接收器的方法——是非法的,因为指针接收器的方法不在该值类型的方法集中。
从方法推导出的函数值是用函数调用语法来调用的;接收器作为调用的第一个实参。也就是,给定 f := T.Mv
, f
是作为 f(t, 7)
而非 t.f(7)
被调用的。使用 函数字面值 或 方法值 来构建一个绑定了接收器的函数。
从一个接口类型的方法中得到一个函数值是合法的。所得到的函数使用该接口类型的显式的接收器(原文: The resulting function takes an explicit receiver of that interface type. )。
方法值
如果表达式 x
有静态类型 T
,并且 M
在类型 T
的 方法集 中,那么 x.M
被称为一个 方法值 。方法值 x.M
是一个可以用与 x.M
的方法调用的相同的实参来调用的函数值。表达式 x
在该方法值的求值过程中被求值和保存;保存的副本被用在(可能会在后续被执行的)任意调用中作为接收器。
|
|
类型 T
可以为接口或者非接口类型。
就像上面 方法表达式 所讨论的,考虑一个带两个方法的结构体 T
,方法一是接收器为类型 T
的 Mv
,其二是接收器为类型 *T
的 Mp
。
|
|
表达式
|
|
产生了一个类型如下的函数值
|
|
这两种调用是等同的:
|
|
类似的,表达式
|
|
产生了一个类型如下的函数值
|
|
就 选择器 来说,如果以值作为接收器的非接口方法使用了指针来引用,那么会自动解除到该指针的引用: pt.Mv
等同于 (*pt).Mv
。
就 方法调用 来说,如果以指针作为接收器的非接口方法使用了可寻址值来引用,那么会自动获取该值的地址来引用: t.Mp
等同于 (&t).Mp
。
|
|
虽然以上的例子使用了非接口类型,但是从接口类型的值来创建一个方法值同样是合法的。
|
|
索引表达式
如下形式的主表达式
|
|
表示了可被 x
索引的数组、到数组的指针、分片、字符串或者被 x
索引的映射 a
的元素。值 x
分别被称为 索引 或 映射键 。以下规则应用于:
如果 a
既不是一个映射又不是一个类型形参:
- 索引
x
必须是非类型化的常量或者其 核心类型 必须是 整数类型 。 - 常量索引必须为非负且可以被类型
int
所表示的 的一个值 - 非类型化的常量索引会被给定一个类型
int
- 当
0 <= x < len(a)
时,索引x
在范围内 ,否则它就 超出了范围
对于为 数组类型 A
的 a
:
- 常量 索引必须在范围内
- 如果在运行时
x
超出了范围,那么会发生一个 run-time panic a[x]
是一个在索引x
处的数组元素,且a[x]
的类型是A
的元素类型
对于到数组类型的 指针 a
:
a[x]
是(*a)[x]
的速记
对于为 分片类型 S
的 a
:
- 如果在运行时
x
超出了范围,那么会发生一个 run-time panic a[x]
是在索引x
处的分片元素,且a[x]
的类型是S
的元素类型
对于 字符串类型 a
:
- 当字符串
a
是常量时, 常量 索引必须在范围内 - 如果在运行时
x
超出了范围,那么会发生一个 run-time panic a[x]
是在索引x
处的非常量字节,并且a[x]
的类型为byte
a[x]
不能被赋值
对于为 映射类型 M
的 a
:
x
的类型必须是 可分配 为M
的键类型的- 如果映射带键为
x
的条目,那么a[x]
是带键x
的映射值,并且a[x]
的类型为M
的值类型。 - 如果映射为
nil
或者不存这样这样子的一个条目,那么a[x]
是针对M
的值类型的 零值 。
对于为 类型形参 P
的 a
:
- 索引表达式
a[x]
必须对P
类型集中所有类型的值有效。 P
类型集中所有类型的元素类型都必须是一致的。 关于这点,string 类型的的元素类型是byte
。- 如果
P
类型集的类型中有映射, 那么该类型集中的所有类型都必须是映射类型,且对应的键类型都必须是一致的。 a[x]
是在索引x
处的数组、分片或字符串元素, 或带P
实例化时所用类型实参(所代表类型的)键x
的映射元素, 且a[x]
的类型为(一致的)元素类型的类型。- 如果
P
类型集中的类型包含字符串类型,那么a[x]
可能无法被分配到。
否则 a[x]
是非法的。
对类型为 map[K]v
的映射 a
使用特殊格式的 赋值 或初始化索引表达式
|
|
会产生一个额外的非类型化的布尔值。当键 x
存在于映射中时, ok
的值为 true
,否则为 false
。
给 nil
映射的元素赋值会导致一个 run-time panic 。
分片表达式
分片表达式从一个字符串、数组、到数组的指针或者分片中构建一个子字符串或者一个分片。有两种变体:指定一个低位和高位边界的简单格式,以及同时在容量上有指定的完整格式。
简单的分片表达式
主表达式
|
|
构造了一个子字符串或者分片。 a
的 核心类型 必须字符串、数组、到数组的指针或者分片类型。 索引 low
和 high
选择了操作数 a
的哪些元素显示在结果种。结果有从零开始且长度等于 high - low
的索引。 在分片了数组 a
后
|
|
分片 s
有类型 []int
,长度 3,容量 4,以及元素
|
|
为了方便,每一个索引都可能被省略。缺少的 low
索引默认为零;缺少的 high
索引默认为被分片的操作数的长度:
|
|
如果 a
为到数组的指针,那么 a[low : high]
为 (*a)[low : high]
的速记。
对于数组或者字符串,如果 0 <= low <= high <= len(a)
,那么索引是 在范围内 的,否则就 超出了范围 。对于分片,上索引边界是分片的容量 cap(a)
而不是其长度。 常量 索引必须为非负且是可以被类型 int
所表示的 ;对于数组和常量字符串而言,常量索引也必须在范围内。如果两个索引都是常量,那么它们必须满足 low <= high
。如果在运行时索引超出了范围,那么会发生 run-time panic 。
除了 非类型化的字符串 以外,如果被分片的操作数是一个字符串或者分片,那么分片操作的结果为一个和该操作数具有相同类型的非常量值。对于非类型化的字符串操作数而言,其结果是一个类型为 string
的非常量值。如果被分片的操作数是一个数组,那么它必须是 可被寻址的 ,并且分片操作的结果为和该数组具有相同元素类型的分片。
如果一个有效的分片表达式的被分片的操作数是一个 nil
分片,那么结果是一个 nil
分片。否则,结果会共享该操作数的底层数组。
|
|
完整的分片表达式
主表达式
|
|
构建了一个有相同类型的分片,并且带有和简单的分片表达式 a[low : high]
一样的长度和元素。此外,它通过设置分片的容量为 max - low
来控制产生分片的容量。只有第一个索引是可以被省略的;默认为零。 a
的 核心类型 必须是数组、到数组的指针或者分片类型(但不能是字符串类型)。 在分片了数组 a
后
|
|
分片 t
有类型 []int
,长度 2,容量 4,以及元素
|
|
和简单的分片表达式一样,如果 a
是一个到数组的指针,那么 a[low : high : max]
是 (*a)[low : high : max]
的速记。如果被分片的操作数是一个数组,那么它必须是 可被寻址的 。
如果 0 <= low <= high <= max <= cap(a)
,那么索引是 在范围内 的,否则就 超出了范围 。 常量 索引必须是非负的且可以被类型 int
所代表的值;对于数组,常量索引也必须在范围内。如果多个索引为常量,那么存在的常量必须在相对彼此的范围内。如果在运行时索引超出了范围,那么会出现一个 run-time panic 。
类型断言
对于一个为 接口类型 但非 类型形参 的表达式 x
以及一个类型 T
,主表达式
|
|
断言 x
不为 nil
并且存储在 x
中的值具有类型 T
。记法 x.(T)
被称为 类型断言 。
更准确地来说,如果 T
不是一个接口类型,那么 x.(T)
断言 x
的动态类型和类型 T
一致 。在这种情况下, T
必须 实现 x
的(接口)类型;否则类型断言是无效的,因为对于 x
来说存储一个类型为 T
的值是不可能的。如果 T
是一个接口类型,那么 x.(T)
断言 x
的动态类型 实现了 接口 T
。
如果类型断言成立,那么表达式的值为存储在 x
中的值,并且其类型为 T
。如果类型断言不成立,会发生一个 run-time panic 。换句话来说,即使 x
的动态类型仅在运行时可知, x.(T)
的类型也可以在一个正确的程序中被已知为 T
。
|
|
用于 赋值 或如下特殊格式的初始化中的类型断言
|
|
产生一个额外的非类型化的布尔值。如果断言成功,那么 ok
的值为 true
。否则为 false
,并且 v
的值为类型 T
的 零值 。这种情况下不会发生 run-time panic。
调用
给定一个带 核心类型 为函数类型 F
的表达式 f
,
|
|
带实参 a1, a2, … an
调用了 f
。除了一种特殊情况以外,实参必须是单一值的 可分配 给 F
的参数类型的表达式,并且它们在函数调用之前就被求值好了。上述函数表达式的类型是 F
的结果类型。方法调用是类似的,但是方法本身是被指定为一个在该方法的接收器的值之上的选择器。
|
|
如果 f
表示一个泛型函数,那么在它可以被调用前或者作为一个函数值前,必须先被 实例化 。
在一个函数调用中,函数值和实参使用 通常的顺序 被求值。在它们求值好后,调用的参数以值传递给函数,然后被调用的函数开始执行。函数的返回参数在函数返回时以值返回给调用者。
调用一个 nil
函数会发生 run-time panic 。
作为一个特殊情况,如果一个函数或方法 g
的返回值数量上等于且可以分别被分配给另一个函数或方法 f
的参数,那么调用 f(g(parameters_of_g))
将会在按序绑定了 g
的返回值到 f
的参数后调用 f
。 f
这个调用必须排除 g
调用以外的参数,并且 g
必须要有最少一个返回值。如果 f
有一个最终的 ...
参数,这个参数会被分配那些在普通参数赋值完之后的剩余的 g
的返回值。
|
|
如果 x
的(类型的)方法集包含了 m
,并且实参列表可以被分配给 m
的形参列表,那么方法调用 x.m()
是有效的。如果 x
是 可被寻址的 并且 &x
的方法集包含了 m
,那么 x.m()
是 (&x).m()
的速记:
|
|
这里没有明确的方法类型,也没有方法字面值。
传递实参给 ...
参数
如果 f
是带最终参数 p
(其类型为 ...T
)的 variadic ,那么在 f
内, p
的类型等同于类型 []T
。如果 f
在调用时没有实参给 p
,那么传递给 p
的值为 nil
。否则,传递的值是一个新的类型为 []T
的分片,这个分片带一个底层数组,这个底层数组的连续的元素作为实参,并且必须 可分配 给 T
。因此该分配的长度和容量是绑定到 p
的实参的数量,而且每次调用可能会不同。
给定函数和调用
|
|
在 Greeting
中, who
的值第一次调用时为 nil
,在第二次调用时为 []string{"Joe", "Anna", "Eileen"}
。
如果最终的实参可分配给一个分片类型 []T
且其后跟着 ...
的话,它就会在不改变值的情况下传递给一个 ...T
参数。在这种情况下,不会创建新的分片。
给定一个分片 s
和调用
|
|
在 Greeting
内, who
有和 s
有同一个值和同一个底层数组。
:ruby:实例化|Instantiations
泛型或泛型函数是通过用 类型实参 替代类型形参而 被实例化 的。 实例化会分两个步骤进行:
- 每个类型实参替换掉在泛型声明中对应的类型形参。 这个替换行为会在整个函数或类型声明中发生,包括类型形参列表自身及列表中的每个类型。
- 替换完成后,每个类型实参必须 能实现 对应类型形参的 约束 (若有必要则实例化它)。 否则实例化会失败。
实例化一个类型会产生一个新的非泛型的 命名类型 ; 实例化一个函数会产生一个新的非泛型的函数。
|
|
对于泛型函数,其类型实参可以被明确地提供,或者被部份或完全 推断 。 未被 调用 的泛型函数需要类型实参列表以用于实例化; 如果列表是不完整的,那么剩下所有的实参都必须是可被推断的。 调用的泛型函数可以提供一份(可能不完整的)类型实参列表; 也可以整个省略它,前提是被省略的类型实参是可以从普通的(非类型)函数实参中推断得出的。
|
|
一份部份提供的类型实参列表不能是空的;至少第一个实参需存在。 这个列表是完整实参列表的前缀,剩下的实参则留待推断。简单地说,类型实参可以 「从右往左」被省略。
|
|
对于泛型(类型,非函数),则所有类型实参都要被明确的提供。
类型推断
缺失的函数类型实参可以通过一系列步骤 被推断出 ,如下所述。 每个步骤都会尝试使用已知的信息来推断额外的类型实参。 一旦所有类型实参都已知,类型推断就会停止。 类型推断完成后,仍会将所有类型形参替换为类型实参,并验证每个类型实参是否 实现了 相关约束; 推断的类型实参可能会无法实现其约束,这种情况下实例化会失败。
类型推断基于:
- 类型形参列表
- 使用已知类型实参进行初始化的替换映射 M (如果有)
- (可能为空的)普通函数实参列表(仅适用于函数调用)
然后继续执行以下步骤:
如果没有普通或非类型化的函数实参,那么对应的步骤是跳过的。 如果前一个步骤没有推断任何新的类型实参,那么约束类型推断是跳过的, 但在有缺失的类型实参时,这个步骤会至少运行一次。
替换映射 M 是贯穿所有步骤的,且每个步骤可能会添加条目到 M 。 一旦 M 针对每个类型形参都有一个类型实参,或推断步骤失败时,该过程就会停止。 如果一个推断过程失败了,或者在最后的步骤之后 M 依旧缺失类型实参,那么推断失败。
:ruby:类型联合|Type unification
类型推断是基于 类型联合 的。 一个单一的联合步骤应用于一个 替换映射 和两个类型,这两个类型中的一个或两个可能是或包含有类型形参。 替换映射会追踪已知的(明确提供的或者已经被推断出的)类型实参:这个映射对每个类型形参 P
及对应的已知类型实参 A
包含有一个条目 P
→ A
。 联合过程中,已知的类型实参会在比较时取代其对应类型形参的位置。 联合过程是一个寻找可以使两个类型等同的替换映射条目的过程。
对于联合过程而言,如果当前类型形参列表中的两个不包含任何类型形参的类型其本身是一致的, 或者它们是信道类型且在忽略信道方向下是一致的,或者它们的潜在类型是等同的,那么它们就是 等同的 。
联合过程是通过比较类型对的结构来进行的: 忽视类型形参后的结构必须是一致的,且除类型形参以外的类型必须是等同的。 一个类型中的类型形参能匹配其它类型中任何完整的子类型; 每次成功的匹配都能使一个条目被添加到替换映射中。 如果结构不一致,或者除类型形参外的类型不等同,那么联合失败。
比如,如果 T1
和 T2
是类型形参, 那么 []map[int]bool
可以被以下任何联合:
|
|
另一方面, []map[int]bool
不能被以下任何联合:
|
|
作为这个一般规则的一个例外,因为 定义类型 D
和类型字面值 L
是永不等同的, 所以联合过程会转而用 D
的潜在类型和 L
去比较。 比如,给定一个定义类型
|
|
以及类型字面值 []E
,联合过程会比较 []float64
和 []E
并且添加条目 E
→ float64
到替换映射。
函数实参类型推断
函数实参类型推断从函数实参中推荐类型实参: 如果函数形参声明时带有使用了类型形参的类型 T
, 那么将对应函数实参的类型与 T
进行 联合 可能会推断出被 T
所使用的类型形参的类型实参。
例如,给定泛型函数
|
|
及调用
|
|
Number
的类型实参可以通过联合 vector
的类型与对应的类型形参 从而从函数实参 vector
中推断得出: []float64
与 []Number
在结构上是匹配的且 float64
与 Number
是匹配的。 这会添加条目 Number
→ float64
到 替换映射 。 如此处第二个函数实参这样的非类型化的实参会在函数实参类型推断的第一轮中被忽略, 且只有在还有未解决的类型形参时才会被考虑。
推断会在两个单独的阶段发生;每个阶段会在一份指定的(形参、实参)配对列表上进行操作:
- 列表 Lt 包含了所有使用了类型形参的形参类型和 类型化的 的函数实参所组成的(形参、实参)配对。
- 列表 Lu 包含了所有剩下的参数类型是单一类型形参的配对。在这个列表中,各自的函数实参都是非类型化的。
任何其它(形参、实参)配对是被忽略的。
通过构造,在 Lu 中配对的实参是 非类型化的 常量(或一个比较的非类型化的布尔型结果)。 且因为非类型化的值的 默认类型 总为预先声明的非复合类型,它们永远无法与复合类型匹配, 所以仅考虑是单一类型形参的形参类型就足够了。
每个列表都是在一个独立阶段中处理的:
- 在第一个阶段中, Lt 中每个配对的形参和实参类型会被联合。 如果一个配对的联合过程成功了,它们可能会产生添加到替换映射 M 的新条目。 如果联合失败,那么类型推断就会失败。
- 第二个阶段考虑
Lu
列表的条目。那些类型实参已经被确定的类型形参在这个阶段会被忽略。 针对每一个剩下的配对,(单一类型形参的)形参类型会与对应的非类型化的实参的 默认类型 进行联合。 如果联合失败,那么类型推断就会失败。
当联合过程成功时,即使在最后的列表元素被处理到之前所有的类型实参就都被推断出了, 每个列表的处理还是会继续进行直到所有列表元素都被考虑到。
示例:
|
|
在示例 min(1.0, 2)
中,处理函数实参 1.0
会产生替换映射条目 T
→ float64
。 因为在所有非类型化实参被考虑之前处理过程会继续,所以一个错误被报告了。 这保证了类型推断不依赖非类型化实参的顺序。
约束类型推断
约束类型推断通过考虑类型约束来推断类型实参。 如果类型形参 P
有带 核心类型 C
的约束,那么 联合 P
与 C
可能推断出额外的类型实参,可能是 P
的类型实参,如果这个已知,那么也可能是 C
中使用的类型形参的类型实参。
例如,考虑一个带类型形参 List
和 Elem
的类型形参列表:
|
|
因为 Elem
是 List
的核心类型 []Elem
中的一个类型形参, 所以约束类型推断可以从 List
的类型实参中推断 Elem
的类型。 如果该类型实参是 Bytes
:
|
|
那么联合 Bytes
的潜在类型与该核心类型就意味着联合 []byte
和 []Elem
。 这个联合过程会成功并且产生 替换映射 : Elem
→ byte
。 因此,在这个例子中,约束类型推断能从第一个类型实参中推断出第二个的。
使用约束的核心类型可能会丢失一些信息: 针对一个(不太可能的)约束的类型集仅包含单一 定义类型 N
的情况, 对应的核心类型是 N
的潜在类型而不是 N
自己。在这种情况下, 约束类型推断可能成功但是实例化会失败,因为推断的类型不在约束的类型集里。 因此,约束类型推断使用约束的 调整后的核心类型 : 如果类型集仅包含单一类型,那就使用这个类型;否则使用约束的核心类型。
通常,约束类型推断分两个阶段进行:从给定的替换映射 M 开始
- 对所有带调整后的核心类型的类型形参,联合该类型与其类型形参。 如果有任意联合过程失败,那么约束类型推断失败。
- 此时,一些 M 中的条目可能映射了类型形参到其它类型形参或包含有类型形参的类型。 对于 M 中每个条目
P
→A
,其中A
是或者包含有 M 中已存条目Q
→B
的 类型形参Q
,用对应的B
替换掉这些A
中的Q
。 在无法进行进一步替换时停止。
约束类型推断的结果是一份没有类型形参 P
出现在任一 A
中的从类型形参 P
到类型实参 A
的最终替换映射。
例如,给定类型形参列表
|
|
以及为类型形参 A 的提供的单个类型实参 int
。初始的替换映射 M 会包含条目 A
→ int
。
在第一个阶段,类型形参 B
和 C
与其对应约束的核心类型联合。这个过程添加了条目 B
→ []C
和 C
→ *A
到 M 。
此时 M 中存在两个右侧是或者包含有 M 中其它条目的类型形参( []C
和 *A
)的条目。 在第二个阶段,这些类型形参会被用它们对应的类型取代。取代的顺序是无所谓的。从第一阶段后 M 的状态开始:
|
|
用 int
取代 → 右侧的 A
|
|
用 *int
取代 → 右侧的 C
|
|
此时没有进一步替换的可能且映射已满。因此 M 代表了给定类型参数列表的类型形参到类型实参的最终映射。
运算符
运算符把操作数结合进一个表达式。
|
|
比较运算符会在 其它地方 讨论。对于其它二元运算符来说,操作数类型必须是 一致的 ,除非运算涉及位移或者非类型化的 常量 。对于只涉及常量的运算,看 常量表达式 一节。
除了位移运算之外,如果一个操作数是非类型化的 常量 而另一个操作数不是,那么该常量会被隐式地 转换 为另一个操作数的类型。
在位移表达式的右侧的操作数必须为 整数类型 ,或者可以被 uint
类型的值 所表示的 非类型化的常量。如果一个非常量位移表达式的左侧的操作数是一个无符号常量,那么它会先被隐式地转换为假如位移表达式被其左侧操作数单独替换后的类型。
Note
译注: 2019 年 7 月版,在上面这句话中,”无符号整数类型“变成了”整数类型“,看后文的描述,应该是正数即可,负数则会恐慌。
|
|
运算符优先级
一元运算符有最高的优先级。由于 ++
和 --
运算符构成了语句(而不是表达式),超出了运算符的结构。因此,语句 *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
)。
|
|
如果除数是一个 常量 ,那么它一定不能为零。如果在运行时除数为零,那么会发生一个 run-time panic 。如果被除数不为负值并且除数可以表示为以 2 为底数的一个次方常量,那么除法可以被向右位移所替换,计算余数可以被按位与运算符所替换:
|
|
位移运算符通过右侧操作数(必须为非负)所指定的位移数来位移左侧的操作数。如果在运行时位移数为负,那么会发生一个 run-time panic 。如果左侧操作数是一个带符号的整数,那么位移运算符实现算数位移;如果是一个不带符号的整数,那么实现逻辑位移。位移数是没有上限的。对于 n
个位移数来说,位移行为犹如左侧操作数以间隔 1 来位移 n
次。因此, x << 1
等于 x*2
而 x >> 1
等于 x/2
,不过向右位移会向负无穷截断。
对于整数操作数,一元运算符 +
, -
和 ^
有如下定义:
|
|
整数溢出
对于无符号整数值来说, +
, -
, *
和 <<
运算是以 2^n
为模来计算的, n
为 无符号整数 类型的位宽。大致来说就是,这些无符号整数运算丢弃了溢出的高位,并且程序可以依赖于 “wrap around” 。
对于带符号整数值来说, +
, -
, *
, /
和 <<
运算可以合法地溢出,其产生的值是存在的并且可以被带符号整数表示法、其运算和操作数明确地定义。溢出不会发生 run-time panic 。编译器不会在不发生溢出这个假设情况下来优化代码。比如,它不会假设 x < x + 1
始终是真。
浮点数运算符
对于浮点数和复数来说, +x
和 x
是一样的,但是 -x
是负的 x
。除了 IEEE-754 标准外,没有规定浮点数或者复数除以零的值;是否发生 run-time panic 是由具体实现规定的。
某个实现可能会组合多个浮点数操为单一的融合操作(可能跨语句的), 然后产生一个与单独执行指令再取整所不同的值。一个显示的 浮点数类型 转换 会约到目标类型的精度,以防会丢弃该舍入的融合。
比如,有些架构提供了一个“积和熔加运算”(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
,那么X
的值x
和T
的值t
是可比较的。如果t
的动态类型和X
一致并且t
的动态值等于x
的话,那么它们相等。 - 当结构体的所有字段都是可比较的,那么该结构体是可比较的。如果两个结构体的对应的非 空白 字段相等,那么两个结构体相等。
- 如果数组的元素值是可比较的,那么该数组是可比较的。如果两个数组对应的元素是相等的,那么两个数组相等。
当两个比较中的接口值的动态类型一致,但是该类型的值是不可比较的时候,会发生一个 run-time panic 。这种情况不仅仅发生在接口值比较上,同样也会发生在比较接口值数组或者带接口值字段的结构体上。
分片、映射和函数值是不可比较的。不过作为一个特例,一个分片、映射或者函数值可以和预先声明的标识符 nil
来比较。指针、信道和接口值与 nil
的比较也是允许的,并遵循上述通用规则。
|
|
逻辑运算符
逻辑运算符应用于 布尔 值,并产生一个和操作数相同类型的结果。右侧的操作数是按条件来求值的。
|
|
地址运算符
对于类型为 T
的操作数 x
来说,地址运算 &x
生成了一个类型为 *T
的到 x
的指针。该操作数必须是 可被寻址的 ,也就是,一个变量、 :ruby:指针间接|pointer indirection
、或分片索引操作;或一个可寻址的结构体操作数的字段选择器;或一个可寻址的数组的数组索引操作。作为可被寻址要求的一个例外, x
也可以是(可能带括号的) 复合字面值 。如果 x
的求值会导致一个 run-time panic ,那么 &x
的求值也会。
对于指针类型 *T
的操作数 x
来说,指针间接 *x
表示被 x
指向的类型为 T
的 变量 。如果 x
是 nil
,那么对于 *x
的求值尝试会导致一个 run-time panic 。
|
|
接收运算符
对于核心类型为 信道类型 的操作数 ch
而言,接收操作 <-ch
的值是从信道 ch
接收到的值。信道方向必须允许接收操作,并且接收操作的类型为信道的元素类型。 直到一个值可用前该表达式都会阻塞。从一个 nil
信道接收值会永远阻塞下去。针对一个 closed 信道的接收操作总是会立即进行,并在之前已经发送完成的值被接收完毕后产生一个该元素类型的 零值 。
|
|
用于 赋值 或特殊格式的初始化中的接收表达式
|
|
产生一个额外的非类型化的布尔值用于报告通信是否成功。如果接收的值被到该信道的成功的发送操作传递过来,那么 ok
的值为 true
,如果因为该信道已经关闭且为空,接收到的是零值,那么 ok
为 false
。
:ruby:转换|Conversions
转换会把一个表达式的 类型 改成被该转换所指定的类型。一个转换可能会在字面上出现在源文件中,也可能 隐含在 表达式所在的上下文中。
一个 显示的 转换是 T(x)
这样子形式的表达式,其中 T
是一个类型而 x
是一个可以被转换到类型 T
的一个表达式。
|
|
如果类型由运算符 *
或者 <-
开头,或者由关键字 func
开头并且没有结果列表,那么当必要时它必须被括起来以避免混淆:
|
|
如果一个 常量 值 x
可以被类型为 T
的值 所表示 ,那么 x
可以被转换为 T
。特殊情况下,整数常量 x
可以使用像非常量 x
一样的规则 被显示地转换为 字符串类型 。
转换常量到一个非 类型形参 的类型会产生一个类型化的常量。
|
|
转换常量到一个类型形参会产生一个该类型的 非常量化 值, 这个值会以该类型形参实例化时所带类似实参的值来表示。 比如,给定函数:
|
|
转换 P(1.1)
产生一个类型为 P
的非常量化值,且值 1.1
是以 float32
或 float64
表示的,取决于 f
的类型实参。 因此,如果 f
用 float32
类型来实例化,那么表达式 P(1.1) + 1.2
的值会用与非常量化的 float32
加法相同的精度来计算。
非常量值 x
在以下这些情况下可以被转换为类型 T
:
x
可分配 给T
。- 忽略结构体标签(见下文),
x
的类型和T
不是 类型形参 但有 一致的 潜在类型 。 - 忽略结构体标签(见下文),
x
的类型和T
都不是 命名类型 的指针类型, 并且它们的基础类型不是类型形参但有一致的潜在类型。 x
的类型和T
都是整数或者浮点数类型。x
的类型和T
都是复数类型。x
是一个整数或者一个字节/ rune 分片,并且T
是字符串类型。x
是一个字符串并且T
是一个字节/ rune 分片。x
是一个分片,T
是一个到数组的指针,且该分片与数组的类型有 一致的 的元素类型。
此外,如果 T
或 x
的类型 V
是类型形参, 那么如果以下条件之一满足, x
也可以被转换为类型 T
:
V
和T
都是类型形参且V
类型集中的每个类型的值都可以被转换为T
类型集中的每个类型。- 只有
V
是类型形参且V
类型集中的每个类型的值都可以被转换为T
。 - 只有
T
是类型形参且x
可以被转换为T
类型集中的每个类型。
在为了转换的目的而比较结构体类型是否一致时, 结构体的标签 是被忽略的:
|
|
数字类型之间或者数字类型和字符串类型之间的(非常量)转换有特殊的规则。这些转换可能改变 x
的表现方式并产生运行时成本. 所有其它的转换仅改变其类型而不会改变 x
的表现形式。
没有语言机制可以在指针和整数间做转换。在一些受限制的情况下,包 unsafe 实现了这个功能。
数字类型间的转换
以下的规则应用于非常量数值间的转换:
- 当在 整数类型 间做转换时,如果值是一个带符号整数,那么它会用符号位扩展到隐式的无限精度; 否则它会用零扩展。然后它会截断以满足结果类型的大小。比如,如果
v := unit16(0x10F0)
,那么uint32(int8(v)) == 0xFFFFFFF0
。这种转换总是会产生一个有效的值;也不会有溢出指示。 - 当转换 浮点数类型 到整数时,小数部分会被丢弃(截断到零)。
- 当转换整数或者浮点数到浮点数类型,或者 复数 到其它复数类型时,结果值会约到目标类型所规定的精度。 比如,
float32
类型变量x
的值可能使用超过 IEEE-754 32 位数的精度保存着,但是float32(x)
表示的是把x
的值约到 32 位精度的结果。类似的,x + 0.1
可能使用了超过 32 位精度,但是float32(x + 0.1)
则不然。
在所有涉及浮点数或复数的非常量转换中,如果结果类型不能表示转换后的值,转换依旧是成功的,但结果值依赖实现。
从/到字符串的转换
转换带/不带符号的整数值到字符串类型会产生包含该数 UTF-8 表示形式的字符串。超过有效 Unicode 代码点范围的值会被转换为
\\uFFFD
。1 2 3 4 5
string('a') // "a" string(-1) // "\ufffd" == "\xef\xbf\xbd" string(0xf8) // "\u00f8" == "ø" == "\xc3\xb8" type MyString string MyString(0x65e5) // "\u65e5" == "日" == "\xe6\x97\xa5"
转换字节分片到字符串类型会产生一个以该分片的元素作为连续字节的字符串。
1 2 3 4 5 6
string([]byte{'h', 'e', 'l', 'l', '\xc3', '\xb8'}) // "hellø" string([]byte{}) // "" string([]byte(nil)) // "" type MyBytes []byte string(MyBytes{'h', 'e', 'l', 'l', '\xc3', '\xb8'}) // "hellø"
转换 rune 分片到字符串会产生一个把独立的 rune 值转换为 string 后再级联的字符串。
1 2 3 4 5 6
string([]rune{0x767d, 0x9d6c, 0x7fd4}) // "\u767d\u9d6c\u7fd4" == "白鵬翔" string([]rune{}) // "" string([]rune(nil)) // "" type MyRunes []rune string(MyRunes{0x767d, 0x9d6c, 0x7fd4}) // "\u767d\u9d6c\u7fd4" == "白鵬翔"
转换字符串类型的值到字节类型的分片会产生一个以该字符串的字节作为连续元素的分片。
1 2 3 4
[]byte("hellø") // []byte{'h', 'e', 'l', 'l', '\xc3', '\xb8'} []byte("") // []byte{} MyBytes("hellø") // []byte{'h', 'e', 'l', 'l', '\xc3', '\xb8'}
转换字符串类型到 rune 类型分片会产生一个包含该字符串独立 Unicode 代码点的分片。
1 2 3 4
[]rune(MyString("白鵬翔")) // []rune{0x767d, 0x9d6c, 0x7fd4} []rune("") // []rune{} MyRunes("白鵬翔") // []rune{0x767d, 0x9d6c, 0x7fd4}
从分片到数组指针的转换
转换一个分片到一个数组指针会产生一个到该分片底层数组的指针。当分片的 长度 小于数组的长度时,会出现 run-time panic 。
|
|
常量表达式
常量表达式仅包含 常量 操作数,且是在编译的时候进行计算的。
在可以合法使用布尔、数字或字符串类型操作数的地方,分别都可以使用非类型化的布尔、数字和字符串常量作为操作数。
常量 比较 总是会产生一个非类型化的布尔常量。如果常量 位移表达式 的左侧操作数是一个非类型化的常量,那么其结果是一个整数常量;否则就是和左侧操作数同一类型的常量(必须是 整数类型 )。
任何其它在非类型化的常量上的操作结果都是同一个类别的非类型化的常量;也就是:布尔、整数、浮点数、复数或者字符串常量。如果一个二元运算(非位移)的非类型化的操作数是不同类的,那么其结果是在如下列表中靠后显示的操作数的类:整数、 rune、浮点数、复数。举例:非类型化的整数常量除以非类型化的复数常量会产生一个非类型化的复数常量。
|
|
把内置函数 complex 应用到非类型化的整数、 rune 或者浮点数常量会产生一个非类型化的复数常量。
|
|
常量表达式总是会被精确地求值;中间值和常量本身可能会需求比任何在语言中预定义的类型所支持的更大的精度。以下都是合法的声明:
|
|
常量除法或取余操作的除数一定不能是零:
|
|
类型化的 的常量的值必须是能被该常量类型所精确得 表示的 。以下常量表达式是非法的:
|
|
用于一元按位补码运算符 ^
的掩码符合用于非常量的规则:对于无符号常量来说所有位都是 1,而对于带符号且非类型化的常量来说,则是一个整的 -1。
|
|
实现限制:编译器可能会在计算非类型化的浮点数或者复数常量表达式时凑整;请参阅 常量 一节中的实现限制。这种凑整可能导致在整数上下文中该浮点数常量表达式无效,即使它在使用无限精度计算时将是整数,反之亦然。
求值顺序
在包的级别上, 初始化依赖关系 确定了 变量声明 中独立的初始化表达式的求值顺序。除此之外,当对表达式/赋值/ return 语句 的 操作数 进行求值时,所有的函数调用、方法调用和通信操作都是以词法的从左至右的顺序被求值的。
比如,在(函数局部)赋值
|
|
中,函数调用和通信是按照 f()
, h()
, i()
, <-c
, g()
和 k()
的顺序发生的。不过,以上这些事件相对于 x
的求值和索引,以及 y
的求值的顺序却是没有规定的。
|
|
Note
如上,尽管我怎么尝试都是 a++
在前,但这里要注意绝对不能依赖这个极大概率的顺序来编码。
在包的级别上,对独立的初始化表达式来说,初始化依赖关系会覆盖掉其原本从左至右的求值规则,但不会针对在每个表达式中的操作数:
|
|
函数调用是按照 u()
, sqr()
, v()
, f()
, v()
和 g()
的顺序发生的。
在单一表达式中的浮点数操作是根据运算符的结合性来求值的。明确的括号会通过覆盖默认的结合性来影响求值。在表达式 x + (y + z)
中,加法 y + z
在加 x
前被执行。
语句
语句控制着执行。
|
|
终止语句
终止语句 中断了一个 块 中常规控制流。以下语句是终结的:
对内置函数 panic 的调用。
语句列表以终止语句结束的 块 。
满足如下条件的
“if”
语句:
- “else” 分支存在,并且
- 两个分支都是终止语句。
满足如下的
“for”
语句:
- 没有针对这个 “for” 语句的 “break” 语句,并且
- 循环条件为空,并且
- 这个 “for” 语句没有使用 “range” 子句。
满足如下的
“switch”
语句:
- 没有针对这个 “switch” 语句的 “break” 语句,
- 有一个 default case,并且
- 在每个 case 中(包括默认的)的语句列表以终止语句或者一个可能带标签的 “fallthrough” 的语句结束。
满足如下的
“select”
语句:
- 没有针对这个 “select” 语句的 “break” 语句,并且
- 在每个 case 中(包括默认的)的语句列表是存在的并以终止语句结束。
标记终止语句的 标签语句 。
所有其它语句都不是终止的。
如果语句列表非空且其最后的非空语句是终止的,那么这个 语句列表 以终止语句结束。
:ruby:空语句|Empty statements
空语句什么都不做。
|
|
:ruby:标签语句|Labeled statements
标签语句可以是 goto
, break
或 continue
语句的目标。
|
|
:ruby:表达式语句|Expression statements
除了特定的内置函数外,函数/方法 调用 以及 接收操作 可以作为语句上下文出现。这种语句能够被括起来。
|
|
下述内置函数不允许出现在语句上下文中:
|
|
:ruby:发送语句|Send statements
发送语句在信道上发送一个值。 信道表达式的 核心类型 必须是 信道类型 ,信道方向必须允许发送操作, 并且,发送值的类型必须 可分配 为信道的元素类型。
|
|
信道和值表达式都会在通信开始前被求值。直到发送进行前,通信都是阻塞的。在无缓冲的信道上的发送操作只有在接收端准备好后才可以进行。在带缓冲的信道上的发送操作只要缓冲区还有空间就可以进行。在关闭的信道上进行发送会产生一个 run-time panic 。在值为 nil
的信道上的发送是会永久阻塞的。
|
|
:ruby:自增/减语句|IncDec statements
"++"
和 "--"
语句用非类型化的 常量 1 来增加或减少其操作数。和赋值一样,这个操作数必须是 可被寻址的 或者是一个映射索引表达式。
|
|
以下 赋值 语句在语义上是等同的:
|
|
:ruby:赋值语句|Assignments
|
|
左侧的每个操作数必须是 可被寻址的 ,或一个映射索引表达式,或一个(仅对 = 赋值) 空白标识符 。操作数可以被括起来。
|
|
当 op 是一个二元 算数运算符 时, 赋值操作 x op= y
等同于 x = x op (y)
,不过 x
仅求值一次。 op= 结构是一个单独的 token. 在此赋值操作中,无论是左侧亦或是右侧的表达式列表,都必须仅包含一个确切的单一值表达式,并且左侧的表达式不能为空白标识符。
|
|
多元赋值方式分配多值运算得到的每个值到一个变量列表。这里有两种形式。第一种,右侧的操作数是譬如函数调用、 信道 、 映射 运算 、 类型断言 这样的单个多值表达式。左侧的操作数的个数必须和值的个数匹配。比如,如果 f
是一个返回两个值的函数,
|
|
分配第一个值给 x
,第二个值给 y
。第二种形式,左侧操作数的个数必须等于右侧表达式的个数,每个表达式必须是单一的值,并且右侧第 n 个表达式会分配给左侧第 n 个操作数:
|
|
在此赋值中, 空白标识符 提供了一个忽略右侧值的方法:
|
|
赋值会分两个阶段进行。第一阶段,左侧的 索引表达式 和 指针间接 (包括在 选择器 中隐式的指针间接)以及右侧的表达式都会按照 通常的顺序 来求值。第二阶段,赋值按从左至右的顺序进行。
|
|
在赋值中,每个值都必须是 可分配 给需要分配的操作数的类型的,不过会有以下特殊情况:
- 任何类型的值都可以被分配给空白标识符。
- 当非类型化的常量被分配给一个接口类型变量或是空白标识符时,常量会先被隐式地 转换 为它的 默认类型 。
- 当非类型化的布尔值被分配给一个接口类型变量或是空白标识符时,它会先被隐式地转换为布尔类型。
If 语句
“if” 语句根据布尔表达式的值来有条件地指定两个分支的执行。当表达式求值得真时, “if” 分支被执行,否则执行 “else” 分支(存在的话)。
|
|
表达式可能会前缀一个简单的语句,这个语句会在表达式被求值之前执行。
|
|
Switch 语句
“switch” 语句提供了多路执行。表达式或者类型会和在 “switch” 内的 “case” 做比较去确定执行哪一个分支。
|
|
有两种形式:表达式开关(switch)和类型开关。在表达式开关中, case 包含了要与 switch 表达式的值作比较的表达式。在类型开关中, case 包含了要与特别说明的 switch 表达式的类型作比较的类型。 switch 表达式在一个开关语句中仅求值一次。
:ruby:表达式开关|Expression switches
在表达式开关中, switch 表达式先求值完毕, case 表达式(不一定是常量)则按照从左至右、从上之下的顺序进行求值;第一个和 switch 表达式相等的 case 中对应的语句会被触发执行;其它 case 则会被跳过。如果没有 case 匹配且有一个 “default” case,那么会执行这个 case 的语句。最多只能有一个 default case ,它可以出现在 “switch” 语句的任意位置。当 switch 表达式不存在时,其相当于有一个布尔值 true
。
|
|
如果 switch 表达式求值为一个非类型化的常量,它会先被隐式地 转换 为它的 默认类型 。预定义的非类型化的值 nil
不能用在 switch 表达式中。开关表达式类型必须是 可比较的 。
如果 case 表达式是非类型化的,那么它会先被隐式地 转换 为 switch 表达式的类型。对于每个(可能是转换过的) case 表达式 x
和 switch 表达式的值 t
, 比较 x == t
必定是有效的。
也就是说, switch 表达式就像是被用来声明和初始化一个没有明确类型的临时变量 t
;为了测试相等性,这个临时变量 t
的值会和每一个 case 表达式 x
做判断。
在一个 case 或 default 子句中,最后的非空语句可能是一个(或许带 标签 的) “fallthrough” 语句用来指示控制应从本子句流出以流入下个子句的第一个语句。不然的话控制会流到 “switch” 语句的末尾。 “fallthrough” 语句可以作为除了表达式开关的最后一个子句外的其它所有子句的最后一条语句出现。
switch 表达式可以前缀一个简单的语句,这个语句会在表达式之前被求值。
|
|
实现限制:编译器可能会不允许多个 case 表达式求值结果为相同的常量。例如,现在的编译器不允许重复的整数、浮点数或字符串常量出现在 case 表达式中。
:ruby:类型开关|Type switches
类型开关用于比较类型而不是值。其它方面和表达式开关类似。它的标识是一个特殊的 switch 表达式,这个表达式形式是一个使用了关键字 type
而不是一个实际类型的 类型断言 。
|
|
然后 case 将实际的类型 T
与表达式 x
的动态类型进行匹配。与类型断言一样, x
必须是 接口类型 ,但不能是类型形参,并且在 case 中的每一个非接口类型 T
必须实现 x
的类型。在类型开关中, case 的类型必须都是 不同的 。
|
|
TypeSwitchGuard 可能会包含一个 短变量声明 。当这种形式被使用后,对于每一个子句,该变量都会在 TypeSwitchCase 末尾的隐式 块 中被声明。且,在仅列出一个类型的 case 子句中,该变量类型就是此列出的类型;否则,该变量类型为 TypeSwitchGuard 中表达式的类型。
除了类型外, case 子句也可以使用预声明的标识符 nil ;当 TypeSwitchGuard 中的表达式为一个 nil
接口值时,这个 case 会被选中。最多只可以有一个 nil
case。
给定一个 interface{}
类型的表达式 x
,以下类型开关:
|
|
可以被重写为:
|
|
类型形参 或 泛型 可以作为类型在 case 中使用。 如果在 实例化 上该类型在 switch 中产生了另一个重复的条目, 那么第一个匹配的 case 会被选中。
|
|
TypeSwitchGuard 可以前缀一个简单的语句,这个语句在 guard 之前被求值。
“fallthrough” 语句在类型开关中是不被允许的。
For 语句
“for” 语句规定了一个块的重复执行。有三种形式:迭代可以被一个单一条件、一个 “for” 子句或是一个 “range” 子句控制。
|
|
带单一条件的 for 语句
在它最简单的形式中, “for” 语句只要一个求值为真的布尔条件来指定一个块的重复执行。这个条件的值会在每次迭代前都被求一下。空条件相当于布尔值 true
。
|
|
带 for 子句的 for 语句
带 ForClause 的 “for” 语句也是通过其条件来控制的,但是它会额外指定一个 init 和 post 语句,比如一个赋值、增量或减量语句。 Init 语句可以是一个 短变量声明 ,但 post 语句一定不是。通过 init 语句声明的变量会在每次迭代时被重复使用。
|
|
Init 语句如果非空, 则它会在首次迭代的条件求值前被执行一次; post 语句会在每次块执行完后被执行(并且只在块执行过后)。 ForClause 每个元素都可以是空的,但是 分号 是必须要有的,除非仅存在一个条件元素。如果条件也省略了,那么就相当于是布尔值 true
。
|
|
带 range 子句的 for 语句
带 “range” 子句的 “for” 语句会彻底地迭代数组的、分片的、字符串的或映射的所有条目,或是从信道接收到的值。针对每一个条目,它在分配 迭代值 给对应的存在的 迭代变量 后再执行语句块。
|
|
“range” 子句中右侧的表达式被称为 范围表达式 , 它的核心类型必须是数组、到数组的指针、分片、字符串、映射或是允许 接收操作 的信道。 和赋值一样,如果左侧操作数存在,那么它一定是 可被寻址的 或映射索引表达式;它们表示迭代变量。 如果范围表达式是一个信道,那么最多允许一个迭代变量,其它情况下可以最多到两个。 如果最后的迭代变量是 空白标识符 ,那么这个 range 子句和没有此空白标识符的子句是等同的。
范围表达式 x
会在开始此循环前被求值一次,但有一个例外:当存在最多一个迭代变量且 len(x)
是 常量 时,范围表达式不被求值。
左侧的函数调用在每次迭代时被求值。对于每个迭代,如果迭代变量存在,那么对应的迭代值是按以下说明产生的:
|
|
对于数组、到数组的指针或是分片值
a
,其索引迭代值是从索引 0 开始,以递增次序产生的。如果存在最多一个迭代变量, range 循环会创建从0
到len(a) - 1
的迭代值,且不会索引进数组或分片内。对于nil
分片而言,迭代数是 0。对于字符串值, “range” 子句从字节索引 0 开始迭代字符串中的 Unicode 代码点。在连续的迭代上,索引值是字符串中连续 UTF-8 编码的代码点的第一个字节的索引,而第二个值(类型是
rune
)是对应的代码点的值。如果迭代遇到了无效的 UTF-8 序列,那么第二个值会变成 Unicode 替换字符0xFFFD
,且下一个迭代将在字符串中前进一个字节。Note
译注,如下这段代码有助于理解
1 2 3 4 5 6 7 8 9 10 11 12
for i, c := range "\x61\xF0\x62\x63\xe4\xb8\xad\xe6\x64\x65" { fmt.Printf("%d -> %c [%#x, %U]\n", i, c, string(c), c) } //output: // 0 -> a [0x61, U+0061] // 1 -> � [0xefbfbd, U+FFFD] // 2 -> b [0x62, U+0062] // 3 -> c [0x63, U+0063] // 4 -> 中 [0xe4b8ad, U+4E2D] // 7 -> � [0xefbfbd, U+FFFD] // 8 -> d [0x64, U+0064] // 9 -> e [0x65, U+0065]
映射的迭代顺序是未指定的,并且不能保证两次完整的迭代是相同的。如果在迭代中某个未接触到的映射条目被移除了,那么对应的迭代值就不会产生。如果在迭代中新创建了一个映射条目,那这个条目可能会在迭代中被产生也可能被跳过。对于每个条目的创建或是一个迭代到下一个迭代,选择可能很多样。如果映射是
nil
,迭代数为 0。对于信道,产生的迭代值是在信道 关闭 前信道上发送的连续值。如果信道是
nil
,那么范围表达式会永久阻塞。
迭代值会像 赋值语句 一样被赋值给对应的迭代变量。
迭代变量可以被 “range” 子句使用 短变量声明 (:=)的形式声明。这种情况下,它们的类型会被设置为对应迭代值的类型,且它们的 作用域 是 “for” 语句块;这些变量会在每次迭代时复用。如果迭代变量是在 “for” 语句外被声明的,那么在执行完毕后,它们的值会是最后一次迭代的值。
|
|
Go 语句
“go” 语句会在同一地址空间执行一个函数调用作为一单独的并行控制流程( goroutine )。
|
|
表达式必须是函数或方法调用;它不能是括起来的。对内置函数的调用会有和 表达式语句 一样的限制。
在调用的 goroutine 中的函数值和参数是按 通常的情况来求值 的,但不同于普通调用的是,程序执行不会等待被调用的函数执行完毕。相反,在新的 goroutine 中的函数是独立执行的。当函数终止,其 goroutine 也会终止。如果函数存在任何返回值,这些值会在函数完成时被丢弃。
|
|
Select 语句
“select” 语句会选择一组或是 发送 或是 接收 的操作来进行。它看起来和 “switch” 语句类似,但它所有的 case 只涉及通信操作。
|
|
带 RecvStmt 的 case 可能会分配 RecvExpr 的结果到一个或两个变量,变量是用 短变量声明 声明的。 RecvExpr 一定是一个(可能带括号的)接收操作。最多可以有一个 default case ,它可以出现在 case 列表的任意位置。
“select” 语句的执行按如下几个步骤进行:
对于语句中的所有 case 来说,其接收操作的信道操作数及信道以及发送语句右侧的表达式会在进入 “select” 语句时以源码的顺序被执行仅一次。结果是一组需要接收或发送的信道,以及对应的需要发送的值。无论选择哪个(如果有)通信操作进行,在这个求值中的任何副作用都会发生。 RecvStmt 左侧的带短变量声明或赋值的表达式还不会被求值。
Note
译注,如下代码可执行后参考以助于理解
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
var a [2]int var ca [2]chan int var c0 chan int = make(chan int, 1) var c1 chan int = make(chan int, 1) ca[0] = make(chan int, 1) ca[1] = make(chan int, 1) c0 <- 10 c1 <- 99 ca[1] <- 1 select { case a[<-ca[1]] = <-c1: fmt.Println(a[1]) case ca[0]<- <-c0: fmt.Println(<-ca[0]) default: fmt.Println("default") } fmt.Println(<-c1) fmt.Println(<-ca[1]) //fmt.Println(<-ca[0])
如果可以发生一个或多个通信,通过统一的伪随机选择来确定一个来进行。否则,如果有一个默认的 case,那么这个默认 case 会被选择。如果没有默认的 case,那么这个 “select” 语句会阻塞,直到至少发生了一个通信。
除非被选择的 case 是默认的 case,否则各自的通信操作会被执行。
如果被选择的 case 是一个带短变量声明或赋值的 RecvStmt,那么左侧的表达式会被求值且接收到的值会被分配。
被选择的 case 的语句列表被执行。
由于在 nil
信道上的通信永不会进行,所以只带 nil
信道且没有默认 case 的 select 会永久阻塞。
|
|
Return 语句
函数 F
中的 “return” 语句会终止 F
的执行,并可选择地提供一个或更多的返回值。任何被 F
推迟 的函数会在 F
返回到它调用者前被执行。
|
|
在没有结果类型的函数中, “return” 语句一定不指定任何返回值。
|
|
有三种从带结果类型的函数内返回值的方法:
返回值会明确地列在 “return” 语句中。每个表达式一定是单一值的且是 可分配 给对应的函数返回类型的元素。
1 2 3 4 5 6 7
func simpleF() int { return 2 } func complexF1() (re float64, im float64) { return -7.0, -4.0 }
在 “return” 语句中的表达式列表可以是对多值函数的单一调用。效果就犹如从这个函数返回的值被分配给带对应值类型的一个临时变量,然后这些变量会跟随在 “return” 语句后,并适用上述情况指明的规则。
1 2 3
func complexF2() (re float64, im float64) { return complexF1() }
如果函数结果值对其 结果参数 规定了名字,那么表达式列表可以为空。结果参数会作为本地变量,函数也可以在需要时给它们赋值。 “return” 语句会返回这些变量的值。
1 2 3 4 5 6 7 8 9 10
func complexF3() (re float64, im float64) { re = 7.0 im = 4.0 return } func (devnull) Write(p []byte) (n int, _ error) { n = len(p) return }
不管它们是如何声明的,在进入函数时,所有结果值都会被初始化为其类型的 零值 。指定结果的 “return” 语句会在任何推迟函数执行前设置结果参数。
实现限制:当一个和结果参数同名的实体(常量、类型或变量)在 return 位置的 作用域 内时,编译器会不允许空的表达式列表出现在 “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” 语句执行时,针对调用的函数值和参数是按 通常的情况来求值 并重新保存的,但实际的函数是不调用的。相反,被推迟的函数会在其环绕函数返回前,按照被推迟的反序被瞬间调用。也就是说,如果围绕函数通过一个明确的 return 语句 返回的话,那么被推迟的函数会在所有被 return 语句所设置的结果参数 后 ,在函数返回到其调用者 前 被执行。如果被推迟函数求值得 nil
,那么执行会在该被推迟的函数被调用时(而不是在 “defer” 语句被执行时) 恐慌 。
例如,如果被推迟的函数是一个 函数字面值 并且其环绕函数有在该字面值作用域内的 命名的结果参数 ,那么该被推迟的函数可以在这些结果参数被返回前访问并修改它们。如果被推迟的函数有任何返回值,这些值会在函数完成时被丢弃。(也看一下 处理恐慌 一节)
|
|
内置函数
内置函数是 预先声明 的。它们和其它任何函数一样调用,但是其中有一些接受类型而不是表达式作为其第一个实参。
内置函数没有标准的 Go 类型,所以它们只能出现在 调用 表达式中;它们不能作为函数值来使用。
Close
对于 核心类型 为 信道类型 的实参 ch
,内置函数 close(c)
标明了将不会再有值被发送到这个信道。如果 ch
是一个仅可接收的信道,那么(关闭它)是一个错误。发送到或者关闭一个已经关闭的信道会发生 run-time panic 。 关闭 nil
信道也会发生 run-time panic 。调用 close
后,以及任何之前被发送的值都被接收后,接收操作不会阻塞而将是会返回对应信道类型的零值。多值 接收操作 会返回一个接收到的值,随同一个信道是否已经被关闭的指示符。
Note
译注,这里返回的指示符标识的其实是信道是否还有值,即 true
or false
长度和容量
内置函数 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
会被求值。
|
|
分配
内置函数 new
获取类型 T
,在运行时为此类型的 变量 分配存储空间,并返回一个 指向 它的类型为 *T
的值。这个变量会按照 初始化值 一节所描述的来初始化。
|
|
例如:
|
|
为 S
类型变量分配存储空间,初始化它( a=0, b=0.0
),然后返回含有位置地址的类型为 *S
的一个值。
制作分片、映射和信道
内置函数 make
获取类型 T
,可选择性地接一个类型相关的表达式列表。 T
的 核心类型 必须是分片、映射或者信道类型。 它会返回类型为 T
的值(不是 *T
)。存储内容会按照 初始化值 一节所描述的来初始化。
|
|
每个 大小实参 n
和 m
,必须为 整数类型 ,或有一个仅包含整数类型的 类型集 ,或是一个非类型化的 常量 。常量大小实参必须是非负的且可被 int
类型值 所表示的 ;如果它是个非类型化的常量,那么会被给定类型 int
。如果 n
和 m
都提供了且为常量,那么 n
一定不能大于 m
。如果在运行时 n
为负值或者大于了 m
,那么会发生 run-time panic 。
|
|
带映射类型和大小提示 n
来调用 make
会创建一个带持有 n
个映射元素初始化空间的映射。其精度表现是依赖于具体实现的。
Note
译注,关于对映射的取值,详见 索引表达式 一节。
添加到和拷贝分片
内置函数 append
和 copy
会协助常见的切片操作。对于这两个函数,其结果和实参的内存引用是否交叠无关。
variadic 函数 append
附加零个或多个值 x
到分片 s
,并返回带有与 s
相同类型的结果分片。 s
的 核心类型 必须是 []E
类型的分片。 值 x
被传递给类型为 ...E
的形参,并应用对应的 参数传递规则 。 作为一种特殊的情况,如果 s
的核心类型是 []byte
,那么 append
也接受一个核心类型为 string
且其后紧跟着一个 ...
的字符串类型的作为第二个实参。 这种形式会添加字符串内的字节。
|
|
如果 s
的容量不足以满足额外的值,那么 append
会分配一个新的足够大的底层数组来同时满足已经存在的分片元素和那些额外的值。否则, append
复用原来的底层数组。
|
|
函数 copy
从源 src
拷贝分片元素到目的 dst
并返回拷贝的元素个数。 两个实参的 核心类型 都必须是带 一致的 元素类型的分片。 拷贝的元素数量是 len(src)
和 len(dst)
中的最小值。 作为一个特殊情况,如果目标的核心类型是 []byte
,那么 copy
也接受带核心类型 string
的源实参。这种形式会从字符串中拷贝字节到字节分片中。
|
|
例子:
|
|
映射元素的删除
内置函数 delete
会根据键 k
从 映射 m
中删除元素。 k
的值必须是 可分配 给 m
的键类型的。
|
|
如果 m
的类型是 类型形参 ,那么所有该类型集中的类型都必须是映射,且它们必须有一致的键类型。
如果映射 m
是 nil
或元素 m[k]
不存在,那么 delete
是一个空操作。
操纵复数
有三个函数用来聚合和分解复数。内置函数 complex
用浮点的实和虚部来构造一个复值,而 real
和 imag
从一个复值中提取其实部和虚部。
|
|
实参的类型和返回值相对应。 对于 complex
,两个实参必须是相同的 浮点数类型 且返回类型是带对应浮点成分的 复数类型 , complex64
对应 float32
实参, complex128
对应 float64
实参。如果有一个实参求值为一个非类型化的常量,那么它会先被隐式地 转换 为另一个实参类型。 如果两个实参都求值为非类型化的常量,那么它们必须是非复数或者它们的虚部一定为零, 然后函数的返回值也是一个非类型化的复数常量。
对于 real
和 imag
,实参必须是复数类型,返回值是对应的浮点类型: float32
对应 complex64
实参, float64
对应 complex128
实参。如果实参求值为一个非类型化的常量,那么它必须是一个数,然后函数的返回类型是一个非类型化的浮点常量。
real
和 imag
函数一起组成了 complex
的反相,所以对于一个复数类型为 Z
的值 z
来说, z == Z(complex(real(z), imag(z)))
。
如果这些函数的操作数都是常量,那么返回值也是一个常量。
|
|
类型形参类型的实参是不被允许的。
处理恐慌
有两个内置函数, panic
和 recover
,协助报告和处理 run-time panic 和程序定义的错误状态。
|
|
当执行函数 F
时,对 panic
的明确调用或 run-time panic 会终止 F
的执行。任何被 F
推迟 的函数会照常执行。然后,任何被 F
的调用者所推迟的函数会运行,以此类推直到被在执行中 goroutine 中的顶层函数所推迟的。在这个阶段,程序会终止并且错误状态会被报告,这个错误状态包括了给 panic
的实参的值。这个终止过程被称为 panicking 。
|
|
recover
函数允许程序管理一个 panicking goroutine 的行为。假设函数 G
推迟了调用 recover
的函数 D
,且恐慌发生在了和 G
执行的同一个 goroutine 的函数中。当运行中的被推迟的函数到达了 D
时, D
对 recover
调用的返回值是传递给 panic
调用的值。如果 D
正常返回而没有开始一个新的 panic
,那么 panicking 序列会停止。在这种情况中,在 G
和 panic
调用之间的函数状态会被丢弃,然后恢复正常的执行。接着会运行被 G
推迟的在 D
前的函数,然后 G
通过返回到它的调用者来终止执行。
如果以下任何条件成立,那么 recover
的返回值为 nil
:
panic
的实参是nil
;- goroutine 没有 panicking;
recover
没有被一个延迟函数直接调用。
在以下例子中的 protect
函数调用了函数实参 g
并使调用者免受 g
中发生的 run-time panic 之害。
|
|
Bootstrapping
目前的实现提供了一些在 bootstrapping 时有用的内置函数。这些函数已经被记录完整了但是不能保证会一直存在在语言中。它们不会返回一个结果。
|
|
实现限制: print
和 println
不一定要可以接受任意的实参类型,但是布尔、数字和字符串 类型 的打印一定要支持。
包
Go 程序是通过连结 包 来构建的。反过来,包由一个或多个源文件构成,这些源文件一起声明属于包的常量、类型、变量和函数,并且可以在同一包的所有文件中访问。这些元素可能被 暴露 并在其它包中使用。
源文件组织
每个源文件都是由以下组成的:定义其所属包的包子句,一组可能为空的用于声明其想要使用内容的包的导入声明,一组可能为空的函数、类型、变量和常量声明。
|
|
:ruby:包子句|Package clause
每个源文件由包子句开始,其定义了文件所属的包。
|
|
PackageName 一定不能是 空白标识符 。
|
|
共享同一包名的一组文件形成了一个包的实现。实现可能要求一个包的源文件都在同一文件夹下。
:ruby:导入声明|Import declarations
导入声明 陈述了 这个包含声明的源文件 依赖 被导入的 包的功能( 程序初始化和执行 )并启用了对该包被 暴露 的标识符的访问。此导入会命名一个标识符(包名)用于被访问,以及一个表示被导入包的导入路径。
|
|
PackageName 是用在 限定标识符 中来访问导入源文件中的包的暴露标识符的。它是在文件 块 中被声明的。如果 PackageName 缺失,那它默认为被导入包的 包子句 中指定的标识符。如果明确的句号( .
)取代名字出现了,那么所有在那个包的包 块 中声明的包的暴露标识符将在这个导入包的源文件的文件块中被声明,并且必须不带限定符来访问。
导入路径的解释是依赖于实现的,但它通常是被编译包完整文件名的子字符串,并可能是相对于已安装包的库的。
实现限制:编译器可能会限制导入路径仅使用属于 Unicode 的 L, M, N, P 和 S 主类的这些非空字符串(无空格的可见字符),并也可能去除了字符 !"#$%&'()*,:;<=>?[\\]^\
{|}` 和 Unicode 替换字符 U+FFFD 。
假定我们已经编译了一个包含包子句 package math
的包,它暴露了函数 Sin
,并将编译好的包安装在由 "lib/math"
标记的文件。此表格说明了 Sin
是如何在在各种导入声明后导入包的文件中被访问的。
|
|
导入声明声明了导入者和被导入包的依赖关系。在包中直接/间接导入它自己是非法的,直接导入一个没有引用任何其暴露标识符的包也是非法的。仅仅为了包的副作用(初始化)来导入一个包的话,使用 空白 标识符作为明确的包名:
|
|
一个示例包
这里有一个实现并发质数筛选的完整 Go 包。
|
|
Note
译注,简单说明这个质数发生器,质数是在大于 1 的自然数中,除了 1 和它自身无法被其他自然数整除的数,这个菊花链简直就是把这条定义发挥到了极致。
主要看这个 for 循环,它创建了无数条 filter gorountine:
- 第一条是以 3 作为起始数, 2 为除数,求得的第一个不被 2 整除的数发送给
ch1
,因为一开始数小,所以就这样子的过滤条件足以求得第一个数必为质数; - 接下来,输出以此条过滤器求得的第一个数即质数,并将其作为下一条过滤器的除数,再以此条过滤器获得的下一个不被 2 (即上一条过滤器的除数)整除的数作为下一条过滤器的起始数,开始下一条过滤器;
- 这样,就可以保证接下来的每一条过滤器的起始数已经经过上一条过滤器过滤过,每条过滤器的除数固定,就这样层层过滤,就可以过滤出除了 1 和其本身不会被其他自然数整除的所有数了,即可以无穷尽地执行下去。
程序初始化和执行
零值
当存储空间被分配给一个 变量 (无论是通过一个声明、对 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
函数不能在程序中的任何位置被引用。
不带导入声明的包是这样初始化的:按照出现在源码中的顺序分配初始化值到它所有的 包级 变量,再调用:code:init 函数(可能会在多个文件中,那就按照提交到编译器的顺序)。如果包有导入声明,那么在初始化包本身之前,被导入的包会先初始化好。如果多个包导入了一个包,那么被导入的包只会初始化一次。通过构造可以保证包的导入不存在循环初始化依赖关系。
包的初始化(变量初始化和对 init
函数的调用)在单一 goroutine 内,循序的,每次一个包地发生。 init
函数可以发起其它与初始化代码并行运行的 goroutine。不过,初始化过程总是会序列化 init
函数:在上一个没有返回前不会调用下一个。
为了确保可重现的初始化行为,建议构建系统以词法文件名顺序将属于同一个包的多个文件呈现给编译器。
程序执行
一个完整的程序是通过按轨迹地连接一个单一的,未导入的被叫做 main package 的包与其它所有其导入的包来创建的。主包的包名一定是 main
,并且声明一个无实参也无返回值的 main
函数。
|
|
程序通过先初始化主包再调用 main
函数来开始执行。当这个函数调用返回时,程序退出。并不会等待其它(非 main
) goroutine 完成。
错误
预先声明的类型 error
定义如下:
|
|
它是表示错误条件的常见接口, nil
值代表没有错误。例如,从文件读入数据的函数可能被定义为:
|
|
Run-time panics
像尝试超出数组边界的索引这样的执行错误会触发一个 run-time panic ,它等同于带由实现所定义的接口类型 runtime.Error
的值来对内置函数 panic 的调用,这个类型满足预先声明的接口类型 error 。表示不同运行时错误条件的确切错误值是未指定的。
|
|
系统注意事项
unsafe
包
编译器已知且可以通过 导入路径 "unsafe"
访问的内置包 unsafe
提供了包括违反类型系统操作在内的用于低级编程的功能集。使用 unsafe
的包必须手动审查以确保类型安全,且可能不具备可移植性。该包提供了以下接口:
|
|
Pointer
是一个 指针类型 但是 Pointer
值可能不能被 解引用 。任何指针或 潜在类型 uintptr
的值都可以被 转换 为潜在类型 Pointer
的类型,反之亦然。在 Pointer
和 uintptr
间的转换效果是由实现定义的。
|
|
函数 Alignof
和 Sizeof
获取任意类型的表达式 x
并分别返回假想变量 v
的定位或大小( v
就像是通过 var v = x
声明的)。
函数 Offsetof
获取一个(可能带括号的) 选择器 s.f
(这个选择器表示由 s
或 *s
所表示结构体的字段 f
),并返回相对于结构体地址的以字节表示的字段偏移量。如果 f
是一个 嵌入字段 ,那么必须可以在无需指针间接的情况下通过结构体字段访问。对于带字段 f
的结构体 s
:
|
|
Note
译注, go-1.15.3 测试,如果使用 new
内置函数对结构体进行初始化,那么会不符合上述对字段地址的描述。
计算机架构可能会要求内存地址是 对其的 ;也就是说,使变量的地址为一个因子的倍数,这个因子是变量类型的 :ruby:对准值|alignment
。函数 Alignof
获取一个表示任意类型变量的表达式,并以字节为单位返回变量(类型)的对准值。对于一个变量 x
:
|
|
如果 T
是 类型形参 ,或者它是一个包含可变大小字段或元素的数组或者结构体类型, 那么类型 T
(的变量)有 可变大小 。否则其大小是 常量 。 如果调用 Alignof
、 Offsetof
、 Sizeof
时的实参 (或对于 Offsetof
来说的选择器表达式 s.f
中的结构体 s
) 是常量大小的类型,那么这些调用是类型为 uintptr
的编译时 常量表达式 。
函数 Add
会加 len
到 ptr
并返回一个更新好的指针 unsafe.Pointer(uintptr(len) + uintptr(ptr))
。 len
实参必须为 整数类型 或者一个非类型化的 常量 。 常数 len
实参必须可以被一个 int
类型的值 所表示 ;如果它是非类型化的常量那么它会被给定类型 int
。 有效使用 Pointer
的规则仍然适用。
函数 Slice
返回了一个分片,该分片的底层数组起始于 ptr
且其长度和容量为 len
。 Slice(ptr, len)
等同于
|
|
除了这样外,还有一个特殊的情况,当 ptr
为 nil
且 len
为零时, Slice
返回 nil
。
len
实参必须为 整数类型 或者一个非类型化的 常量 。一个常数 len
实参必须是非负的且可以被一个 int
类型的值 所表示 ;如果它是非类型化的常量那么它会被给定类型 int
。在运行时,如果 len
为负,或 ptr
为 nil
但 len
不为 nil
,那么 run-time panic 会发生。
大小和对准值保证
对于 数字类型 ,以下大小是保证的:
|
|
以下最小对准值属性是保证的:
- 对于任意类型变量
x
:unsafe.Alignof(x)
最小为 1。 - 对于结构体类型变量
x
:unsafe.Alignof(x)
是所有unsafe.Alignof(x.f)
(对于x
的每个字段f
)中最大的值,但最小为1。 - 对于数组类型变量
x
:unsafe.Alignof(x)
和数组元素类型变量的对准值相同。
Note
译注,这边我一开始很纠结为什么 complex128 类型的对准值是 8 字节,后来发现 complex64 的对准值是 4 字节,所以大胆猜测它是拆开来算的
如果结构体或数组没有包含大于零大小的字段(或元素,对数组而言),那么它大小为零。两个不同的零大小的变量在内存中可能拥有同一个地址。