Go编程语言规范

2016年01月05日版本

简介
记号
源代码表示
字符
字母和数字
词法元素
注释
符号
分号
标识符
关键字
运算符和分隔符
整型字面量
浮点字面量
虚数字面量
文符字面量
字符串字面量
常量
变量
类型
方法集
布尔类型
数值类型
字符串类型
数组类型
分片类型
结构体类型
指针类型
函数类型
接口类型
映射类型
管道类型
类型和值的性质
类型一致
转换
声明和作用域
标号作用域
空白标识符
预声明标识符
导出的标识符
唯一标识符
常量声明
Iota
类型声明
变量声明
短变量声明
函数声明
方法声明
表达式
操作数
合法标识符
复合值
函数值
主表达式
Selectors
下标
分片
类型推断
调用
传递参数
操作符/运算符
运算符优先级
算术运算符
整数溢出
比较运算符
逻辑运算符
地址运算符
接收运算符
方法表达式
转换
常量表达式
值的计算顺序
语句
空语句
标号语句
表达式语句
发送语句
自加自减语句
赋值语句
if 语句
switch 语句
for 语句
go 语句
select 语句
return 语句
break 语句
continue 语句
goto 语句
fallthrough 语句
defer 语句
内置函数
关闭
长度和容量
分配
构造分片、映射和管道
追加和拷贝分片
映射元素删除
复数操作
问题处理
Bootstrapping
源文件的组织
Package clause
导入声明
一个例子
程序初始化和执行
0 值
程序执行
错误
运行时问题
系统考量
包不安全
尺寸大小和对齐保证

简介

这是Go语言的一个参考手册。如果想了解更多信息或是其他文档的话,可以去http://golang.org查看。

Go是一门立志于系统编程的通用语言。它是强类型,带有垃圾回收,并且内在支持并发编程。程序由组成,可以方便地管理它们之间的依赖。已有的实现采用了传统的编译/链接模型,最终生成可执行的二进制代码。

Go语言语法紧凑,非常有规则可循,可以很容易地被集成开发环境(IDE)等自动工具分析。

记号

后面的语法使用扩展的巴克斯-诺尔范式 (EBNF)进行描述:

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

产生式由一些术语和下面的几个按优先级从低到高的运算符组成:

|   任选其一
()  一个整体
[]  可选/可有可无 (0 或是 1次)
{}  重复多次 (0 到 n 次)

小写的产生式的名字通常用于表示一个词法符号;非终结符一般用驼峰式命名。词法符号我们使用双引号""或是反向单引号``引住。

a … b这种形式代表的是从ab可选的字符集合。省略号在本规范中也用于某些处的表示不完全枚举或是不再详细列出的代码分片。字符(不同于三个字符的...),它不是Go 语言的一个词法符号。

源代码表示

源文件是用UTF-8编码的Unicode文本。文本并不是规范化的,所以,一个加重音的代码点不同于重音再加一个字符,后面的认为是两个字符。为了简化,本文档使用了并不是很规范的字符术语来指代源文本中的一个Unicode代码点。

每一个代码点都应该进行区分,比如说,大写字母和小写字母就是不同的字符。

实现限制: 为了和其他的工具兼容,一个编译器不允许在源文本中出现NUL字符(U+0000)。

字符

下面是一些比较特殊的Unicode字符类:

newline        = /* Unicode 代码点 U+000A */ .
unicode_char   = /* 除了 newline 之外的其他 Unicode 代码点 */ .
unicode_letter = /* Unicode 代码点中归为 "字母" 的字符*/ .
unicode_digit  = /* Unicode 代码点中归为 "十进制数字" 的字符 */ .

Unicode标准6.0中,4.5章节“通用分类”定义了一系列的分类。Go会认为在这些分类中的 Lu,Ll,Lt,Lm,或是Lo是Unicode字符,Nd是Unicode数字。

字母和数字

下划线_(U+005F) 被认定为一个字母。

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

词法元素

注释

有两种形式的注释:

  1. 行注释从两斜杠//开始到这一行结束。一个行注释给人的感觉就是一个换行;
  2. 通用注释(块注释)/*开始到*/结束。块注释中如果有行注释的话,那么它像是换行;其他情况下,它像是空白。

注释不能嵌套。

词法符号

词法符号是Go语言的词汇表。它们分为四类:标识符关键字操作符分隔符。由空格(U+0020)、水平制表符(U+0009)、回车符(U+000D)和换行符(U+000A)所组成的空白除了可能是用于组成符号之外,其他的时候用作分隔符,在分析阶段会被忽略掉。 还有就是,换行符或是页面结束可能导致分号的插入。在将代码文本分割成符号的过程中,下一个符号应该是能组成一个合法符号的最长字符序列。

分号

正式语法使用分号";" 作为一些产生式的分隔符。 但是 Go 程序可以基于下面两条规则省略多数时候的分号:

  1. 当输入文本在被拆成了记号的时候,在一些情况下分号会自动被插入非空白行的尾部的记号流中去,但是需要这一行的最后一个记号是:

  2. 为了允许复杂的语句能够挤在一行中,所以在")" or "}"前面的分号可能省略掉。

为了反映通常的使用习惯,本文档中的代码例子通常使用这些规则而省略了分号。

标识符

标识符用来命名变量、类型等程序实体。一个标识符实际上就是一个或是多个字母/数字序列,不过第一个字符应该是字母而不能是数字。

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

有一些标识符是预声明的

关键字

下面的关键字被保留了因而不能作为标识符使用:

break        default      func         interface    select
case         defer        go           map          struct
chan         else         goto         package      switch
const        fallthrough  if           range        type
continue     for          import       return       var

操作符/运算符和间隔符

下面的一些字符序列被当做操作符/运算符、分隔符或是其他一些特殊的符号:

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

整数值

一个整数值实际就是一串数字组成的整数常量。一个可选的前缀表明了这个整数值的基数: 0表示八进制, 0x或者0X 代表十六进制。在十六进制表示中,a-f或是A-F表示数字 10 - 15。

int_lit     = decimal_lit | octal_lit | hex_lit .
decimal_lit = ( "1" … "9" ) { decimal_digit } .
octal_lit   = "0" { octal_digit } .
hex_lit     = "0" ( "x" | "X" ) hex_digit { hex_digit } .
42
0600
0xBadFace
170141183460469231731687303715884105727

浮点数值

一个浮点值实际就是由十进制数字所组成的浮点常量。它有一个整数部分、小数点、小数部分和指数部分。整数部分和小数部分还是由十进制数字组成; 指数部分是一个e或是E后面跟一个可选的有符号十进制指数。整数部分或是小数部分二者可以省略其一;小数点和指数部分也可以省略其一。

float_lit = decimals "." [ decimals ] [ exponent ] |
            decimals exponent |
            "." decimals [ exponent ] .
decimals  = decimal_digit { decimal_digit } .
exponent  = ( "e" | "E" ) [ "+" | "-" ] decimals .
0.
72.40
072.40  // == 72.40
2.71828
1.e+0
6.67428e-11
1E6
.25
.12345E+5

虚数值

一个(纯)虚数是一个复数常量,只不过它只有虚数部分,而虚数部分是用十进制数字表示。它实际就是由一个浮点常量 或是一个十进制整数后面跟一个小写的字母i组成。

imaginary_lit = (decimals | float_lit) "i" .
0i
011i  // == 11i
0.i
2.71828i
1.e+0i
6.67428e-11i
1E6i
.25i
.12345E+5i

分符值

一个分符值就是一个分符常量,实际上是一个能标识一个 Unicode 代码点的整数值。一个分符值用一个单引号引住的一个或是多个字符来表示; 在引号中,除了单引号和换行是不允许的,其他的都可以。一个单引号引住的单个字符也表示这个字符本身的 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的和surrogate halves。

在反斜杠后面某些固定的字符代表一些特殊的值:

\a   U+0007 响铃符
\b   U+0008 后退符
\f   U+000C form feed
\n   U+000A 换行符
\r   U+000D 回车符
\t   U+0009 水平制表符
\v   U+000b 垂直制表符
\\   U+005c 反斜杠
\'   U+0027 单引号 (只在分符值中才是合法的转义)
\"   U+0022 双引号 (只在字符串值中是合法的转义)

所有其他的在分符值中的以反斜杠开始的转义都是不合法的。

char_lit         = "'" ( unicode_value | byte_value ) "'" .
unicode_value    = unicode_char | little_u_value | big_u_value | escaped_char .
byte_value       = octal_byte_value | hex_byte_value .
octal_byte_value = `\` octal_digit octal_digit octal_digit .
hex_byte_value   = `\` "x" hex_digit hex_digit .
little_u_value   = `\` "u" hex_digit hex_digit hex_digit hex_digit .
big_u_value      = `\` "U" hex_digit hex_digit hex_digit hex_digit
                           hex_digit hex_digit hex_digit hex_digit .
escaped_char     = `\` ( "a" | "b" | "f" | "n" | "r" | "t" | "v" | `\` | "'" | `"` ) .
'a'
'ä'
'本'
'\t'
'\000'
'\007'
'\377'
'\x07'
'\xff'
'\u12e4'
'\U00101234'
'aa'         // 非法:太多的字符
'\xa'        // 非法:太少的十六进制数字
'\0'         // 非法:太少的八进制数字
'\uDFFF'     // 非法: surrogate half
'\U00110000' // 非法:不正确的Unicode代码点

字符串值

一个字符串值就是一系列的字符连接在一起的一个字符串常量。它有两种形式:一种是元字符串,一种是解释性字符串。

元字符串就是包括在两个方向单引号``内的字符序列。在引号内,除了反向单引号外其他字符都可以包括。一个元字符串的值就是将引号内的所有字符都不加解释滴看成是字符(UTF-8 字符) 而形成的字符串;比如说,不会将反斜杠看成特殊的字符;再者,里面可以包含换行符。元字符串中的回车符在字符串求值的过程中会被忽略掉。

解释性字符串是包括在双引号""内的字符序列。双引号内不能包括换行,引号内文本会像分符值一样对反斜杠进行转义,当然限制也一样,就是\'\"这里也是合法的。3 个八进制\nnn或是两个十六进制\xnn都是对单个字节的转移表示;而其他的转义形式都指的是(可能是多个字节的) UTF-8编码的单个字符。所以,在字符串值中的\377\xFF都表示的是单个字节,值是0xFF=255;而ÿ\u00FF\U000000FF和两个字节0xc30xbf表示的\xc3\xbf,其实都是对 UTF-8 字符 U+00FF 的表示。

string_lit             = raw_string_lit | interpreted_string_lit .
raw_string_lit         = "`" { unicode_char | newline } "`" .
interpreted_string_lit = `"` { unicode_value | byte_value } `"` .
`abc`  // 等同于 "abc"
`\n
\n`    // 等同于 "\\n\n\\n"
"\n"
""
"Hello, world!\n"
"日本語"
"\u65e5本\U00008a9e"
"\xff\u00FF"
"\uD800"       // illegal: surrogate half
"\U00110000"   // illegal: invalid Unicode code point

下面的例子的表示实际上是表示的一个东西:

"日本語"                                 // UTF-8 文本
`日本語`                                 // UTF-8 元字分符本
"\u65e5\u672c\u8a9e"                    // 明确的 Unicode 代码点
"\U000065e5\U0000672c\U00008a9e"        // 明确的 Unicode 代码点
"\xe6\x97\xa5\xe6\x9c\xac\xe8\xaa\x9e"  // 明确的 Unicode 字节

如果源文本使用两个代码点表示一个字符,比如组合一个重音和一个字母,那么如果将这个字符放入到符文值中会有问题(因为它不是一个代码点),而如果放在字符串中它占据两个代码点。

常量

常量有布尔常量文符常量整型常量浮点常量复数常量,和字符串常量等。文符、整型、浮点和复数常量统称数值常量。

常量值会出现在很多地方,比如文符值整型值浮点值虚数值,或是字符串值、指代常量的表达式、常量表达式、 常量结果的转换,或是一些内置函数比如unsafe.Sizeof(可适用于任意类型)、cap/len(用于 一些表达式)、real/imag(用于复数常量和虚数常量)的返回结果。布尔值可以用预声明的常量 truefalse表示,预声明的标识符iota也用来表示常量。

一般的,复数常量是常量表达式的一种,讨论也是放在那一节。

数值常量表示任意精度的值,不会溢出。因此,没有常量对应IEEE-754的负0、无穷和NAN值(not a number)。

常量可能是有类型的或是无类型的。值常量、truefalseiota,和一些只包含无符号操作数的 常量表达式都是无符号的。

一个常量可能明确地来自于常量声明或是某个转换,也可能隐式地出现在变量声明 中,或是在一个表达式中被赋值或是作为操作数。如果一个常量不能用它对应的类型来表示,这么这就是个错误。比如说3.0 既可以当做整数也可以当做浮点数,然而2147483648.0 (等于1<<31)可以是float32float64或是uint32类型,就是不能是 int32或是string类型。

并没有常量能代表 IEEE-754 的无穷大和非数值这两个值,但是math中的 InfNaNIsInf、和IsNaN 函数可以在运行时来返回或是测试这些值。

实现限制: 尽管我们说数值常量在语言中是任意精度的,但是一个编译器可能在实现的时候在内部只是用有限的精度来表示。不管怎么说,每一个实现必须满足:

这些需求适用于值常量和常量表达式的求值结果。

类型

一种类型决定了一个值的可能的取值范围以及能对这个值所进行的操作。 一个类型由(可能要限定的)类型名类型声明) 或是一个类型文字指定,它们本身又是由预声明的类型进行构造而得。

Type      = TypeName | TypeLit | "(" Type ")" .
TypeName  = identifier | QualifiedIdent .
TypeLit   = ArrayType | StructType | PointerType | FunctionType | InterfaceType |
         SliceType | MapType | ChannelType .

一些已经命名了的类型,比如布尔类型、数值类型和字符串类型,这些都是预声明好的。 复合类型 — 比如数组、结构体、指针、函数、接口、分片、映射和管道类型 — 需要使用已有的类型进行构造。

所谓的一个变量的静态类型 (或是简单地说类型)就是它声明时候的类型。接口类型对应的变量有种特殊的动态类型之说,也就是说它的实际类型是在运行时候根据存的值所决定的。 动态类型在程序执行的过程中类型可能改变,只要它对于接口定义时所指定的静态类型是可赋值的就可以。

每一个类型T 都有一个底层类型:如果类型T是个预定义的布尔、数值、字符串类型或是类型字面量,那么它对应的底层类型就是T本身。 否则, T的底层类型就是它在类型声明时所指定的类型的底层类型。

   type T1 string
   type T2 T1
   type T3 []T1
   type T4 T3

stringT1T2的底层类型是string。而[]T1T3T4的底层类型是[]T1

方法集

每一种类型都有其对应的方法集接口类型,§方法声明)。接口类型的方法集就是它的接口。而其他类型比如T的方法集就是所有T作为接收器的方法的集合;指针类型比如*T,的方法集是*T或是T作为接收器的所有方法的集合,这也就是说,指针的方法集包括其基类型T的方法集。包含匿名字段的结构体还有一些规则,我们会在结构体类型那里详细描述。除此之外的类型则是有一个空的方法集。在一个方法集中,每一个方法必须有唯一的不为方法名

一个类型的方法集决定了这种类型需要实现的接口,以及这种类型的接收器可以调用的方法。

布尔类型

布尔类型只能在预声明的常量truefalse中取值,它对应的预声明类型是bool

数值类型

数值类型包括整数值和浮点数值。预声明的与机器无关的数值类型有下面这些:

uint8       无符号的 8 位整数 (0 ~ 255)
uint16      无符号的 16 位整数 (0 ~ 65535)
uint32      无符号的 32 位整数 (0 ~ 4294967295)
uint64      无符号的 64 位整数 (0 ~ 18446744073709551615)

int8        带符号的 8 位整数 (-128 ~ 127)
int16       带符号的 16 位整数 (-32768 ~ 32767)
int32       带符号的 32 位整数 (-2147483648 ~ 2147483647)
int64       带符号的 64 位整数 (-9223372036854775808 ~ 9223372036854775807)

float32     IEEE-754 32 位浮点数
float64     IEEE-754 64 位浮点数

complex64   由 float32 实部和虚部所能组成的复数
complex128  由 float64 实部和虚部所能组成的复数

byte        和 uint8 一样
rune        和 int32 一样

一个有n位的整数是说有n个比特位宽,并且采用二进制补码表示。

下面还有一些预声明的类型,但是它们的长度与具体实现有关:

uint     32 位或是 64位
int      和 uint 长度一样
uintptr  一个无符号整数类型,它的长度可以容纳一个指针值

为了避免移植性的问题,所以除了byteuint8的类型一样、runeint32类型一样之外,其他的任意两种类型都是互相区分的。所以,在表达式中或是赋值的时候只要类型不一样就要进行转换。譬如说,假设在某一种机器实现上int32int都是有符号的而且长度一样,但是在使用的时候也必须进行转换。

字符串类型

字符串类型就是取值字符串的类型。字符串用起来像是分片,但是它们是不可修改的,也就是说一个字符串一旦创建,它的内容就是不变的了。预声明的字符串类型名字是string

字符串的元素的类型是byte,我们可以通过下标操作来访问它们。对字符串的某个元素取地址是不允许的,比如s[i]是第i个元素,但&s[i]操作是不允许的。字符串s的长度可以使用内置的函数len获得。一个字符串的长度在编译时就已确定了。

数组类型

数组就是某种类型的一个序列,只不过序列中的每个元素都有一个编号。序列中元素的个数叫做长度,不能为负。

ArrayType   = "[" ArrayLength "]" ElementType .
ArrayLength = Expression .
ElementType = Type .

长度是数组类型的一部分,而且必须是能求出非负整数值的常量表达式。数组a的长度可以通过内置函数len(a)求得。数组元素的下标从0开始计算一直到len(a)-1下标)。一个数组通常是一维的,但是也可以组成多维。

[32]byte
[2*N] struct { x, y int32 }
[1000]float64
[3][5]int
[2][2][2]float64  // 和 [2]([2]([2]float64)) 一个意思

分片类型

一个分片就是对一个数组上的连续一段的引用,它也是一个有编号的序列,元素取自对应的数组。一个分片类型指代的是其元素类型的数组的所有分片的集合。一个未初始化的分片的值是nil

SliceType = "[" "]" ElementType .

像数组一样,分片也有个长度,也是通过下标进行访问。分片s的长度可以通过内置的函数len(s)取得。和数组不同的是,分片的长度在执行的过程中可以改变。分片元素的下标范围从0到len(s)-1下标)。 对于同一个元素来说,在分片中的下标可能会比对应的底层数组的下标要小。

一个分片,一旦初始化后它总是关联着一个容纳其元素的底层数组。所以一个分片和它的数组共享存储,当然也和该数组的其他分片共享;需要注意的是,两个不同的数组总是代表不同的存储。

一个分片对应的底层数组可以能超出分片的范围。容量可以用来说明这种扩展:超过分片的范围但是又在数组范围内的部分;可以在原来分片(§分片)的基础上通过“再分片”获取一个扩大到数组容量的分片。一个分片a的容量可以通过内置的函数cap(a)取得。

一个新的未初始化的T类型的分片,可以使用内置函数make在创建。make 带有一个分片的类型和指定长度和容量的参数,其中容量参数是可选的:

make([]T, length)
make([]T, length, capacity)

make的调用会创建一个隐含的数组,分片就是引用的这个数组。这也意味着,执行

make([]T, length, capacity)

和下面的操作都分配一个数组并在基础上生成一个分片,生成的两个分片是相同的:

make([]int, 50, 100)
new([100]int)[0:50]

如同数组,分片通常是一维的,但是也可以复合构造更高维的对象。对于数组的数组来说,内层的数组的长度在构造的时候总是一样的,然而分片的分片(分片的数组)的长度却是可以变化的。此外,内层的分片需要单独创建。

结构体类型

一个结构体就是一个命名的元素序列,每个元素又叫做字段,每个字段都有一个类型和名字。字段的名字可以是明确指定的(标识符列表),也可以能是隐含的(匿名字段)。 在一个结构体内部,只要是非空白字段的名字就必须是唯一的

StructType     = "struct" "{" { FieldDecl ";" } "}" .
FieldDecl      = (IdentifierList Type | AnonymousField) [ Tag ] .
AnonymousField = [ "*" ] TypeName .
Tag            = string_lit .
// 空结构体
struct {}

// 有 6 个字段的结构体
struct {
  x, y int
  u float32
  _ float32  // 占位/填充
  A *[]int
  F func()
}

一个字段声明的时候只有类型却没有名字,我们叫它为匿名字段,或是嵌入字段或是类型嵌入。一个嵌入的类型必须指定一个类型名T或是一个非接口的指针*T,而且T本身不能是指针类型。这些嵌入类型的类型名作为对应的字段名。

// 带有四个匿名字段 T1, *T2, P.T3 和 *P.T4 的结构体
struct {
  T1        // field name is T1
  *T2       // field name is T2
  P.T3      // field name is T3
  *P.T4     // field name is T4
  x, y int  // field names are x and y
}

下面的声明是不合法的,因为字段名在结构体中并不唯一:

struct {
  T     // 和匿名字段 *T , *P.T 冲突
  *T    // 和匿名字段 T ,*P.T 冲突
  *P.T  // 和匿名字段 T , *T 冲突
}

对于结构体x的一个匿名字段的字段或是方法 f,如果x.f是一个合法的选择子(可能是一个字段或是一个方法f),我们就说,它被提升了。

提升后的字段用起来就赶脚是结构体的普通字段,只不过它们在结构体的复合表示中不能用作字段名。

给一个结构体类型S和一个命名类型T,提升了的方法按照下面所说的包括在结构体的方法集中:

一个字段的声明中可以跟着一个可选的字符串标签,它在相应的字段声明中会算做字段的一种属性/性质。这些标签在反射接口类型一致那里中是可见的,其他的时候可以认为是忽略不计的。

// 一个用于时间戳协议缓冲区的结构体
// 标签字符串定义了协议缓冲区字段号
struct {
  microsec  uint64 "field 1"
  serverIP6 uint64 "field 2"
  process   string "field 3"
}

指针类型

一个指针就是可以指向其他类型变量的变量。被指向的变量的类型叫做指针的基类型;如果么有初始化的话,指针值是nil

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

函数类型

一个函数类型指代的是带有相同参数和返回值类型的一类函数。一个未初始化的函数变量的值是nil

FunctionType   = "func" Signature .
Signature      = Parameters [ Result ] .
Result         = Parameters | Type .
Parameters     = "(" [ ParameterList [ "," ] ] ")" .
ParameterList  = ParameterDecl { "," ParameterDecl } .
ParameterDecl  = [ IdentifierList ] [ "..." ] Type .

在函数的参数/结果列表中,名字(标识符列表)可以都有也可以都么有。如果有的话,一个名字代表对应类型的一项(参数/结果),非名称必须不相同;如果没有,一个类型代表该类型的一项。参数/结果列表通常用小括号括起来,不过当只有一个返回值且没有名字的情况下,这个括号可以省略掉。

一个函数签名的最后一个参数可能以...为前缀,这样的函数我们叫可变函数,它们在调用的时候对于那个参数可以传递0或是多个值。

func()
func(x int) int
func(a, _ int, z float32) bool
func(a, b int, z float32) (bool)
func(prefix string, values ...int)
func(a, b int, z float64, opt ...interface{}) (success bool)
func(int, int, float64) (float64, *[]int)
func(n int) func(p *T)

接口类型

一个接口类型指定了一个称为接口方法集。一个接口类型的变量可以存某个类型的值,只要这种类型的方法集是接口方法集的超集;这样的一种类型,我们说它实现了接口.一个无初始化的接口的值为nil

InterfaceType      = "interface" "{" { MethodSpec ";" } "}" .
MethodSpec         = MethodName Signature | InterfaceTypeName .
MethodName         = identifier .
InterfaceTypeName  = TypeName .

在一个接口类型中对于所有的方法集,每一个方法必须有唯一的名字。

// 一个简单的文件接口
interface {
  Read(b Buffer) bool
  Write(b Buffer) bool
  Close()
}

可以有多种类型都实现了一个接口。比如,如果S1S2都有方法集:

func (p T) Read(b Buffer) bool { return … }
func (p T) Write(b Buffer) bool { return … }
func (p T) Close() { … }

(其中T代表S1或是S2)那么,File接口就是被S1S2都实现了,而我们并不再去关心S1S2 是否是有其他方法或是共享了其他神马方法。

一个类型实现了一个接口,只要它的所有方法中的一个子集是接口的方法即可;所以,一种类型可能实现了多个接口。比如说,所有的类型都实现了空接口:

interface{}

类似的,考虑下面的接口说明,这个说明出现在类型声明中,这个类型声明定义了一个叫做Lock的接口:

type Lock interface {
  Lock()
  Unlock()
}

如果S1S2都实现了

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

那么,它们就实现了Lock接口,当然它们也实现了File接口(看上面)。

一个接口T可以使用一个(可能带限定的)接口类型E来代替一系列方法说明,我们叫T中嵌入了接口E;这样做意味着把E中所有(导出和非导出)的方法添加到T中。

type ReadWriter interface {
  Read(b Buffer) bool
  Write(b Buffer) bool
}

type File interface {
  ReadWrite  // 和一一枚举 ReadWrite 中的方法效果一样
  Lock       // 和一一枚举 Lock 中的方法效果一样
  Close()
}

type LockedFile interface {
  Locker
  File        // 不合法: Lock 不唯一
  Lock()      // 不合法: Lock 不唯一
}

一个接口类型T不能自身嵌套在自身之中,或是递归地嵌套一个包含它自身T的接口。

// 非法:不能自身嵌套
type Bad interface {
  Bad
}

// 非法:Bad1 不能通过 Bad2 来嵌套自身
type Bad1 interface {
  Bad2
}
type Bad2 interface {
  Bad1
}

映射类型

一个 map/映射是一群无序的元素构成的组,这些元素的类型以一种类型的值作为唯一的索引key然后访问到另一种类型的某个值。一个未初始化的映射变量的值为nil

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

对于 kye/关键字类型的比较运算符== and !=比较运算符) 必须是完整定义的;于是 key 的类型不能是函数、映射或是分片。 如果 key 的类是一个接口的话,这两个比较运算符应该对动态的两个 key 值是完整定义的;失败会引起一个运行时问题

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

map 元素的个数叫做它的长度。对于一个 map m我们可以通过一个内置函数len(m)访问它的长度, 不过它的长度在执行过程中可能会发生变化。元素可以在赋值的时候进行添加,可以通过使用索引/下标表达式来获取; 我们也可以使用内置的函数delete来删除元素。

一个新的空的 map 值可以使用内置的函数make来构造,这个函数带有 map 的类型和一个可选的容量长度作为参数:

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

一个初始过的 map 的容量不受尺寸的限制:map 根据存的东西的多少会自我调整,当然有个例外就是nilnil map 等价于一个空 map,只不过它还不能添加任何元素。 may be added.

管道类型

管道提供了一种两个并发执行的函数进行同步执行或是通信的机制。管道里面只能传输某一种指定类型的值,初始化的管道的值是nil

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

<-运算符指定了管道中输出传输的方向发送或是接收。如果管道的方向并没有指定的话,那么就认为是双向的。 管道经过装换或是赋值后可能就变成只能发送或是只能接收的了。

chan T          // 可以发送或是接收 T 类型的数据
chan<- float64  // 只能发送 float64 数据
<-chan int      // 只能接收 int 数据

<-尽可能地左结合chan

chan<- chan int    // 等同于 chan<- (chan int)
chan<- <-chan int  // 等同于 chan<- (<-chan int)
<-chan <-chan int  // 等同于 <-chan (<-chan int)
chan (<-chan int)

一个新的未初始化的管道可以使用make进行构造,构造的时候需要指定管道中数据的类型,而管道的容量则是可选的, 也就是可以指定可以不指定:

make(chan int, 100)

容量 —— 也就是管道中元素的个数,指定了管道中的缓冲区的大小。如果容量大于 0,那么管道就是异步的,也就是说只有满的时候阻塞发送、空的时候阻塞接收,而其他的时候不阻塞; 当然,元素的接收顺序和发送的顺序一致。如果容量是 0 或是不指定,那么,只有在发送和接收都准备好的时候,通信正常进行,否则都进行阻塞。 一个nil管道不能进行通信。

管道可以通过内置的函数close进行关闭;而对管道是否关闭的测试可以通过接收操作符的多值赋值来实现。

类型以及值的属性

类型一致

两种类型要么它们是一致的,要么它们是不同的

两个命名类型一致只有当它们可以追溯到相同的TypeSpec。 一个命名类型和一个无名类型总是不同的类型。两个无名类型如果它们对应的类型字面量是一致的,也就是它们要有相同的结构以及每一部分都是相同的,那么它们就是相同类型

。 详述如下:

给一些声明:

type (
  T0 []string
  T1 []string
  T2 struct{ a, b int }
  T3 struct{ a, c int }
  T4 func(int, float64) *T0
  T5 func(x int, y float64) *[]string
)

下面这些类型是的等价的:

T0 和 T0
[]int 和 []int
struct{ a, b *T5 } 和 struct{ a, b *T5 }
func(x int, y float64) *[]string 和 func(int, float64) (result *[]string)

T0T1是不同的类型,因为它们是不同声明中的命名类型。func(int, float64) *T0func(x int, y float64) *[]string 是不同类型,因为T0的类型不同于[]string

可赋值

只有在下面的情况下,一个值x才可以赋值给一个T类型的变量,或是说x对于T是可赋值的:

任何一个值都可以赋值给空标识符

一个就是放置在一对大括号内的一系列声明和语句。

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

除了源文本中明确的块之外,还有一些不显眼的块:

  1. 包围着 Go 源文本的整体块
  2. 任何一个package都有一个将包中的所有源文本包住的包块
  3. 任何一个文件都有一个将文件中的所有 Go 源文本包住的文件块
  4. 每一个ifforswitch语句都认为它们在一个隐含的块之中。
  5. switch或是select语句中的每个子句都像是个隐式块。

块可以嵌套而且影响作用域

声明和作用域

一个声明绑定了一个非的标识符至一个常量、类型、变量、函数或是包。程序中的每一个标识符都必须声明。相同的块中同一个标识符不能声明两次,文件块和包块不能声明相同的标识符。

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

一个标识符的指的是标识符在文件内的影响的范围。

Go的作用域是分块的:

  1. 预声明的标识符的域是一个全局的块;
  2. 顶层(在函数外的)的常量、类型、变量、函数(不包括方法)的域是包块;
  3. 导入包的标识符的域,是导入的文件的文件块;
  4. 函数参数或是返回值变量的域,是整个函数体;
  5. 函数内的常量或是变量标识符开始于声明的位置,结束于所在块的最右侧;
  6. 函数内的类型标识符开始于类型声明处,结束于所在块的最右侧;

一个块的标识符可以在内层块中再声明,这种情况下标识符的域是在内层块的域。

package并不是一个声明。包名不属于任何域。它的意义就是标识某一个文件所属的,以及指定导入声明的默认包名。

Label域

label语句用来声明标号,它们会用在breakcontinue, and goto语句中(§break语句, §continue语句, §goto语句)。声明一个标号但是不使用是不允许的。和其他的标识符不同的是,标号并不是块作用域,它不会与其他的非标号冲突。标号的域是所在函数的函数体,但不包含嵌套函数的函数体。

空标识符/通配符

空标识符/通配符,使用下划线表示 _,它可以像其他标识符一样用在声明之中,不过空标识符在声明中并不会将名字和值的绑定。

预声明的标识符

下面的一些标识符在通用块中预声明了:

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

常量:
  true false iota

0 值:
  nil

函数:
  append cap close complex copy delete imag len
  make new panic print println real recover

导出的标识符

一个标志符被导出后就可以在其他包中使用,但是必须满足下面两个条件:

  1. 标识符的首字母是 Unicode 大写字母 (Unicode "Lu" 类); 而且
  2. 标识符要在包块中进行了声明,或是它是个字段名 /方法名

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

唯一的标识符

给定一些标识符,如果在这些中某一个不同于其他一个,我们就说它是唯一的。如果两个标识符拼写都不一样,那么肯定是不同的,或者是它们处于不同的 之内而又没有被导出,这也是不同的;除此之外的,就认为是相同的标识符。

常量声明

常量声明绑定一系列标识符到一系列常量表达式的值。标识符的数目应该和表达式的数目相等,第n个标识符绑定到第n个表达式。

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

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

如果指定了类型,那么所有的常量都是指定的类型,而且表达式对于类型来说得是可赋值的。如果类型没有指定,那么常量取的是表达式对应的类型。如果表达式是无类型常量,那么声明的常量也是无类型的,常量值就是表达式的值。举个例子,如果表达式是一个浮点数字面量,那么常量标识符就是一个浮点数常量,尽管小数部分为0。

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

在带括号的常量声明中,表达式列表除了第一个之外其他的可以省略。这种情况下省略的表达式等价于前置的第一个非空表达式的文本替换,省略表达式等价于重复前面的。标识符的数量应该等于表达式的数量。iota提供了一种生成序列常量值的机制:

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

iota

常量声明中, 预定义标识符iota 代表了连续的无类型整数 常量. 当在源代码中一个遇到常量声明的保留字 const它就会被置为0,然后依次增加。 它可以用来构造一系列常量:

const (  // iota 重置为 0
  c0 = iota  // c0 == 0
  c1 = iota  // c1 == 1
  c2 = iota  // c2 == 2
)

const (
  a = 1 << iota  // a == 1 (iota 又被重置)
  b = 1 << iota  // b == 2
  c = 1 << iota  // c == 4
)

const (
  u         = iota * 42  // u == 0     (无类型整型常量)
  v float64 = iota * 42  // v == 42.0  (float64 常量)
  w         = iota * 42  // w == 84    (无类型整型常量)
)

const x = iota  // x == 0 (iota 被重置)
const y = iota  // y == 0 (iota 被重置)

在常量列表中, 每一个iota的值是相同的,因为前面说了, 它只会在常量声明之后增加:

const (
  bit0, mask0 = 1 << iota, 1<<iota - 1  // bit0 == 1, mask0 == 0
  bit1, mask1                           // bit1 == 2, mask1 == 1
  _, _                                  // 跳过 iota == 2
  bit3, mask3                           // bit3 == 8, mask3 == 7
)

上面这个例子利用了隐式地最后一个非空表达式的重复。

类型声明

一个类型声明绑定一个标识符,类型名,至一个新的类型。新的类型和其底层类型是一样的,但不同于已经存在的类型。

TypeDecl     = "type" ( TypeSpec | "(" { TypeSpec ";" } ")" ) .
TypeSpec     = identifier Type .
type IntArray [16]int

type (
  Point struct{ x, y float64 }
  Polar Point
)

type TreeNode struct {
  left, right *TreeNode
  value *Comparable
}

type Block interface {
  BlockSize() int
  Encrypt(src, dst []byte)
  Decrypt(src, dst []byte)
}

声明的类型不会继承已有类型的方法,但是接口类型或是复合类型的元素的方法集是不变的。

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

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

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

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

// MyBlock is an interface type that has the same method set as Block.
type MyBlock Block

一个类型声明还可以用来定义不同的布尔值、数值、字符串类型,然后给它们添加方法:

type TimeZone int

const (
  EST TimeZone = -(5 + iota)
  CST
  MST
  PST
)

func (tz TimeZone) String() string {
  return fmt.Sprintf("GMT+%dh", tz)
}

变量声明

一个变量声明创建一个变量,并绑定一个标识符到该变量;声明的时候需要指定类型,而初始值则是可选的。

VarDecl     = "var" ( VarSpec | "(" { VarSpec ";" } ")" ) .
VarSpec     = IdentifierList ( Type [ "=" ExpressionList ] | "=" ExpressionList ) .
var i int
var U, V, W float64
var k = 0
var x, y float32 = -1, -2
var (
  i       int
  u, v, s = 2.0, 3.0, "bar"
)
var re, im = complexSqrt(-1)
var _, found = entries[name]  // map 查找;这里只对 "found" 感兴趣

如果指定了初始化表达式列表,那么变量就按顺序使用表达式进行赋值;所有的表达式都会用来初始化某个变量,没有对应表达式的变量会初始化为0值

如果类型指定,那么每个变量都是那种类型。否则的话,类型通过对表达式列表进行推断而得。

如果类型没有指定而且对应的表达式求得的是一个常量,那么这个声明的变量取§赋值这里描述的类型。

实现限制:一个编译器可以不允许在函数体内声明变量但是却从来不适用的情况。

短变量声明

可以使用短变量声明

ShortVarDecl = IdentifierList ":=" ExpressionList .

它是一个变量声明不带类型的简化版:

"var" IdentifierList = ExpressionList .
i, j := 0, 10
f := func() int { return 7 }
ch := make(chan int)
r, w := os.Pipe(fd)  // os.Pipe() 返回两个值
_, y, _ := coord(p)  // coord() 返回三个值,不过只关心 y 而已

不像其他的变量声明,短变量声明可能重复声明一个在相同块下的相同类型的变量,这种重复声明只能出现在多变量短声明中。重复声明不会引入一个新的变量,它只是给原来的变量赋了一个新值。

field1, offset := nextField(str, 0)
field2, offset := nextField(str, offset)  // 重新声明 offset
a, a := 1, 2                              // 不允许: a 进行了两次声明,或是 a 在别处声明过但此处没有声明新变量

短变量声明只能出现在函数内部。在一些地方譬如ifforswitch的初始化中,可以用来声明临时变量(§语句)。

函数声明

函数声明将一个标识符也就是函数名和一个具体的函数绑定到一起。

FunctionDecl = "func" FunctionName ( Function | Signature ) .
FunctionName = identifier .
Function     = Signature FunctionBody .
FunctionBody = Block .

如果函数签名中指定了返回值参数,那么函数体的语句必须最后以终结语句结束。

func IndexRune(s string, r rune) int {
  for i, c := range s {
    if c == r {
      return i
    }
  }
  // invalid: 缺少返回语句
}

一个函数声明可以没有函数体,这样的函数声明只是提供了函数调用时的签名,而真正的函数甚至可以是go语言之外实现的,比如一个汇编例程。

func min(x int, y int) int {
  if x < y {
    return x
  }
  return y
}

func flushICache(begin, end uintptr)  // 外部实现

方法声明

方法就是带接收器的函数。方法声明绑定一个标识符就是方法名到一个方法,同时它也方法和把接收器的基类型关联了起来。

MethodDecl   = "func" Receiver MethodName ( Function | Signature ) .
Receiver     = Parameters .

在方法名之前指定的参数就是方法的接收器,这里的参数必须是单个不可变的,类型必须是T或是*T的形式,其中T是一个类型名。类型T被称为接收器的基类型,它不能是一个指针或是接口类型,而且它必须和方法处于相同的包下。我们会说方法绑定到了基类型上,方法名只对类型T或是*T选择子是可见的。

绑定到基类型的方法的非名字必须是唯一的。如果接收器的值并没有在方法内部使用到,那么声明中的标识符可以省略;这条规则也适用于函数和方法的参数。如果基类型是结构体类型,那么非空的方法名和字段名必须是不同的。

如果Point是一个类型,那么下面的声明

func (p *Point) Length() float64 {
  return math.Sqrt(p.x * p.x + p.y * p.y)
}

func (p *Point) Scale(factor float64) {
  p.x *= factor
  p.y *= factor
}

绑定了LengthScale方法到接收器类型*Point,基类型是Point

方法的类型是一个函数类型,接收器是其第一个参数。譬如说,方法Scale的类型是

func(p *Point, factor float64)

但是这样的函数声明不能算作一个方法。

表达式

表达式是求一系列操作数的运算结果或是函数值。

操作数

操作数是表达式中用到的值。操作数可以是一个字面量,或是一个(可能限定的)标识符。标识符可以是常量变量函数方法表达式,或是带括号的表达式。

Operand    = Literal | OperandName | MethodExpr | "(" Expression ")" .
Literal    = BasicLit | CompositeLit | FunctionLit .
BasicLit   = int_lit | float_lit | imaginary_lit | char_lit | string_lit .
OperandName = identifier | QualifiedIdent.

限定标识符

一个限定的标识符指的是一个带有包名前缀限制的标识符。包名和标识符都不能为

QualifiedIdent = PackageName "." identifier .

一个限定的标识符访问的不是本包的一个标识符,而这个包必须提前导入导出的必须是标识符,并且声明在包的包块作用域。

math.Sin  // math 包的 Sin 函数

复合字面量

复合字面量用来构造结构体、数组、分片和map类型的值,每一次都是创建一个新的。复合字面量包含值的类型,然后是一系列由{}括起来的复合元素。每一个元素都是单个表达式或是键值对。

CompositeLit  = LiteralType LiteralValue .
LiteralType   = StructType | ArrayType | "[" "..." "]" ElementType |
                SliceType | MapType | TypeName .
LiteralValue  = "{" [ ElementList [ "," ] ] "}" .
ElementList   = Element { "," Element } .
Element       = [ Key ":" ] Value .
Key           = FieldName | ElementIndex .
FieldName     = identifier .
ElementIndex  = Expression .
Value         = Expression | LiteralValue .

LiteralType必须是结构体、数组、分片或是map类型(语法会强制校验,除非给的是 TypeName 类型)。表达式的类型必须可赋值给LiteralType相应的字段、元素、或是键类型,不能有任何转换。这里的键应该这样理解:对于结构体就是字段名,对于数组和分片就是下标,对于map就是对应的key。如果是map字面量的话,所有元素都必须有key。元素有重复的字段名或是常量key值是错误的。

结构体字面量有一下规则:

如果有以下声明

type Point3D struct { x, y, z float64 }
type Line struct { p, q Point3D }

那么可以这样

origin := Point3D{}                            // 0值
line := Line{origin, Point3D{y: -4, z: 12.3}}  // line.q.x 是 0 值

数组和分片字面量遵从一下规则:

取地址的复合字面量(§取地址运算符)生成字面量值唯一实例的指针.

var pointer *Point3D = &Point3D{y: 1000}

数组字面量的长度由 LiteralType 中的 length 指定.如果提供的元素少于长度,缺少的元素初始化为0值.如果提供的元素下标如果超过了数组的范围,这是错误的.记号...指定数组的长度,也就是最长下标+1.

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

分片字面量描述的是整个底层数组的字面量,那么分片字面量的长度和容量是最大元素下标+1.一个分片字面量是如下形式:

[]T{x1, x2, … xn}

其实这是下面的一种简化版:

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

在数组/分片/map类型T复合字面量内,如果元素本身也是复合字面量,那么它可以省略类型T.相似的,是复合字面量地址&T的元素可以省略类型*T.

[...]Point{{1.5, -3.5}, {0, 0}}   // 等同于 [...]Point{Point{1.5, -3.5}, Point{0, 0}}
[][]int{{1, 2, 3}, {4, 5}}        // 等同于 [][]int{[]int{1, 2, 3}, []int{4, 5}}

[...]*Point{{1.5, -3.5}, {0, 0}}  // 等同于 [...]*Point{&Point{1.5, -3.5}, &Point{0, 0}}

如果在关键字和 if/for/switch 语句的左侧括号 { 之间有使用了 TypeName 形式的复合字面量,那么可能有一个解析歧义发生,因为字面量中的表达式的括号{和块作用域的混淆了.为了避免这种情况,可以在字面量两边加上括号().

if x == (T{a,b,c}[i]) { … }
if (x == T{a,b,c}[i]) { … }

合法的数组、分片、map字面量的例子:

// 素数列表
primes := []int{2, 3, 5, 7, 9, 2147483647}

// 当 ch 是元音的时候 vowels[ch] 为 true
vowels := [128]bool{'a': true, 'e': true, 'i': true, 'o': true, 'u': true, 'y': true}

// 数组 [10]float32{-1, 0, 0, 0, -0.1, -0.1, 0, 0, 0, -1}
filter := [10]float32{-1, 4: -0.1, -0.1, 9: -1}

// frequencies in Hz for equal-tempered scale (A4 = 440Hz)
noteFrequency := map[string]float32{
  "C0": 16.35, "D0": 18.35, "E0": 20.60, "F0": 21.83,
  "G0": 24.50, "A0": 27.50, "B0": 30.87,
}

函数字面值

一个函数字面值代表一个匿名函数。它包括一个函数类型的说明以及一个函数体。

FunctionLit = FunctionType Body .
func(a, b int, z float64) bool { return a*b < int(z) }

一个函数字面值可以赋值给一个变量,也可以指直接调用。

f := func(x, y int) int { return x + y }
func(ch chan int) { ch <- ACK }(replyChan)

函数字面量可以是闭包closures: 它访问它周围函数中的变量。这些变量既可以在周围的函数中使用,也可以在这个函数字面量中使用,只要它们在使用着,它们就存在着,也就是说,它们的 生存期可以延长。

主表达式

主表达式指的是那些一元、二元运算符表达式:

PrimaryExpr =
  Operand |
  Conversion |
  BuiltinCall |
  PrimaryExpr Selector |
  PrimaryExpr Index |
  PrimaryExpr Slice |
  PrimaryExpr TypeAssertion |
  PrimaryExpr Call .

Selector       = "." identifier .
Index          = "[" Expression "]" .
Slice          = "[" [ Expression ] ":" [ Expression ] "]" .
TypeAssertion  = "." "(" Type ")" .
Call           = "(" [ ArgumentList [ "," ] ] ")" .
ArgumentList   = ExpressionList [ "..." ] .
x
2
(s + ".txt")
f(3.1415, true)
Point{1, 2}
m["foo"]
s[i : j + 1]
obj.color
f.p[i].x()

选择子

对于主表达式x(不是包名)来说,选择子表达式

x.f

代表的是x(有时候可能会是*x;见下面)的字段或是方法f。标识符f,不管是字段或是方法,我们都叫它选择子;它必须 不能是空标识符。选择子表达式的类型就是f的类型。如果x是个包名的话,你还是看限定标识符这里吧。

选择子f可能指代的就是类型T的字段或是方法f,它也可能指代的是T的一个匿名字段的字段或是方法f。 访问到f所要经过的匿名字段的数量叫做在T中的深度。如果字段fT中声明,那么它的深度就是0。在T中的字段或是方法 f的深度是f在匿名字段A(在T声明)中的深度再加 1。

对选择子有下面一些规则:

  1. 对已一个T类型或是一个指针*TT不是接口类型)的值xx.f代表的是在T中的最浅层次的字段或是方法, 它们之中有一个f。如果在最浅层次上没有精确的一个f,那么选择子表达式就是不合法的。
  2. 对于一个I类型(I是一个接口)的值x 那么 x.f指代的是赋值给x的实际的f名字的方法。如果在I的 方法集中没有一个名字为method set的方法,那么这个选择子表达式就是不合法的。
  3. 其他情况下,x.f都是不合法的。
  4. 如果x是个指针或是接口类型,但是值却是nil,不管是赋值、计算值或是调用x.f都引起一个运行时问题

选择子会自动解析指向结构体的指针。如果x是个一个结构体指针,那么x.y就代表(*x).y;如果y还是一个 结构体指针,那么x.y.z代表(*(*x).y).z,以此类推。如果x包括一个匿名字段类型*A,而A又是一个结构体类型,那么 x.f代表的是(*x.A).f

举个例子,给个声明:

type T0 struct {
x int
}

func (*T0) M0()

type T1 struct {
y int
}

func (T1) M1()

type T2 struct {
z int
T1
*T0
}

func (*T2) M2()

type Q *T2

var t T2     // with t.T0 != nil
var p *T2    // with p != nil and (*p).T0 != nil
var q Q = p

下面的都是合法的:

t.z          // t.z
t.y          // t.T1.y
t.x          // (*t.TO).x

p.z          // (*p).z
p.y          // (*p).T1.y
p.x          // (*(*p).T0).x

q.x          // (*(*q).T0).x        (*q).x is a valid field selector

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

但下面的是不合法的:

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

索引/下标

一个有下面形式的主表达式:

a[x]

用来访问数组、分片、字符串或是映射 map a中的以x为下标/索引的元素。值x被叫做下标/索引index或是map 键。对于它们有下面的规则:

假设 A是一个数组类型,那么对于A或是*Aa,或是对于是分片类型S的变量a,那么

如果T是一个string类型,那么对于T类型的a来说:

如果Mmap 类型,那么对于M类型的a来说:

除了上面的情况之外,a[x]都是不合法的。

对于类型map[K]V的mapa来说,下标表达式可以以一种特殊的形式用在赋值或是初始化中:

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

在这种形式中,下表表达式的结果是一个类型为(V, bool)pair对的值。 如果keyx是存在的,那么ok的值是true,否则是falsev的值就是a[x]作为单个结果的值。

对于一个nil map的元素的赋值,会引起运行时错误

分片

对于一个字符串、数组、数组指针或是一个分片a,主表达式

a[low : high]

实际上构造了一个子字符串或是分片。下标表达式lowhigh决定了哪些元素出现在结果中。结果是下标从low开始长度为high  - low。在堆数组a进行分片之后

a := [5]int{1, 2, 3, 4, 5}
s := a[1:4]

分片s类型[]int,长度为3,容量为4, 对应的元素是:

s[0] == 2
s[1] == 3
s[2] == 4

为了方便起见,下标表达式的前后两部分都可以省略,缺省的low默认是0,缺省的high默认是分片的长度:

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

对于数组或是字符串来说,下标lowhigh 必须满足 0 <= low <= high <= 长度; 对于分片来说,访问上界不是长度而是容量大小。

如果分片的对象是字符串或是分片类型,那么分片的结果还是对应的字符串或是分片类型;如果分片的对象是数组,那么它必须是 可寻址的,分片的结果是一个分片类型,元素的类型和数组的元素类型一致。

类型断言

对于接口类型或是其他类型T来说,x的如下主表达式

x.(T)

断言x不是nil,并把T类型的值保存到x中。这种x.(T)形式叫做类型断言

更精确地说,如果T不是一个接口类型,那么x.(T)断言x等同于类型T;如果T是一个接口类型,x.(T)断言x实现了接口T接口类型)。

如果断言成立,那么表达式的值和x中的值一样,类型是T;如果断言失败,会引起一个运行时异常。换句话说,即便x是一个动态类型,但是一个运行正常的程序会保证x.(T)就是类型T

如果断言是用在赋值或是初始化中:

v, ok = x.(T)
v, ok := x.(T)
var v, ok = x.(T)

断言的结果是一个(T, bool)对。如果断言成立,那么结果是(x.(T), true)对;否则,结果是(Z, false),其中Z是一个T类型的0值,这种情况下不会引起一个运行时异常。这种构造的类型断言更像是返回一个值和一个成功标记的函数调用(§赋值)。

函数调用

假定f是函数类型F,那么下面的表达式

f(a1, a2, … an)

调用f,参数是a1,a2,… an。除了一个特殊的情况之外,所有的参数都必须是单值表达式,而且可以赋值F的对应参数。参数是在函数调用之前求值。表达式的类型就是F的返回值类型。方法的调用和函数调用类似,只不过方法要指定一个方法的接收器类型的选择子。

math.Atan2(x, y)  // 函数调用
var pt *Point
pt.Scale(3.5)  // 方法调用,pt 是接收器

函数调用中,函数值和参数以正常顺序求值。在它们求值之后,调用参数会传递给函数,然后调用开始执行。当函数返回的时候,返回值参数会传递给调用方。

nil函数进行调用会引起一个运行时异常

介绍一种特殊的情况,如果函数或是方法g的返回值参数正好匹配函数或是方法f的参数,那么f(g(parameters_of_g))就是将g调用的返回结果依次传给f进行调用。f的调用参数必须全部都是g的结果,并且g需要有至少一个返回结果。如果f的最后一个参数带...,那么g的返回值在正常的参数赋值之后g依然保留。

func Split(s string, pos int) (string, string) {
  return s[0:pos], s[pos:]
}

func Join(s, t string) string {
  return s + t
}

if Join(Split(value, len(value)/2)) != value {
  log.Panic("test fails")
}

方法调用x.m()如果合法,需要x方法集包含m,并且参数列表可以赋值给m。如果x可寻址的,并且&x的方法集包含m,那么x.m()(&x).m()的简化版:

var p Point
p.Scale(3.5)

没有特殊的方法类型,也没有方法字面量.

参数传递给...

如果f的最后一个参数是...T的可变参数,那么那个参数等价于一个[]T类型的参数.对于f的每一次调用,最后一个参数是一个[]T的分片,所有的参数对于T类型必须是可赋值的,分片的长度是调用时传递参数的个数,每一次调用可能不同.

假定给如下函数和调用

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

Greeting的参数who的值是[]string{"Joe", "Anna", "Eileen"}

如果最后一个参数可以赋值给一个分片类型[]T,那么它可以不加修改地传递给...T参数,也不会创建新的分片。

给分片s和调用

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

Greeting的参数who的值和s一样,使用相同的底层数组。

运算符

运算符结合了运算数和表达式。

Expression = UnaryExpr | Expression binary_op UnaryExpr .
UnaryExpr  = PrimaryExpr | unary_op UnaryExpr .

binary_op  = "||" | "&&" | rel_op | add_op | mul_op .
rel_op     = "==" | "!=" | "<" | "<=" | ">" | ">=" .
add_op     = "+" | "-" | "|" | "^" .
mul_op     = "*" | "/" | "%" | "<<" | ">>" | "&" | "&^" .

unary_op   = "+" | "-" | "!" | "^" | "*" | "&" | "<-" .

我们会在其他地方讨论比较。除了移位运算或是无类型常量外,其他二元运算符的操作数类型必须是一致的如果操作数都是常量,可以看常量表达式小节。

除了移位操作之外,如果两个运算符中的一个是无类型的常量而另外一个不是,那么常量会转换为另外的一个类型。

移位表达式的右侧运算符必须是无符号整型,或是可以转换成整型的无类型常量.如果一个非常量移位表达式的左侧是一个无类型常量,那么常量的类型和如果移位表达式被替换成其左侧表达式后的类型.如果根据上下文不能判断类型,那么类型就是int,譬如移位表达式和无符号常量比较.

var s uint = 33
var i = 1<<s           // 1 has type int
var j int32 = 1<<s     // 1 has type int32; j == 0
var k = uint64(1<<s)   // 1 has type uint64; k == 1<<33
var m int = 1.0<<s     // 1.0 has type int
var n = 1.0<<s != 0    // 1.0 has type int; n == false if ints are 32bits in size
var o = 1<<s == 2<<s   // 1 and 2 have type int; o == true if ints are 32bits in size
var p = 1<<s == 1<<33  // illegal if ints are 32bits in size: 1 has type int, but 1<<33 overflows int
var u = 1.0<<s         // illegal: 1.0 has type float64, cannot shift
var v float32 = 1<<s   // illegal: 1 has type float32, cannot shift
var w int64 = 1.0<<33  // 1.0<<33 is a constant shift expression

运算符优先级

一元运算符总是有最高的优先级。然而++--运算符形成的语句,它们不是表达式,语句*p++等同于(*p)++。二元运算符的优先级有5级。乘法一级是最强的,后面是加法一级,其次比较一级,再其次逻辑&&一级,最后是逻辑||一级。

  优先级           运算符
    5             *  /  %  <<  >>  &  &^
    4             +  -  |  ^
    3             ==  !=  <  <=  >  >=
    2             &&
    1             ||

优先级相同的二元运算符自左向右结合。举个例子x / y * z(x / y) * z相同。

+x
23 + 3*x[i]
x <= f()
^a >> b
f() || g()
x == y+1 && <-chanPtr > 0

算术操作

算出运算符作用于数值类型,得到一个和第一个操作数类型相同的结果。四个标准的算术运算操作(+-*/)可以用于整数、浮点数和复数类型;+还可以用于字符串类型。其他算术运算只能用于整数。

+    求和                    integers, floats, complex values, strings
-    求差                    integers, floats, complex values
*    求积                    integers, floats, complex values
/    求商                    integers, floats, complex values
%    求模                    integers
 
&    按位与            integers
|    按位或            integers
^    按位异或          integers
&^   按位清除          integers

<<   左移            integer << unsigned integer
>>   右移            integer >> unsigned integer

字符串可以使用+进行连接操作,或是+=进行赋值操作。

s := "hi" + string(c)
s += " and good bye"

字符串连接操作会创建新的字符串。

对于两个整型xy,商q = x / y和模r = x % y应该满足以下关系:

x = q*y + r  and  |r| < |y|

其中,x / y向0截取("truncated division")。

 x     y     x / y     x % y
 5     3       1         2
-5     3      -1        -2
 5    -3      -1         2
-5    -3       1        -2

规则有一个例外,如果x是最小的负数值,那么商q = x / -1等于x (r = 0)。

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

如果除数是0,那么引起一个运行时异常。如果被除数是正数,而除数是2的指数,那么除法可能会用右移运算符替代,余数会用按位与操作来计算。

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

移位运算符按照第二个参数的位数按位移动第一个操作数。如果左侧操作数是有符号的,那么就是算术移位;如果是无符号的,那么就是逻辑移位。移动的位数没有限制。移位操作就像是第一个操作数移动了n次,一次一位。x << 1x*2相同,x >> 1x/2相同,只不过是向负无穷截取。

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

+x                          : 0 + x
-x    取负              : 0 - x
^x    取反    : m ^ x  with m = "all bits set to 1" for unsigned x
                                      and  m = -1 for signed x

对于浮点数和复数,+x就是x-x就是x的负值。根据 IEEE-754 标准,一个浮点数或是复数除以0结果是未指定的;不过实现可以让其引起一个运行时异常

整数溢出

对于无符号整数值来说,+-*,和<<都是模2n计算。n 就是无符号整数类型(§整数类型)的位宽。简单滴说,这些整数操作会将溢出的高位忽略掉,但是程序可能会依赖于“环绕”现象。

对于有符号整数,+-*<<可能会溢出,这是合法的,结果值也是存在的,不过依赖于有符号数的表示、执行的操作以及操作数本身。溢出不会引起异常。编译器不能在假定溢出不会发生的情况下优化代码。譬如说,不能假定x < x + 1总是成立。

比较运算符

比较运算符比较两个操作数,然后得出一个bool值。

==    相等
!=    不相等
<     小于
<=    小于等于
>     大于
>=    大于等于

在比较中,第一个操作数对于第二个操作数的类型来说必须是可赋值的,相反依然成立。

等值比较元素符==!=比较的对象必须是可比较的.顺序运算符<<=>, and >=比较的对象必须是可排序的.比较结果如下定义:

比较两个一致动态类型的值的时候,如果值的类型本身不可比较,那么会引起一个运行时异常。这种行为不仅适用于直接的接口值比较,同样适用于比较接口值数组,或是带接口值字段的结构体。

分片、map和函数值是不可比较的。但是,作为一个特殊情况,一个分片、map或是函数值可以和nil进行比较。指针、管道、接口值和nil的比较也是允许的,也是服从上面说的规则。

比较操作的结果可以赋值给任何布尔类型。如果没有指定布尔类型,那么结果类型是bool

type MyBool bool

var x, y int
var (
  b1 MyBool = x == y // 结果类型 MyBool
  b2 bool   = x == y // 结果类型 bool
  b3        = x == y // 结果类型 bool
)

逻辑运算符

逻辑运算符应用于bool值,得出一个和操作数类型相同的类型。右侧操作数不一定求值。

&&    conditional and    p && q  is  "if p then q else false"
||    conditional or     p || q  is  "if p then true else q"
!     not                !p      is  "not p"

取地址运算符

对于T类型的操作数x来说,取地址操作&x会生成一个指向x的类型为*T的指针。操作数必须是可寻址的,也就是说,操作数必须是变量、指针间解析、分片索引操作,或是可寻址的结构体的字段选择子,或是可寻址数组的下标操作。也有一种情况是,x也可以是复合字面量

对于*T类型的操作数x来说,指针解析*x就是x指向的T类型的值。如果xnil,那么试图*x将会引起一个运行时异常

&x
&a[f(2)]
*p
*pf(x)

接收操作符

对于管道类型的操作数ch来说,接收操作<-ch是从管道ch中接收到的值,类型就是管道元素的类型。如果值没有获取到,表达式一直阻塞。从nil管道中接收会永远阻塞。从一个关闭的管道中接收总是成功,返回的是元素类型的0值

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

用在赋值或是初始化中的接收表达式的形式:

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

会求得一个额外的bool类型的结果,指示通信是否成功。如果接收成功则oktrue,否则是false,也就是当管道中没有值或是已经关闭的情况下,取得的值是0值。

方法表达式

如果M在类型T方法集中,那么T.M就像是一个普通的参数调用,参数是M,只不过前面追加了一个方法的接收器作为参数。

MethodExpr    = ReceiverType "." MethodName .
ReceiverType  = TypeName | "(" "*" TypeName ")" .

考虑一个结构体类型T有两个方法:T类型接收器的Mv*T类型接收器的Mp

type T struct {
  a int
}
func (tv  T) Mv(a int) int         { return 0 }  // 值接收器
func (tp *T) Mp(f float32) float32 { return 1 }  // 指针接收器
var t T

表达式

T.Mv

得出一个函数,等价于Mv只不过第一个参数是显式的接收器,它的签名是:

func(tv T, a int) int

这个函数可以正常调用,所以下面的三个调用是一样的:

t.Mv(7)
T.Mv(t, 7)
f := T.Mv; f(t, 7)

类似的,表达式:

(*T).Mp

会生成一个函数值,签名是:

func(tp *T, f float32) float32

对于一个值接收器的方法来说,可以衍生出一个显式带指针接收器的函数,所以:

(*T).Mv

会生成一个函数值,签名是:

func(tv *T, a int) int

这种创建一个值作为接收器传递给底层方法的情况,方法不会重写传给函数调用的地址值。

最后一个case,指针接收器的方法用一个值接收器去调用是不允许的,因为指针接收器的方法不在值接收器方法集内。

从方法中衍生出的函数值使用的是函数调用的预发,接收器作为第一个参数传递给函数。譬如,f := T.Mvf可以如此调用f(t, 7)而不是t.f(7)。构造一个绑定到接收器的函数,可以使用闭包

从一个借口类型的方法衍生出一个函数值是可以的,函数结果显式带接口类型的接收器的参数。

转换

转换表达式的形式是:T(x),其中T是要转换的类型,x是要转换的表达式。

Conversion = Type "(" Expression ")" .

如果一个类型开始于一个运算符,那么它必须用括号()括起来:

*Point(p)        // 如同 *(Point(p))
(*Point)(p)      // p 转换为 (*Point)
<-chan int(c)    // 如同 <-(chan int(c))
(<-chan int)(c)  // c 转换为 (<-chan int)

常量x转换为T类型,有以下几种情况:

转换一个常量生成一个有类型的常量作为结果:

uint(iota)               // iota 值类型:uint
float32(2.718281828)     // 2.718281828 类型: float32
complex128(1)            // 1.0 + 0.0i 类型: complex128
string('x')              // "x" 类型: string
string(0x266c)           // "♬" 类型: string
MyString("foo" + "bar")  // "foobar" 类型: MyString
string([]byte{'a'})      // 非常量: []byte{'a'} 不是一个常量
(*int)(nil)              // 非常量: nil 不是一个常量, *int 不是布尔、数值或是字符串类型
int(1.2)                 // 不合法: 1.2 不能表示成 int
string(65.0)             // 不合法: 65.0 不是整数常量

一个非常量值x可以转换为T,有下面一些情况:

数值类型之间,或是和字符串之间的(非常量)转换还有特殊的规则。这些转换可能会改变x的表示,以及引起一些运行时的开销。所有其他的转换只是改变类型,不会改变x的表示。

指针和整型之间不能转换。不过unsafe包在一个限制条件下实现了这个功能。

数值类型之间的转换

非常量数值的转换,有一下规则:

  1. 整型之间进行转换的时候,如果有一个值是无符号,那么它会符号扩展到隐式的无穷精度,其他情况进行0扩展。然后截取到结果类型的尺寸。譬如,如果v := uint16(0x10F0),那么uint32(int8(v)) == 0xFFFFFFF0。转换总是会得到一个有效的值,但不会有任何溢出的迹象;
  2. 当浮点数转换为整数类型,是去掉小数部分(向0截取);
  3. 当把整型或是浮点数转换为浮点型,或是复数转换成另外一个附属,那么会向目标类型四舍五入。譬如,float32类型的变量x可能使用超过 IEEE-754 整型的精度进行存储,但是float32(x)是将x的值往32位精度进行四舍五入。类似的,x + 0.1可能会超过32位精度,而float32(x + 0.1)不会。

所有涉及到浮点数或是复数的非常量转换,如果结果类型不能成功滴表示转换,那么结果值依赖于实现。

和字符串类型之间的转换

  1. 有符号或是无符号整数转换成字符串,得到的是整数表示的UTF-8字符串。超过 Unicode 代码点范围外的数值转换成"\uFFFD"
    string('a')       // "a"
    string(-1)        // "\ufffd" == "\xef\xbf\xbd"
    string(0xf8)      // "\u00f8" == "ø" == "\xc3\xb8"
    type MyString string
    MyString(0x65e5)  // "\u65e5" == "日" == "\xe6\x97\xa5"
    
  2. 将字节分片转换成字符串,得到的是分片中连续元素的字符串。如果分片是nil,则结果是空串。
    string([]byte{'h', 'e', 'l', 'l', '\xc3', '\xb8'})  // "hellø"
    
    type MyBytes []byte
    string(MyBytes{'h', 'e', 'l', 'l', '\xc3', '\xb8'})  // "hellø"
    
  3. 将 rune 分片转换成字符串,得到的是每个 rune 转换成字符串的连接串。如果分片是nil,则结果是空串。
    string([]rune{0x767d, 0x9d6c, 0x7fd4})  // "\u767d\u9d6c\u7fd4" == "白鵬翔"
    
    type MyRunes []rune
    string(MyRunes{0x767d, 0x9d6c, 0x7fd4})  // "\u767d\u9d6c\u7fd4" == "白鵬翔"
    
  4. 将字符串转换成字节分片,得到的是字符串字节元素的分片。如果是空串,则结果是[]byte(nil)
    []byte("hellø")   // []byte{'h', 'e', 'l', 'l', '\xc3', '\xb8'}
    MyBytes("hellø")  // []byte{'h', 'e', 'l', 'l', '\xc3', '\xb8'}
    
  5. 将字符串转换成 rune 分片,得到的是字符串每个代码点的分片。如果是空串,则结果是[]rune(nil)
    []rune(MyString("白鵬翔"))  // []rune{0x767d, 0x9d6c, 0x7fd4}
    MyRunes("白鵬翔")           // []rune{0x767d, 0x9d6c, 0x7fd4}
    

常量表达式

常量表达式只能包含常量操作数,它们是在编译期求值。

无类型的布尔、数值、字符串常量可以用在布尔、数值或是字符串类型可以出现的地方。除了移位运算符之外,如果二元运算符是不同类型的无类型常量,结果类型是下面类型列表出现的类型的靠后的一个:整型、rune型、浮点型、复数。举个例子,一个无类型的整数常量除以一个无类型的复数常量,结果是一个无类型的复数常量。

常量的比较结果总是一个无类型的布尔常量。如果左侧的操作数是无类型的移位操作,那结果是一个整数常量,否则常量的类型和左侧操作数的类型一样,当然它必须是整型(§算术运算符)。对于无类型常量进行操作,结果总是无类型常量(也就是布尔、整型、浮点型、复数,或是字符串常量)。

const a = 2 + 3.0          // a == 5.0   (无类型浮点数常量)
const b = 15 / 4           // b == 3     (无类型整数常量)
const c = 15 / 4.0         // c == 3.75  (无类型浮点数常量)
const Θ float64 = 3/2      // Θ == 1.5   (类型 float64)
const d = 1 << 3.0         // d == 8     (无类型整数常量)
const e = 1.0 << 3         // e == 8     (无类型整数常量)
const f = int32(1) << 33   // f == 0     (类型 int32)
const g = float64(2) >> 1  // illegal    (float64(2) 有类型浮点数常量)
const h = "foo" > "bar"    // h == true  (无类型布尔常量)
const j = true             // j == true  (无类型布尔常量)
const k = 'w' + 1          // k == 'x'   (无类型rune常量)
const l = "hi"             // l == "hi"  (无类型字符串常量)
const m = string(k)        // m == "x"   (类型 string)
const Σ = 1 - 0.707i       //            (无类型复数常量)
const Δ = Σ + 2.0e-4       //            (无类型复数常量)
const Φ = iota*1i - 1/1i   //            (无类型复数常量)

对于无类型整型、rune型或是浮点数常量应用内置函数complex,得出无类型复数常量。

const ic = complex(0, c)   // ic == 3.75i (u无类型复数常量)
const iΘ = complex(0, Θ)   // iΘ == 1.5i  (类型 complex128)

常量表达式总是精确值。内部的值或是常量本身需要的精度要明显高于语言的预声明类型支持的精度。下面是合法的声明:

const Huge = 1 << 100
const Four int8 = Huge >> 98

有类型常量必须能够精确地表示常量类型的值。下面的常量表达式就是不合法的:

uint(-1)     // -1 不能用 uint 表示
int(3.14)    // 3.14 不能用 int 表示
int64(Huge)  // 1<<100 不能用 int64 表示
Four * 300   // 300 不能用 int8 表示
Four * 100   // 400 不能用 int8 表示

一元取反运算符^用于掩码:掩码对于无符号常量总是全1,对于有类型和无类型的常量总是 -1。

^1         // 无类型整型常量,值为 -2
uint8(^1)  // error, same as uint8(-2), out of range
^uint8(1)  // typed uint8 constant, same as 0xFF ^ uint8(1) = uint8(0xFE)
int8(^1)   // same as int8(-2)
^int8(1)   // same as -1 ^ int8(1) = -2

实现限制:一个编译器在计算无类型浮点数或是复数常量表达式的时候,可能会用四舍五入;可以参看常量节的实现限制。这种四舍五入可能导致一个浮点数常量不能精确表示为整数,尽管如果从无限精度上看它是一个整数。

求值顺序

当对一个表达式的operands赋值,或是return语句,所有的函数调用,方法调用以及通信操作进行求值的时候,它们按照词法的从左到右的顺序。

下面的语句

y[f()], ok = g(h(), i()+x[j()], <-c), k()

函数调用以及通信以f()h()i()j()<-cg()k()的顺序进行。然而相比于x的求值以及下标操作,y的求值顺序是未指定的。

a := 1
f := func() int { a = 2; return 3 }
x := []int{a, f()}  // x may be [1, 3] or [2, 3]: evaluation order between a and f() is not specified

在单个表达式内的浮点数操作根据结合性进行求值。可以用括号来改变这种结合性。譬如在表达式x + (y + z)y + zx之前操作。

语句

语句控制了程序的执行。

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

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

空语句

空语句什么也不做。

EmptyStmt = .

标号语句

gotobreak或是continue会用到标号语句。

LabeledStmt = Label ":" Statement .
Label       = identifier .
Error: log.Panic("error encountered")

表达式语句

函数调用、方法调用和接收操作可以出现在语句中;有的时候可能会用到括号。

ExpressionStmt = Expression .
h(x+y)
f.Close()
<-ch
(<-ch)

发送语句

发送语句是向管道中送入一个值。管道表达式当然必须是管道类型,而值对于管道中的元素类型来说必须是可赋值的

SendStmt = Channel "<-" Expression .
Channel  = Expression .

管道和求值都是发生在通信之前。在发送可以开始之前,通信是处于阻塞状态;向一个无缓冲管道发送数据,只有接收准备就绪时发送才正常进行;而向一个有缓冲 管道发送数据,只要缓冲区有空间发送便可以进行。如果向一个已经关闭了的管道发送数据,会导致一个运行时问题。向nil中发送一个数据会导致永远阻塞。

ch <- 3

自增自减语句

++和--语句对操作数增加或是减少一个无类型的常量1。为了能够赋值,要求操作数必须是可寻址的或是一个map的下标表达式。

IncDecStmt = Expression ( "++" | "--" ) .

下面的赋值语句在语义上是等价的:

自增自减语句          赋值
x++                 x += 1
x--                 x -= 1

赋值

Assignment = ExpressionList assign_op ExpressionList .
assign_op = [ add_op | mul_op ] "=" .

左边的每个操作数都必须是可寻址的或是一个map的下标表达式,或是空标识符。操作数可以带括号。

x = 1
*p = f()
a[i] = 23
(k) = <-ch  // 如同: k = <-ch

如果x是一个二元算术运算,那么赋值运算xop=y就等价于x=xopy,其中x只会计算一次。运算符op=是一个整体。在该赋值运算中,运算符的左侧和右侧都必须只是单值表达式。

a[i] <<= 2
i &^= 1<<n

一个元组赋值是将一个多值赋值给多个变量,其中有两种形式。第一种,运算符的右侧是一个多值表达式,譬如函数调用或是管道或是map或是类型断言。这样,运算符左侧变量的数量必须匹配右侧值的数量。举个例子,如果f返回两个值,

x, y = f()

把第一个值赋值给x,把第二个赋值给y空标识符提供了忽略多值表达式某一个值的方式:

x, _ = f()  // 忽略 f() 的第二个返回值

第二种形式,运算符左侧的数量应该等于右侧表达式的数量,每一个右侧表达式都是单值,这种情况下右侧第n个表达式的值赋值给左侧第n个操作数。

赋值运算分两步。第一步,对于下标表达式指针解析表达式(包括选择子中的隐式指针解析)会以通常的顺序进行求值;第二步,按从左到右的顺序进行赋值。

a, b = b, a  // 交换 a 和 b

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

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

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

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

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

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

在赋值中,每一个值对于要赋值的操作数来说,应该是可赋值的。如果一个无类型的constant赋值给一个变量或是一个接口类型,那么常量会转换boolruneintfloat64complex128 or string类型,具体取决于它本身是布尔、rune、整数、浮点数、复数还是字符串常量。

if语句

if语句会根据一个布尔表达式的结果进行条件执行。如果布尔表达式结果为true,那么执行if分支,否则,有else的话就执行else分支。

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

表达式可以前置一个简单语句,该语句在表达式之前进行计算。

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

switch语句

switch语句是多路分支执行。将一个表达式或是类型跟一系列case进行比较,匹配的分支会被执行。

SwitchStmt = ExprSwitchStmt | TypeSwitchStmt .

switch有两种形式:一种是表达式switch,一种是类型switch。在表达式switch中,case会指定一个表达式与条件表达式进行比较。在类型switch中,case会指定一个类型与条件类型进行比较。在switch语句中条件表达式的值只会计算一次。

表达式switch

在表达式switch中,条件表达式和case表达式都会求值,不要求是常量,求值是从左到右、从上到下。和条件表达式的值匹配的第一个case会触发关联语句的执行;其他的跳过。如果没有case匹配,但是有default部分,那么执行default部分。default顶多指定一个,位置随意。如果缺少条件表达式,会认为总是true

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

每一个case或是default的表达式的最后一个语句可以是(可以带标记的)fallthrough语句语句,它会把控制流从当前子句转移到下一个子句;否则整个switch语句会结束。fallthrough只能作为表达式的最后一个语句;但是不能是最后一个表达式。

如果条件表达式是一个无类型的常量,那么它会转换成它的默认类型;如果它是无类型布尔值,那么它转换成bool类型。nil不能作为一个条件表达式。

如果case表达式是无类型的,它会它会转换成条件表达式的类型。对于每一个case表达式x和条件表达式tx == t必须是合法的比较运算

换句话说,条件表达式就像是临时声明和初始化的一个变量t,只不过没有显示指定类型;然后t会和每一个case表达式x来比较相等性。

表达式可以前置一个简单语句,该语句在表达式之前进行计算。

switch tag {
default: s3()
case 0, 1, 2, 3: s1()
case 4, 5, 6, 7: s2()
}

switch x := f(); {  // 缺少表达式的认为是 true
case x < 0: return -x
default: return x
}

switch {
case x < y: f1()
case x < z: f2()
case x == 4: f3()
}

实现限制:一个编译器可能不允许多路case的表达式中出现相同的常量。譬如,当前的编译器就不允许case表达式中有重复的整数、浮点数或是字符串常量。

类型switch

类型switch比较的是类型,而不是值。它和表达式switch很像,只不过它用的是一个特殊的类型断言作为条件表达式,其中用了保留字type而不是一个真实的类型。

switch x.(type) {
// cases
}

case是用真是的类型T去和表达式x的动态类型进行比较。和类型断言一样,x必须是接口类型T必须是费接口类型且实现了接口x

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

类型表达式(就是TypeSwitchGuard)可以包含短变量声明。如果使用这种形式,变量会在每一个case的开始处声明变量;如果case只是指定了一个类型,那么变量就是这种类型,否则就是类型表达式中表达式的类型。

case的类型可能为nil;当类型表达式中表达式是nil值的时候匹配这种case。

给一个interface{}类型的x,下面的类型switch:

switch i := x.(type) {
case nil:
  printString("x is nil")
case int:
  printInt(i)  // i 是 int
case float64:
  printFloat64(i)  // i 是 float64
case func(int) float64:
  printFunction(i)  // i 是函数
case bool, string:
  printString("type is bool or string")  // i 是 bool 或是 string
default:
  printString("don't know the type")
}

上面还可以如下改写:

v := x  // x 只会求值一次
if v == nil {
  printString("x is nil")
} else if i, isInt := v.(int); isInt {
  printInt(i)  // i 是 int
} else if i, isFloat64 := v.(float64); isFloat64 {
  printFloat64(i)  // i 是 float64
} else if i, isFunc := v.(func(int) float64); isFunc {
  printFunction(i)  // i 是函数
} else {
  i1, isBool := v.(bool)
  i2, isString := v.(string)
  if isBool || isString {
    i := v
    printString("type is bool or string")  // i 是 bool 或是 string
  } else {
    i := v
    printString("don't know the type")  // i 是 interface{}
  }
}

类型表达式可以前置一个简单语句,该语句在表达式之前进行计算。

在类型分支中是不允许存在fallthrough语句。

for 语句

for语句来指定需要重复执行的块。迭代在一个条件,或是for子句,或是rang子句的控制之下。

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

有一个简单版本,一个 for 只指定一个块重复执行的条件。每一次迭代的时候会计算表达式的值。如果表达式为空,那么等价于true

for a < b {
  a *= 2
}

带一个ForClause的for语句也会在条件的控制之下,但是会有额外的initpost语句,一般是赋值、增加或是减少语句。init语句可以使用短变量声明,而 post 语句不行。

ForClause = [ InitStmt ] ";" [ Condition ] ";" [ PostStmt ] .
InitStmt = SimpleStmt .
PostStmt = SimpleStmt .
for i := 0; i < 10; i++ {
  f(i)
}

如果非空,那么 init 语句只会在第一次计算条件之前执行一次;post 语句会在块中的每一次执行完毕之后执行。ForClause 中的每一部分都可以为空,但是分号必须保留,除非只有一个条件。如果条件也省略的话,那就认为是true

for cond { S() }    is the same as    for ; cond ; { S() }
for      { S() }    is the same as    for true     { S() }

带rang自己的for语句可以迭代数组、分片、字符串或是map的所有元素,或是一个管道中的接受到的值。对于每一个元素,它将迭代的值赋值给迭代的变量,然后执行语句块。

RangeClause = Expression [ "," Expression ] ( "=" | ":=" ) "range" Expression .

range子句的右侧部分叫做范围表达式,它可以是一个数组、数组指针、分片、字符串、map和管道等。因为要赋值,所有左侧的操作数必须是可寻址的,或是map下标表达式,它们就是说的迭代变量。如果迭代变量是一个管道,只允许有一个迭代变量,初次之外可以有一个或是两个。如果第二个迭代变量是空标识符,那么range子句等价于只有一个变量出现的子句。

范围表达式只在循环开始执行之前计算一次,除非表达式是一个数组,这种情况下要依赖于表达式,它可能不需要计算(看下面)。每一次迭代左侧的函数调用会计算一次。对于每一次迭代,迭代值如下生成:

范围表达式                         第一个值         第二个值 (如果存在的话)

array or slice  a  [n]E, *[n]E, or []E    index    i  int    a[i]       E
string          s  string type            index    i  int    see below  rune
map             m  map[K]V                key      k  K      m[k]       V
channel         c  chan E                 element  e  E
  1. 对于数组、数组指针或是分片值a来说,下标迭代值已升序生成,从0开始。有一种特殊场景,只有当第一个迭代参数存在的情况下,range循环生成0到len(a)的迭代值,而不是索引到数组或是分片。对于一个nil分片,迭代的数量为0。
  2. 对于字符串类型,range子句迭代字符串中每一个Unicode代码点,从下标0开始。在连续迭代中,下标值会是下一个utf-8代码点的第一个字节的下标,而第二个值类型是rune,会是对应的代码点。如果迭代遇到了一个非法的Unicode序列,那么第二个值是0xFFFD,也就是Unicode的替换字符,然后下一次迭代只会前进一个字节。
  3. map中的迭代顺序是没有指定的,也不保证两次迭代是一样的。如果map元素在迭代过程中被删掉了,那么对应的迭代值不会再产生。如果map元素在迭代中插入了,这种行为是依赖于实现的,但是每个元素的迭代值顶多出现一次。如果map是nil,那么迭代次数为0。
  4. 对于管道,迭代值就是下一个send到管道中的值,除非管道被关闭了。如果管道是nil,范围表达式永远阻塞。

迭代值会赋值给相应的迭代变量,就像是赋值语句

迭代变量可以使用短变量声明(:=)。这种情况,它们的类型设置为相应迭代值的类型,它们的是到for语句的结尾,它们可以在每一次迭代中复用。如果迭代变量是在for语句外声明的,那么执行之后它们的值是最后一次迭代的值。

var testdata *struct {
  a *[7]int
}
for i, _ := range testdata.a {
  // testdata.a is never evaluated; len(testdata.a) is constant
  // i ranges from 0 to 6
  f(i)
}

var a [10]string
m := map[string]int{"mon":0, "tue":1, "wed":2, "thu":3, "fri":4, "sat":5, "sun":6}
for i, s := range a {
  // type of i is int
  // type of s is string
  // s == a[i]
  g(i, s)
}

var key string
var val interface {}  // value type of m is assignable to val
for key, val = range m {
  h(key, val)
}
// key == last map key encountered in iteration
// val == map[key]

var ch chan Work = producer()
for w := range ch {
  doWork(w)
}

go语句

go语句在一个独立的线程中执行一个函数或是方法调用;这个线程或是叫做goroutine,它和原来的线程在同一地址空间中。

GoStmt = "go" Expression .

表达式必须是能够调用的。在调用的go例程中函数值和参数都会被正常求值;不过不像常规的函数调用,这个go例程不会等待调用函数的结束;相反的,函数在一个新的例程中开始执行。在函数终止的时候,这个go例程也就终止了;如果函数有返回值,那它们会被忽略。

go Server()
go func(ch chan<- bool) { for { sleep(10); ch <- true; }} (c)

select语句

select语句从多个发送接收操作中选择一个执行。它看上去像是switch语句,只不过它的每一个case都是通信操作。

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

RecvStmt的case可以使用短变量声明将RecvExpr赋值给一个或是两个变量。RecvExpr必须是一个接收操作。顶多有一个default case,它的位置随意。

select语句的执行按照下面的步骤:

  1. select语句中的所有case,接收操作的操作数、管道、发送语句右侧的表达式等都只计算一次,按源文件指定的顺序。
  2. 如果有多个case可以进行下去,那么会使用一致随机算法选择一个去执行。否则,如果有default case,那么就执行default;如果没有default,那么select语句会一直阻塞直到有一个通信可以进行;
  3. 除非选择的是default case,否则就是执行相应的通信操作;
  4. 如果选中的case是一个接收语句并使用了短变量声明或是赋值,那么会把右侧接收的值赋值给左侧的表达式;
  5. 选中的case的语句会被执行;

因为对nil管道通信永远进行不下去,所以select nil管道又没有default case,那么会永远阻塞。

var c, c1, c2, c3 chan int
var i1, i2 int
select {
case i1 = <-c1:
  print("received ", i1, " from c1\n")
case c2 <- i2:
  print("sent ", i2, " to c2\n")
case i3, ok := (<-c3):  // same as: i3, ok := <-c3
  if ok {
    print("received ", i3, " from c3\n")
  } else {
    print("c3 is closed\n")
  }
default:
  print("no communication\n")
}

for {  // send random sequence of bits to c
  select {
  case c <- 0:  // note: 没有语句, 没有 fallthrough, 没有 case 折叠
  case c <- 1:
  }
}

select {}  // 用于阻塞

return 语句

return语句终止当前函数的执行,然后给调用方返回一个结果值(也许没有)。

ReturnStmt = "return" [ ExpressionList ] .

如果一个函数没有返回类型,那么return语句就不能指定返回结果。

func noResult() {
  return
}

从一个函数返回一个结果,有三种方式:

  1. 返回值可以显式地列在return语句中。每一个表达式都必须是单值的,而且可赋值给函数的返回值类型。
    func simpleF() int {
      return 2
    }
    
    func complexF1() (re float64, im float64) {
      return -7.0, -4.0
    }
    
  2. return语句可以是对一个多值函数的调用。这种情况就像是函数的每一个返回值赋值给一个临时变量,然后跟着一个return语句;之后就是前面一个case的场景;
    func complexF2() (re float64, im float64) {
      return complexF1()
    }
    
  3. 表达式列表可以为空,如果函数的返回值已经为结果参数指定了名字(§Function Types)。返回参数就像是普通的局部变量,函数中可以给这些参数赋值。return语句返回的是这些参数的值。
    func complexF3() (re float64, im float64) {
      re = 7.0
      im = 4.0
      return
    }
    
    func (devnull) Write(p []byte) (n int, _ error) {
      n = len(p)
      return
    }
    

不管它们到底是怎么声明的,所有的返回值结果都是初始化为(§0值)。

break语句

break语句结束for的最内层循环、switch、select的执行。

BreakStmt = "break" [ Label ] .

如果有一个标号,它必须是在 for/switch/select 语句的周围, 那是执行结束的地方(§for语句, §switch语句, §select语句)。

L:
  for i < n {
    switch i {
    case 5:
      break L
    }
  }

continue 语句

continue语句开始for语句最内层循环的下一次迭代。

ContinueStmt = "continue" [ Label ] .

如果有一个标号,它必须是是在for语句的周围。

goto 语句

goto语句会把语句控制跳转到对应的标号处。

GotoStmt = "goto" Label .
goto Error

goto语句的跳转不应该造成任何变量的作用域的变化。譬如下面的例子:

  goto L  // BAD
  v := 3
L:

是错误的,因为跳转到L会跳过v的创建。

一个外的goto语句不能跳到块的内部。譬如:

if n%2 == 1 {
  goto L1
}
for n > 0 {
  f()
  n--
L1:
  f()
  n--
}

是错误的,因为标号L1是在 "for" 语句块内,而goto不是。

fallthrough 语句

fallthrough语句会把 switch 语句(§switch)的第一条语句的控制传递给下一条语句。它也可能用在 switch 的最后一个非空的 case 语句,或是 default 语句。

FallthroughStmt = "fallthrough" .

defer 语句

defer语句在所在的函数结束后调用。

DeferStmt = "defer" Expression .

表达式必须是函数或是方法调用。每当 defer 语句要执行的时候,函数值和参数会正常求值并保存,虽然函数并没有调用。defer 函数会以 LIFO 的顺序在函数结束的时候进行调用,这个过程在函数返回给调用者之前。如果 defer 函数是一个函数字面量,外围的函数有命名的返回值参数 ,那么 defer 函数可能会访问和修改函数的返回值。如果 defer 函数有任何返回值,它们都会在函数执行结束之后忽略。

lock(l)
defer unlock(l)  // 在外层函数返回之前解锁

// 在外层函数返回之前打印 3 2 1 0
for i := 0; i <= 3; i++ {
  defer fmt.Print(i)
}

// f 返回 1
func f() (result int) {
  defer func() {
    result++
  }()
  return 0
}

内置函数

内置函数都是预声明的。它们可以像其他函数一样被调用,只不过有些函数的第一个参数是一个类型而不是一个值。

内置函数并没有标准的类型,所以它们只可以出现在调用表达式中,而不能当函数值。

BuiltinCall = identifier "(" [ BuiltinArgs [ "," ] ] ")" .
BuiltinArgs = Type [ "," ExpressionList ] | ExpressionList .

关闭

对一个管道c来说,内置函数close(c)说明不再往管道中发送数据。如果c只是个接收管道,这是错误的。向一个关闭的管道发送数据或是再次关闭都会引起一个运行时异常。关闭nil管道同样引起运行时异常。调用close,并且先前发送的数据全部接收完毕之后,接收操作会根据管道的类型返回一个0值,但不会引起阻塞。使用多值接收操作可以得到一个测试管道是否关闭的标志。

长度和容量

内置函数lencap接收多种类型作为参数,返回一个int类型的值。实现保证返回的结果可以适合int

调用      参数类型          结果

len(s)    string           字符串的字节长度
          [n]T, *[n]T     数组长度 (== n)
          []T              分片长度
          map[K]T          map长度(key的数量)
          chan T           管道缓冲区中排队元素的数量

cap(s)    [n]T, *[n]T     数组长度 (== n)
          []T              分片容量
          chan T           管道缓冲区容量

一个分片的容量就是它底层的数组为它提供的元素个数。任何时候都必须满足一下关系:

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

nil分片、map或是管道的长度和容量都是0。

s是一个字符串常量的时候,len(s)表达式也是常量。只要s的类型是数组类型或是指向数组的指针类型,并且s表达式不包括管道接收函数调用操作,那么len(s)cap(s)都是常量,并不用去计算s的值。而其他情况下,对lencap的调用就不是常量,需要计算s而得。

分配空间

内置函数new接受一个类型参数T然后返回*T类型的一个值。存储空间会按(§0值)处那里说明的对值进行初始化。

new(T)

举个例子:

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

会动态地为S类型的变量分配空间,将值初始化为(a=0b=0.0),然后返回一个*S的值,这个值是分配空间的地址。

创建分片、map和管道

分片、map和管道都是引用类型,所以不需要使用new来分配间址访问的空间。内置函数make带有一个类型T,必须是分片、map或是管道类型,后面跟着可选的跟类型有关的表达式。它返回的值的类型是T(而不是*T)。存储空间也会按(§0值)那里的说明对值进行初始化。

调用             类型T      结果

make(T, n)        分片       长度可容量都是n的分片
make(T, n, m)    分片       长度是n容量是m的分片

make(T)           map         T类型的 map
make(T, n)       map         T类型的 map,有n个经过初始化的元素

make(T)           管道     T类型的同步管道
make(T, n)       管道     带有长度为n的缓冲区的T类型的异步管道

参数nm必须是整型类型。如果n是个负数或是比m还大,亦或是n或是m不能用int表示,那么会有一个运行时异常出现。

s := make([]int, 10, 100)       //  len(s) == 10, cap(s) == 100 的分片
s := make([]int, 10)            // len(s) == cap(s) == 10 的分片
c := make(chan int, 10)         // 缓冲区长度为 10 的管道
m := make(map[string]int, 100)  // 有 100 个初始化元素的 map

分片的追加以及拷贝

内置函数 append 和 copy 是两个常用的分片操作。这两个函数的结果不依赖于数据是否有重叠。

可变函数append向分片类型S的值s追加0个或是多个值x,然后返回结果分片,类型也是Sx的值会传给参数...T,其中TS元素类型,运用参数传递规则。有一种特殊的情况,append第一个参数是[]byte类型,第二个参数是 string 类型,后面跟着...,这种形式下追加的是 string 的字节。

append(s S, x ...T) S  // T是S的元素类型

如果s的容量不是足够大以至于不能容下要增加的值,那么append会分配新的充分大的底层数组来容纳现有的分片元素以及要追加的元素。否则 append 重用底层数组。

s0 := []int{0, 0}
s1 := append(s0, 2)                // 追加单个元素     s1 == []int{0, 0, 2}
s2 := append(s1, 3, 5, 7)          // 追加多个元素    s2 == []int{0, 0, 2, 3, 5, 7}
s3 := append(s2, s0...)            // 追加一个分片             s3 == []int{0, 0, 2, 3, 5, 7, 0, 0}
s4 := append(s3[3:6], s3[2:]...)   // 追加重叠分片    s4 == []int{3, 5, 7, 2, 3, 5, 7, 0, 0}

var t []interface{}
t = append(t, 42, 3.1415, "foo")   //                             t == []interface{}{42, 3.1415, "foo"}

var b []byte
b = append(b, "bar"...)            // 追加字符串      b == []byte{'b', 'a', 'r' }

copy函数从src拷贝分片元素到dst中,然后返回拷贝的元素数量。两者参数必须具有一致的元素类型T,并且对于[]T可赋值的。拷贝的元素数量是 min{len(src), len(dst).}。一种特殊情况是,copy第一个参数是[]byte,第二个参数是 string 类型,这种情况是把字符串的字节。

copy(dst, src []T) int
copy(dst []byte, src string) int

例子:

var a = [...]int{0, 1, 2, 3, 4, 5, 6, 7}
var s = make([]int, 6)
var b = make([]byte, 5)
n1 := copy(s, a[0:])            // n1 == 6, s == []int{0, 1, 2, 3, 4, 5}
n2 := copy(s, s[2:])            // n2 == 4, s == []int{2, 3, 4, 5, 4, 5}
n3 := copy(b, "Hello, World!")  // n3 == 5, b == []byte("Hello")

map元素的删除

内置函数delete可以从mapm中删除键值为k的元素,而k的类型对于m的key类型来说必须是可赋值的

delete(m, k)  // 从 m 中移除 m[k]

如果元素m[k]不存在的话, delete不执行其他操作;如果对 nil 进行delete调用引起一个运行时异常

复数操作

有三个函数用来操作复数。内置函数complex根据一个浮点型的实部和虚部构造一个复数,函数realimag分别获取一个复数的实部和虚部。

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

参数的类型和返回值类型是匹配的。对于complex来说,两个参数需要是相同的浮点数类型,返回值是对应浮点数类型的复数类型:complex64对应float32complex128对应code>float64。real and imag函数正好相反。总之:复数 z ==complex(real(z),imag(z))

这几个函数的操作数是常量的话,那么返回值就是常量。

var a = complex(2, -2)             // complex128
var b = complex(1.0, -1.4)         // complex128
x := float32(math.Cos(math.Pi/2))  // float32
var c64 = complex(5, -x)           // complex64
var im = imag(b)                   // float64
var rl = real(c64)                 // float32

异常处理

有两个内置函数panicrecover用来抛出或是处理运行时的异常和一些程序定义的错误。

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

当一个函数F调用panic,那么F的正常执行会立刻终止。那些defer函数会以通常的方式执行完,然后F返回给它的调用方。对于F来说,它的行为就像是自己调用了panic。然后会一直执行直到本goroutine停止执行。之后,程序会终止掉,报告一个错误条件,包括传递给panic的参数。终止的序列我们叫 panicking

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

recover函数允许程序去管理 panicking goroutine。执行recover调用,panicking序列会被复原成正常执行流,而且能够获取到传给你panic的参数。如果recover是在defer函数外侧进行调用的,那么它不会停止panicking序列。这种情况,或是goroutine不在panicking,或是传递给panaic的参数是nil,那么recover返回nil

下面例子中的protect函数调用了参数g,然后会保护调用者protect出现运行时异常。

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

引导

当前的实现提供了几个内置的有用的引导函数。为了完整性,我们在这里也对它们进行说明,然而不会保证语言中一直存在。它们不返回结果。

函数        行为

print      输出所有的参数;参数的格式化是跟实现有关的;
println    跟 print 函数类型,不过这里在参数之间加上空白以及在结束的时候添加换行;

go程序是由链接在一起的构成的。每一个包则是由一个或是多个源文件构建起来的,源文件中包含常量、类型、变量、函数声明以及一些其他属于包的可以包内访问的东西。这些元素可以导出,然后为另外一个包所使用。

源文件组织

每一个源文件由若干部分构成,首先是一个定义了该文件所属的包的子句;其次是一系列的可以为空的包的导入声明,声明希望使用的包;紧接着可以是一些函数、类型、变量或是常量声明,不过也可以么有。

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

包子句

每个源文件开始于一个包子句,该子句定义了该文件属于的包。

PackageClause  = "package" PackageName .
PackageName    = identifier .

包名不能是空白标识符

package math

多个文件可能共享同一个包名,这时候它们构成包的一个实现。一个包的实现应满足所有的源文件位于相同的目录下。

导入声明

一个导入声明指明依赖功能的imported的包(程序的初始化和执行)的源文件,之后便可以使用包中导出的标识符。导入的名字(包名)会用于访问,导入的路径ImportPath指定的要导入的包。

ImportDecl       = "import" ( ImportSpec | "(" { ImportSpec ";" } ")" ) .
ImportSpec       = [ "." | PackageName ] ImportPath .
ImportPath       = string_lit .

限定标识符中的包名用来限制源文件所在的包中导出的标识符。它是声明在文件块作用域。如果包名省略,那么默认使用包子句中的标识符。如果显示地用一个点号(.)作为名称,那么包所有导出的包块一级的标识符会声明在导入文件的文件块域内,访问的时候不需要再加限定符。

ImportPath的形式依赖于实现,但是它通常是编译包的文件全路径的子串,也可以是相对于包安装位置的路径。

实现要求:一个编译器可以要求ImportPaths是一个非空串,而且只能包含UnicodeL、M、N、P和S分类的字符串,也可以是不包括!"#$%&'()*,:;<=>?[\]^`{|}和Unicode替换字符串U+FFFD。

假定我们有一个路径"lib/math"math包,它导出了Sin,那么Sin可以如此访问:

导入声明          Sin的局部名称

import   "lib/math"         math.Sin
import M "lib/math"         M.Sin
import . "lib/math"         Sin

导入声明建立了一个包和导入包的依赖关系。一个包导入它本身,或是没有用任何导入包的东西,是不允许的。如果导入一个包只是为了其初始化的效果,可以使用 标识符作为导入的包名:

import _ "lib/math"

一个包的例子

下面是一个实现了并发的素数筛的完整的 Go 包:

package main

import "fmt"

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

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

// The prime sieve: Daisy-chain filter processes together.
func sieve() {
  ch := make(chan int)  // Create a new channel.
  go generate(ch)       // Start generate() as a subprocess.
  for {
    prime := <-ch
    fmt.Print(prime, "\n")
    ch1 := make(chan int)
    go filter(ch, ch1, prime)
    ch = ch1
  }
}

func main() {
  sieve()
}

程序初始化和执行

0 值

不管是通过声明,还是make或是new,只要为了保存一个值创造了空间但是却没有显式地初始化,那么这些空间都有默认值。 这样的值的每一个元素都会根据它的类型 被0 值化:对布尔类型值是false,对整数值是0,对浮点数值是0.0,对字符串是"",其他剩下的nil 的指针、函数、 接口、分片、管道和映射等都是nil。而且这个初始化是递归进行的,所以说,如果是个结构体数组的话,那么,里面的每一个元素都会被 0 值,只要它没有被指定。

下面的两个简单声明等价:

var i int
var i int = 0

看下面

type T struct { i int; f float64; next *T }
t := new(T)

接下来有结果:

t.i == 0
t.f == 0.0
t.next == nil

当然,如果是下面还是一样:

var t T

程序执行

一个没有任何import的包,会初始化所有包一级的变量,然后在函数

func init()
中对于包一级的函数进行调用。

一个包可能有多个init函数,也可以是在同一个文件中。这种情况下,它们的调用顺序是未指定的。

在一个包内,包一级的变量会被初始化,常量会被求值,按照它们的依赖关系:如果A的初始化依赖于B的值,那么A会在B之后设定。循环依赖是一种错误。对于依赖的分析完全是词法一级的:当A用到了B,那么A就依赖B,其中包括在初始化中用到了,或是函数调用用到了。如果两个变量是互相独立的,那么按照它们在源文件中的顺序进行初始化。依赖分析是针对每个包进行的,如果A的初始化用到了另外一个包中的B,那么结果是未指定的。

init函数不能再程序中的任何地方进行应用,也就是说它不能显示调用或是把它赋值给一个函数变量。

如果一个包有import,那么导入包的初始化会在包的初始化之前完成。如果一个包P被多次import,它也只会被初始化一次。

导入的包会进行处理,保证不会出现初始化的循环依赖。

一个完整的程序需要链接唯一的不需要导入的包。main的报名必须是main,同时必须要有一个main函数,不带任何参数,也不返回任何值。

func main() { … }

程序通过初始化main包,然后调用main函数开始执行。当main结束的时候,程序便结束了,不管有没有其他的(非main) goroutines 是否完成。

包的初始化—变量的初始化,以及init的调用—是在单个 goroutine 中顺序进行执行的,一次一个包。init函数可以回生成其他的 goroutines,它们和初始化的代码是并发执行的。但是init函数的执行总是按顺序执行的: 一个init未执行完不会去执行另外一个init函数。

错误

预声明的类型error定义如下:

type error interface {
  Error() string
}

对于表示一个错误条件来说,这是个灰常方便的接口,如果没有错误的话就是 nil。比如说,一个从文件中读数据的函数可能是这样定义的:

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

运行时问题

在程序执行的过程中,如果出现访问数组越界这些错误就会触发一个运行时问题。不过也可以通过调用内置函数panic来实现, 这个函数带有一个实现定义的接口类型runtime.Error的值;这个值只要满足预声明的接口类型error就好。 而实际的表示不同运行问题信息的值则不做具体指定。

package runtime

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

系统考量

unsafe

内置的面向编译器的unsafe包,提供了更底层的一些功能和操作,不同的系统可能不一样。使用unsafe的地方要非常小心。unsafe包提供下面几个接口:

package unsafe

type ArbitraryType int  // shorthand for an arbitrary Go type; it is not a real type
type Pointer *ArbitraryType

func Alignof(variable ArbitraryType) uintptr
func Offsetof(selector ArbitraryType) uintptr
func Sizeof(variable ArbitraryType) uintptr

指针或是底层类型 uintptr的值可以和Pointer进行互相转换。

函数Sizeof会计算变量的字节数。

函数Offsetof计算(§选择子)的某个结构体字段相对于结构体地址的偏移字节数。譬如针对结构体s的字段f

uintptr(unsafe.Pointer(&s)) + unsafe.Offsetof(s.f) == uintptr(unsafe.Pointer(&s.f))

计算机体系结构要求内存寻址要进行对齐,也就是说某个变量的地址必须是某个对齐因子的倍数。函数Alignof会计算一个变量需要对齐的字节数。譬如对于变量x来说:

uintptr(unsafe.Pointer(&x)) % unsafe.Alignof(x) == 0

AlignofOffsetofSizeof的计算结果都是编译期的uintptr类型的常量。

大小以及对齐保证

对于(§数值类型)来说,下面的大小必须保证:

type                                 size in bytes

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

下面的最小对齐属性也必须保证:

  1. 对于任意类型的变量xunsafe.Alignof(x)至少是 1。
  2. 对于结构类型的变量xunsafe.Alignof(x)x中的所有字段funsafe.Alignof(x.f)的最大值,至少是 1 。
  3. 对于数组类型xunsafe.Alignof(x)unsafe.Alignof(x[0])是一样的, 至少是 1 。

一个结构体或是数组类型,如果不含有尺寸大于0 的任何字段(或是说元素),那么这种类型的大小是 0 。两个不同的 0 尺寸的变量可能在内存中会有相同的地址。