基本语法
- 1: 语法元素
- 2: Golang 基础语法
- 3: 声明
- 4: 作用域
- 4.1: 标签
- 5: 类型
- 5.1: 概述
- 5.2: 布尔类型
- 5.3: 数字类型
- 5.4: string类型
- 5.5: 数组类型
- 5.6: Slice类型
- 5.7: 结构体类型
- 5.8: 指针类型
- 5.9: 函数类型
- 5.10: 接口类型
- 5.11: Map类型
- 5.12: Channel类型
- 6: 表达式
- 7: 语句
- 8: 控制流程
- 8.1: if 语句
- 8.2: switch 语句
- 8.3: for 语句
- 8.4: for range 语句
- 8.5: range 语句
- 8.6: go 语句
- 8.7: select 语句
- 8.8: return 语句
- 8.9: break 语句
- 8.10: continue 语句
- 8.11: goto 语句
- 8.12: fallthrough 语句
- 8.13: defer 语句
1 - 语法元素
1.1 - 注释
Go语言实战
摘录自 Go语言实战 3.5.3 Go 语言的文档一节
为了在 godoc 生成的文档里包含自己的代码文档,开发人员需要用下面的规则来写代码和注释。
在标识符之前,把文档作为注释加入到代码中,这个方式对包、函数、类型和全局变量都适用。注释可以以双斜线开头,也可以用斜线和星号风格:
// Retrieve 连接到配置库,收集各种链接设置、用户名和密码。这个函数在成功时
// 返回 config 结构,否则返回一个错误。
func Retrieve() (config, error) {
// ...省略
}
如果想给包写一段文字量比较大的文档,可以在工程里包含一个叫作 doc.go 的文件,使用同样的包名,并把包的介绍使用注释加在包名声明之前:
/*
包 usb 提供了用于调用 USB 设备的类型和函数。想要与 USB 设备创建一个新链接,使用 NewConnection
...
*/
package usb
这段关于包的文档会显示在所有类型和函数文档之前。这个例子也展示了如何使用斜线和星 号做注释。
Golang语言规范
https://golang.org/ref/spec#Comments
注释作为程序文档。有两种形式:
- 行注释以字符序列
//
开始,止于行末。 - 一般注释以字符序列
/*
开始,并以随后的第一个字符序列*/
停止。
注释不能在符文或字符串字面值内部开始,也不能在注释内部开始。一个不包含换行符的一般注释就像一个空格。任何其他注释的作用就像一个换行符。
Effective Go
https://golang.org/doc/effective_go.html#commentary
Go 提供了 C风格 /* */
块注释和 C++风格的 //
行注释。行注释是标准的;块注释主要是作为包注释出现的,但在表达式中或禁用大片代码时很有用。
godoc(程序和 web 服务器) 处理 Go 源文件,以提取有关包内容的文档。出现在顶层声明之前的注释,没有中间的换行符,与声明一起被提取出来,作为该项目的解释文本。这些注释的性质和风格决定了 godoc 产生的文档的质量。
每个包都应该有一个包注释,即在包子句之前的块注释。对于多文件的包,包注释只需要出现在一个文件中,任何一个文件都可以。包的注释应该介绍包,并提供与包整体相关的信息。它将首先出现在 godoc 页面上,并且应该构建后面的详细文档。
/*
Package regexp implements a simple library for regular expressions.
The syntax of the regular expressions accepted is:
regexp:
concatenation { '|' concatenation }
concatenation:
{ closure }
closure:
term [ '*' | '+' | '?' ]
term:
'^'
'$'
'.'
character
'[' [ '^' ] character-ranges ']'
'(' regexp ')'
*/
package regexp
如果包的内容简单,包的注释可以简短一些。
// Package path implements utility routines for
// manipulating slash-separated filename paths.
注释不需要额外的格式化,比如星星的横幅。生成的输出可能甚至不会以固定宽度的字体呈现,所以不要依赖对齐的间距–godoc,像gofmt一样,会照顾到这一点。注释是未解释的纯文本,所以 HTML 和其他注释(如 _this_
)会逐字重现,不应使用。godoc 做的一个调整是用固定宽度的字体显示缩进的文本,适合程序片段。fmt包的注释就很好地利用了这一点。
根据上下文的不同,godoc甚至可能不会重新格式化注释,所以确保它们直接看起来不错:使用正确的拼写、标点和句子结构,折叠长行,等等。
在一个包内,任何紧接在顶层声明之前的注释都会作为该声明的 doc 注释。程序中的每一个导出(大写)的名字都应该有一个 doc 注释。
doc注释最好是作为完整的句子来使用,这样就可以有各种各样的自动呈现方式。第一句话应该是一句话的总结,以被声明的名称开始。
// Compile parses a regular expression and returns, if successful,
// a Regexp that can be used to match against text.
func Compile(str string) (*Regexp, error) {
如果每个doc注释都以它所描述的物品名称开头,你可以使用go工具的doc子命令,通过grep运行输出。想象一下,你记不住 “Compile” 这个名字,但又在寻找正则表达式的解析函数,所以你运行了这个命令:
$ go doc -all regexp | grep -i parse
如果包中所有的 doc 注释都以 “This function…” 开头,grep 就不会帮你记住这个名字。但因为软件包中的每条文档注释都以名称开头,你会看到这样的内容,它能让你想起你要找的单词。
$ go doc -all regexp | grep -i parse
Compile parses a regular expression and returns, if successful, a Regexp
MustCompile is like Compile but panics if the expression cannot be parsed.
parsed. It simplifies safe initialization of global variables holding
$
Go的声明语法允许对声明进行分组。一个文档注释可以介绍一组相关的常量或变量。由于整个声明都会被介绍,所以这样的注释往往可以敷衍了事。
// Error codes returned by failures to parse an expression.
var (
ErrInternal = errors.New("regexp: internal error")
ErrUnmatchedLpar = errors.New("regexp: unmatched '('")
ErrUnmatchedRpar = errors.New("regexp: unmatched ')'")
...
)
分组也可以表示项目之间的关系,比如一组变量是由一个mutex保护的。
var (
countLock sync.Mutex
inputCount uint32
outputCount uint32
errorCount uint32
)
1.2 - Token
备注:摘录自 golang语言规范 https://golang.org/ref/spec#Tokens
Token
Token构成了go语言的词汇。有四类Token:identifiers(标识符)、keywords(关键字)、operators and punctuation(运算符和标点符号)、以及literals(字面量)。
由空格(U+0020)、水平制表符(U+0009)、回车符(U+000D)和换行符(U+000A)形成的空白(whitespace)会被忽略,除非它是用来分隔Token,避免多个Token合并成一个Token。
此外,换行或文件末尾可能会触发分号的插入。
在将输入内容分解成Token的同时,下一个Token是形成有效Token的最长字符序列。
分号
在一些产品中正式语法使用分号"; “作为终结符。Go程序可以使用以下两条规则来省略大部分的分号:
-
当输入的内容被分解成Token时,分号会自动插入到Token流中,紧接在一行的最后一个Token之后,如果该Token是… …
- identifier/标识符
- 整型,浮点,虚数,rune(符文)或者 字符串字面值
- break、continue、fallthrough、return等关键字之一
- 运算符和标点符号
++, --, ), ], 或 }
中的一种
-
为了让复杂的语句只占一行,可以在结尾”) “或”}“前省略分号。
1.3 - 标识符
Identifiers/标识符
https://golang.org/ref/spec#Identifiers
identifiers/标识符用于命名程序实体,如变量和类型。标识符是由一个或多个字母和数字组成的序列。标识符的第一个字符必须是字母(不能是数字开头)。
identifier = letter { letter | unicode_digit } .
a
_x9
ThisVariableIsExported
αβ
空白标识符
https://golang.org/ref/spec#Blank_identifier
空白标识符由下划线字符 _
表示。它作为匿名的占位符,而不是常规的(非空白)标识符,在声明、操作数和赋值中具有特殊意义。
预定义的标识符
universe block(宇宙块)中隐式声明了以下标识符:
Types:
bool byte complex64 complex128 error float32 float64
int int8 int16 int32 int64 rune string
uint uint8 uint16 uint32 uint64 uintptr
Constants:
true false iota
Zero value:
nil
Functions:
append cap close complex copy delete imag len
make new panic print println real recover
导出的标识符
标识符可以导出,以便从另一个包中访问它。标识符在以下两种情况下被导出:
- 标识符名称的第一个字符是Unicode大写字母(Unicode class “Lu”);和
- 标识符是在package block(包块)中声明的,或者是字段名或方法名。
所有其他的标识符都不会被导出。
标识符的唯一性
给定一组标识符,如果一个标识符与集合中的其他标识符不同,则称为唯一标识符。如果两个标识符的拼写不同,或者它们出现在不同的包中并且没有被导出,那么它们就是不同的。否则,它们是相同的。
1.4 - 关键字
Keywords/关键字
https://golang.org/ref/spec#Keywords
以下是保留的关键词,不得作为标识符使用。
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
1.5 - 运算符
Operators and punctuation/运算符和标点符号
https://golang.org/ref/spec#Operators_and_punctuation
以下字符序列代表运算符(包括赋值运算符)和标点符号。
+ & += &= && == != ( )
- | -= |= || < <= [ ]
* ^ *= ^= <- > >= { }
/ << /= <<= ++ = := , ;
% >> %= >>= -- ! ... . :
&^ &^=
1.6 - 字面量
备注:摘录自 golang语言规范 https://golang.org/ref/spec#Integer_literals
Integer literals/整型字面量
整数字面量是代表整数常数的数字序列。可选的前缀设置了一个非十进制的基数:0b或
0B代表二进制,0,
0o, 或
0O代表八进制,0x或
0X代表十六进制。单一的0被认为是十进制的0。在十六进制中,字母a到f和A到F代表10到15的数值。
为了便于阅读,下划线字符_可能会出现在基数前缀之后或连续的数字之间;这种下划线不会改变文字的价值。
int_lit = decimal_lit | binary_lit | octal_lit | hex_lit .
decimal_lit = "0" | ( "1" … "9" ) [ [ "_" ] decimal_digits ] .
binary_lit = "0" ( "b" | "B" ) [ "_" ] binary_digits .
octal_lit = "0" [ "o" | "O" ] [ "_" ] octal_digits .
hex_lit = "0" ( "x" | "X" ) [ "_" ] hex_digits .
decimal_digits = decimal_digit { [ "_" ] decimal_digit } .
binary_digits = binary_digit { [ "_" ] binary_digit } .
octal_digits = octal_digit { [ "_" ] octal_digit } .
hex_digits = hex_digit { [ "_" ] hex_digit } .
42
4_2
0600
0_600
0o600
0O600 // 第二个字是大写字母 'O'
0xBadFace
0xBad_Face
0x_67_7a_2f_cc_40_c6
170141183460469231731687303715884105727
170_141183_460469_231731_687303_715884_105727
_42 // 标识符,不是整型字面量
42_ // invalid: _ 必须隔开连续的数字
4__2 // invalid: 一次只能有一个_
0_xBadFace // invalid: _ 必须隔开连续的数字
Floating-point literals/浮点字面量
浮点字面量是浮点常数的十进制或十六进制表示。
十进制浮点字面量由整数部分(小数点)、小数点、分数部分(小数点)和指数部分(e或E后面跟着一个可选的符号和小数点)组成。整数部分或小数部分中的其中一个可以省略;小数点或指数部分中的其中一个可以省略。指数值exp将mantissa(整数和小数部分)的比例为10exp。
十六进制浮点文字由0x或0X前缀、整数部分(十六进制数字)、小数点、小数部分(十六进制数字)和指数部分(p或P,后面跟着一个可选的符号和十进制数字)组成。整数部分或分数部分中的其中一个可以省略;弧度点也可以省略,但指数部分是必须的。(这个语法与IEEE 754-2008 §5.12.3中给出的语法相匹配。)指数值exp将尾数(整数和分数部分)按2exp缩放。
为了便于阅读,下划线字符_可以出现在基数前缀之后或连续的数字之间;这种下划线不会改变字面值。
float_lit = decimal_float_lit | hex_float_lit .
decimal_float_lit = decimal_digits "." [ decimal_digits ] [ decimal_exponent ] |
decimal_digits decimal_exponent |
"." decimal_digits [ decimal_exponent ] .
decimal_exponent = ( "e" | "E" ) [ "+" | "-" ] decimal_digits .
hex_float_lit = "0" ( "x" | "X" ) hex_mantissa hex_exponent .
hex_mantissa = [ "_" ] hex_digits "." [ hex_digits ] |
[ "_" ] hex_digits |
"." hex_digits .
hex_exponent = ( "p" | "P" ) [ "+" | "-" ] decimal_digits .
0.
72.40
072.40 // == 72.40
2.71828
1.e+0
6.67428e-11
1E6
.25
.12345E+5
1_5. // == 15.0
0.15e+0_2 // == 15.0
0x1p-2 // == 0.25
0x2.p10 // == 2048.0
0x1.Fp+0 // == 1.9375
0X.8p-0 // == 0.5
0X_1FFFP-16 // == 0.1249847412109375
0x15e-2 // == 0x15e - 2 (integer subtraction)
0x.p1 // invalid: mantissa has no digits
1p-2 // invalid: p exponent requires hexadecimal mantissa
0x1.5e-2 // invalid: hexadecimal mantissa requires p exponent
1_.5 // invalid: _ must separate successive digits
1._5 // invalid: _ must separate successive digits
1.5_e1 // invalid: _ must separate successive digits
1.5e_1 // invalid: _ must separate successive digits
1.5e1_ // invalid: _ must separate successive digits
Imaginary literals/虚数字面量
虚数字面量表示复数常数的虚数部分,它由一个整数或浮点字组成,后面是小写字母i。
imaginary_lit = (decimal_digits | int_lit | float_lit) "i" .
为了向后兼容,虚字的整数部分完全由十进制数字(可能还有下划线)组成,即使它以前导0开始,也被认为是一个十进制整数。
0i
0123i // == 123i for backward-compatibility
0o123i // == 0o123 * 1i == 83i
0xabci // == 0xabc * 1i == 2748i
0.i
2.71828i
1.e+0i
6.67428e-11i
1E6i
.25i
.12345E+5i
0x1p-2i // == 0x1p-2 * 1i == 0.25i
Rune literals / 符文字面量
符文字面量代表一个符文常量,是一个用于识别Unicode code point的整数值。符文字面量表示为一个或多个用单引号括起来的字符,如’x’或’\n’。在引号内,除了换行和未转义的单引号外,任何字符都可以出现。单引号字符代表字符本身的Unicode值,而以反斜杠开头的多字符序列则以各种格式编码值。
最简单的形式代表引号内的单个字符;由于go源文本是以UTF-8编码的Unicode字符,所以多个UTF-8编码的字节可以代表单个整数值。例如,字面量 ‘a’ 持有一个字节,代表字面量 a,Unicode U+0061,值0x61,而 “ä “持有两个字节(0xc3 0xa4),代表文字 “a-dieresis”,U+00E4,值0xe4。
几个反斜杠转义符允许任意值被编码为ASCII文本。有四种方法可以将整数值表示为数字常数。\x
后面跟着两位十六进制数字;\u
后面跟着四位十六进制数字;\U
后面跟着八位十六进制数字,以及后面跟着三位八进制数字的反斜杠\
。在每一种情况下,字面量的值都是相应基数的数字所代表的值。
虽然这些表示方法都会产生一个整数,但它们的有效范围不同。八进制转义符必须在0到255之间表示一个值。十六进制转义符通过结构满足这个条件。转义符\u和\U代表Unicode码点,所以在它们里面有些值是非法的,特别是那些高于0x10FFFF的值和代用的一半。
在反斜杠之后,某些单字符转义符代表特殊值:
\a U+0007 alert or bell
\b U+0008 backspace
\f U+000C form feed
\n U+000A line feed or newline
\r U+000D carriage return
\t U+0009 horizontal tab
\v U+000b vertical tab
\\ U+005c backslash
\' U+0027 single quote (valid escape only within rune literals)
\" U+0022 double quote (valid escape only within string literals)
所有其他以反斜杠开头的序列在符文字里面都是非法的。
rune_lit = "'" ( unicode_value | byte_value ) "'" .
unicode_value = unicode_char | little_u_value | big_u_value | escaped_char .
byte_value = octal_byte_value | hex_byte_value .
octal_byte_value = `\` octal_digit octal_digit octal_digit .
hex_byte_value = `\` "x" hex_digit hex_digit .
little_u_value = `\` "u" hex_digit hex_digit hex_digit hex_digit .
big_u_value = `\` "U" hex_digit hex_digit hex_digit hex_digit
hex_digit hex_digit hex_digit hex_digit .
escaped_char = `\` ( "a" | "b" | "f" | "n" | "r" | "t" | "v" | `\` | "'" | `"` ) .
'a'
'ä'
'本'
'\t'
'\000'
'\007'
'\377'
'\x07'
'\xff'
'\u12e4'
'\U00101234'
'\'' // rune literal containing single quote character
'aa' // illegal: too many characters
'\xa' // illegal: too few hexadecimal digits
'\0' // illegal: too few octal digits
'\uDFFF' // illegal: surrogate half
'\U00110000' // illegal: invalid Unicode code point
String literals/字符串字面量
字符串字面量表示从字符序列的连接中获得的字符串常量。有两种形式:原始(raw)字符串字元和解释(interpreted)字符串字元。
原始字符串字元是后引号之间的字符序列,如 “`foo`"。在引号内,除了后引号,任何字符都可以出现。原始字符串字面量的值是由引号之间未解释(隐含UTF-8编码)的字符组成的字符串;特别是,反斜杠没有特殊意义,字符串可能包含换行符。原始字符串字元中的回车字符(’\r’)会从原始字符串值中被丢弃。
被解释的字符串字面量是双引号之间的字符序列,如 “bar”。在引号内,除了换行和未转义的双引号外,任何字符都可以出现。引号之间的文字构成了字面量意义的值,反斜杠转义的解释与符文字面意义的解释相同(除了’/‘是非法的,而”/“是合法的),有相同的限制。三位数的八进制(\nnnnn)和两位数的十六进制(\xnn)转义符代表结果字符串的单个字节;所有其他转义符代表单个字符的UTF-8编码(可能是多字节)。因此,在一个字符串中,字面意义中的\377和\xFF代表一个价值0xFF=255的单一字节,而ÿ、\u00FF、\U000000FF和xc3\xbf代表字符U+00FF的UTF-8编码的两个字节0xc3 0xbf。
string_lit = raw_string_lit | interpreted_string_lit .
raw_string_lit = "`" { unicode_char | newline } "`" .
interpreted_string_lit = `"` { unicode_value | byte_value } `"` .
`abc` // same as "abc"
`\n
\n` // same as "\\n\n\\n"
"\n"
"\"" // same as `"`
"Hello, world!\n"
"日本語"
"\u65e5本\U00008a9e"
"\xff\u00FF"
"\uD800" // illegal: surrogate half
"\U00110000" // illegal: invalid Unicode code point
这些例子都代表同一个字符串:
"日本語" // UTF-8 input text
`日本語` // UTF-8 input text as a raw literal
"\u65e5\u672c\u8a9e" // the explicit Unicode code points
"\U000065e5\U0000672c\U00008a9e" // the explicit Unicode code points
"\xe6\x97\xa5\xe6\x9c\xac\xe8\xaa\x9e" // the explicit UTF-8 bytes
如果源码将一个字符表示为两个code point,比如涉及重音和字母的组合形式,如果放在符文rune字面量中,结果将是一个错误(它不是单个的code point),如果放在字符串字面量中,将出现两个code point。
2 - Golang 基础语法
2.1 - 包
在 Go 语言里,包是个非常重要的概念。其设计理念是使用包来封装不同语义单元的功能,更好地复用代码,并对每个包内的数据的使用有更好的控制。
包的规则
- 所有的.go 文件,除了空行和注释,都应该在第一行声明自己所属的包。
- 每个包都在一个单独的目录里。
- 不能把多个包放到同一个目录中,也不能把同一个包的文件分拆到多个不同目录中。
- 这意味着,同一个目录下的所有.go 文件必须声明同一个包名。
包的命名
- 给包命名的惯例是使用包所在目录的名字
- 给包及其目录命名时,应该使用简洁、清晰且全小写的名字
记住:并不需要所有包的名字都与别的包不同,因为导入包时是使用全路径的,所以可以区分同名的不同包。一般情况下,包被导入后会使用你的包名作为默认的名字,不过这个导入后的名字可以修改。
main包
在 Go 语言里,命名为 main 的包具有特殊的含义。 Go 语言的编译程序会试图把这种名字的包编译为二进制可执行文件。所有用 Go 语言编译的可执行程序都必须有一个名叫 main 的包。
包的导入
导入包需要使用关键字import ,它会告诉编译器你想引用该位置的包内的代码。如果需要导入多个包,习惯上是将 import 语句包装在一个导入块中。
import (
"fmt"
"strings"
)
编译器查找packge的顺序:
- 标准库,如
/usr/local/go/src/*/*
,这个路径由环境变量GOROOT决定。 - 当前项目源代码路径,如
/home/myproject/src/*/*
- gopath路径,如
/home/sky/work/soft/go/*/*
,这个路径由环境变量GOPATH决定。
查找策略:一旦编译器找到一个满足 import 语句的包,就停止进一步查找。
远程导入
Go 支持从远程网站获取源代码,如github:
import "github.com/spf13/viper"
获取过程使用 go get
命令完成
命名导入
重名的包可以通过命名导入来导入。
命名导入是指,在 import 语句给出的包路径的左侧定义一个名字,将导入的包命名为新名字。
import (
"fmt"
myfmt "mylib/fmt"
)
导入一个不在代码里使用的包时,Go 编译器会编译失败,输出错误。这个特性可以防止导入了未被使用的包,避免代码变得臃肿。
如果需要导入一个包,但是不需要引用这个包的标识符。在这种情况,可以使用空白标识符_
来重命名这个导入:
import (
_ "mylib/fmt"
)
init函数
每个包可以包含任意多个 init 函数,这些函数都会在程序执行开始的时候被调用。
多个init函数之间的执行顺序不定,不要依赖这个顺序。
所有被编译器发现的 init 函数都会安排在 main 函数之前执行。
init 函数用在设置包、初始化变量或者其他要在程序运行前优先完成的引导工作。
2.2 - 常量
常量的定义
常量的定义与变量类似,只不过使用 const
关键字。
const Pi = 3.14
func main() {
const World = "world"
fmt.Println("Hello", World)
fmt.Println("Happy", Pi, "Day")
const Truth = true
fmt.Println("Go rules?", Truth)
}
常量可以是字符、字符串、布尔或数字类型的值。
注意:常量不能使用 :=
语法定义。
数值常量
数值常量是高精度的 值。
const (
Big = 1 << 100
Small = Big >> 99
)
一个未指定类型的常量由上下文来决定其类型。
go语言规范
https://golang.org/ref/spec#Constants
https://moego.me/golang_spec.html#id271
常量有 布尔值常量 、 rune 常量 、 整数常量 、 浮点数常量 、 复数常量 和 字符串常量 。 Rune、整数、浮点数和复数常量统称为数值常量。
常量的值是由如下所表示的: rune,整数,浮点数,虚数,字符串字面值,表示常量的标识符,常量表达式,结果为常量的变量转换,或者一些内置函数所生成的值,这些内置函数比如应用于任意值的 unsafe.Sizeof ,应用于一些表达式 的 cap 或 len ,应用于复数常量的 real 和 imag 以及应用于数值常量的 complex 。布尔值是由预先声明的常量 true 和 false 所代表的。预先声明的标识符 iota 表示一个整数常量。
通常,复数常量是 常量表达式 的一种形式,会在该节讨论。
数值常量代表任意精度的确切值,而且不会溢出。因此,没有常量表示 IEEE-754 负零,无穷,以及非数字值集。
常量可以是有类型的也可以是无类型的。字面值常量, true , false , iota 以及一些仅包含无类型的恒定操作数的 常量表达式 是无类型的。
常量可以通过 常量声明 或 变量转换 被显示地赋予一个类型,也可以在 变量声明 或 赋值 中,或作为一个操作数在 表达式 中使用时隐式地被赋予一个类型。如果常量的值不能按照所对应的类型来表示的话,就会出错。「前一版的内容: 比如, 3.0 可以作为任何整数类型或任何浮点数类型,而 2147483648.0 (相当于 1«31 )可以作为 float32 , float64 或 uint32 类型,但不能是 int32 或 string 。」
一个无类型的常量有一个 默认类型 ,当在上下文中需要请求该常量为一个带类型的值时,这个 默认类型 便指向该常量隐式转换后的类型,比如像 i := 0 这样子的 短变量声明 就没有显示的类型。无类型常量的默认类型分别是 bool , rune , int , float64 , complex128 或 string ,取决于它是否是一个布尔值、 rune、整数、浮点数、复数或字符串常量。
实现限制:虽然数值常量在这个语言中可以是任意精度的,但编译器可能会使用精度受限的内部表示法来实现它。也就是说,每一种实现必须:
- 使用最少 256 位来表示整数。
- 使用最少 256 位来表示浮点数常量(包括复数常量的对应部分)的小数部分,使用最少 16 位表示其带符号的二进制指数部分。
- 当无法表示一个整数常量的精度时,需要给出错误。
- 当因为溢出而无法表示一个浮点数或复数常量时,需要给出错误。
- 当因为精度限制而无法表示一个浮点数或复数常量时,约到最接近的可表示的常量。
这些要求也适用于字面值常量,以及 常量表达式 的求值结果。
Effective Go
https://golang.org/doc/effective_go.html#constants
Go中的常量仅仅是常量。它们是在编译时创建的,即使是在函数中定义为locals,也只能是数字、字符(符文)、字符串或布尔值。由于编译时的限制,定义它们的表达式必须是常量表达式,可被编译器评估。例如,1«3是一个常量表达式,而math.Sin(math.Pi/4)不是,因为对math.Sin的函数调用需要在运行时发生。
在Go中,使用iota枚举器创建枚举常量。由于 iota 可以成为表达式的一部分,而且表达式可以隐式重复,因此很容易建立复杂的值集。
type ByteSize float64
const (
_ = iota // ignore first value by assigning to blank identifier
KB ByteSize = 1 << (10 * iota)
MB
GB
TB
PB
EB
ZB
YB
)
将String这样的方法附加到任何用户定义的类型上的能力,使得任意值在打印时自动格式化成为可能。虽然你最常看到的是它应用于结构体,但这种技术对于标量类型也很有用,比如浮点类型(如ByteSize)。
func (b ByteSize) String() string {
switch {
case b >= YB:
return fmt.Sprintf("%.2fYB", b/YB)
case b >= ZB:
return fmt.Sprintf("%.2fZB", b/ZB)
case b >= EB:
return fmt.Sprintf("%.2fEB", b/EB)
case b >= PB:
return fmt.Sprintf("%.2fPB", b/PB)
case b >= TB:
return fmt.Sprintf("%.2fTB", b/TB)
case b >= GB:
return fmt.Sprintf("%.2fGB", b/GB)
case b >= MB:
return fmt.Sprintf("%.2fMB", b/MB)
case b >= KB:
return fmt.Sprintf("%.2fKB", b/KB)
}
return fmt.Sprintf("%.2fB", b)
}
表达式YB打印为1.00YB,而ByteSize(1e13)打印为9.09TB。
这里使用Sprintf实现ByteSize的String方法是安全的(避免无限期重复),不是因为转换,而是因为它用%f调用Sprintf,而%f不是字符串格式。Sprintf只有在需要字符串时才会调用String方法,而%f需要的是浮点值。
2.3 - 变量
变量定义
var
语句定义一个变量的列表;跟函数的参数列表一样,类型在后面。
var
语句可以定义在包或函数级别。
var c, python, java bool
func main() {
var i int
fmt.Println(i, c, python, java)
}
变量初始化
// 变量定义可以包含初始值,每个变量对应一个。
var i, j int = 1, 2
func main() {
// 如果初始化是使用表达式,则可以省略类型;
// 变量从初始值中获得类型。
var c, python, java = true, false, "no!"
fmt.Println(i, j, c, python, java)
}
也可以用这个方式初始化多个变量,每个变量一行代码:
var (
ToBe bool = false
MaxInt uint64 = 1<<64 - 1
z complex128 = cmplx.Sqrt(-5 + 12i)
)
如果变量在定义时没有明确的初始化,则会赋值为该类型对应的零值:
- 数值类型为
0
, - 布尔类型为
false
, - 字符串为
""
(空字符串)
短声明变量
在函数中,:=
简洁赋值语句在明确类型的地方,可以用于替代 var
定义。
func main() {
var i, j int = 1, 2
k := 3
c, python, java := true, false, "no!"
}
函数外的每个语句都必须以关键字开始(var
、func
、等等),:=
结构不能使用在函数外。
多赋值模式
上面同时赋值多个变量的模式,可以用于if, for 等语句,实现在if, for 语句中的多赋值模式:
for i, j, s := 0, 5, "a"; i < 3 && j < 100 && s != "aaaaa"; i, j, s = i+1, j+1, s + "a" {
fmt.Println("Value of i, j, s:", i, j, s)
}
go语言规范
https://golang.org/ref/spec#Variables
变量是用来存放数值的存储位置。允许的值集由变量的类型决定。
变量声明,或者对于函数参数和结果,函数声明或函数字面量的签名为命名的变量保留了存储空间。调用内置函数new或取复合字面的地址,在运行时为变量分配存储空间。这样的匿名变量是通过(可能是隐含的)指针间接引用的。
数组、切片和结构体类型的结构化变量具有可以单独寻址的元素和字段。每个这样的元素都像一个变量一样。
变量的静态类型(或者说只是类型)是在它的声明中给出的类型,新调用或复合文字中提供的类型,或者是结构变量的元素的类型。接口类型的变量也有一个独特的动态类型,它是在运行时分配给变量的值的具体类型(除非该值是预先声明的标识符nil,它没有类型)。动态类型在执行过程中可能会发生变化,但存储在接口变量中的值总是可以分配给变量的静态类型。
var x interface{} // x 是 nil,它有一个静态类型 interface{}
var v *T // v 的值为 nil,静态类型为 *T
x = 42 // x 的值为 42,动态类型为 int
x = v // x 的值为 (*T)(nil),动态类型为 *T
变量的值通过在表达式中引用变量来检索;它是分配给变量的最新值。如果一个变量还没有被赋值,它的值就是它的类型的零值。
Effective Go
https://golang.org/doc/effective_go.html#variables
变量可以像常量一样被初始化,但初始化器可以是一个在运行时计算的通用表达式。
var (
home = os.Getenv("HOME")
user = os.Getenv("USER")
gopath = os.Getenv("GOPATH")
)
2.4 - 指针
Go 具有指针。 指针保存了变量的内存地址。
基本语法
定义:类型 *T
是指向类型 T
的值的指针。其零值是 nil
。
var p *int
生成:&
符号会生成一个指向其作用对象的指针。
i := 42
p = &i
取值:*
符号表示指针指向的底层的值。
fmt.Println(*p) // 通过指针 p 读取 i
*p = 21 // 通过指针 p 设置 i
注意:与 C 不同,Go 没有指针运算。
2.5 - 函数
语法
Golang的函数语法和Java、C等有非常大的不同:
- 函数参数定义是类型在变量名之后,而不是变量名之前
- 函数返回值定义在函数签名之后,而不是函数签名之前
- 函数可以返回多个值
func swap(x, y string) (string, string) {
return y, x
}
参数
函数可以没有参数或有多个参数。
注意类型在变量名 之后。
func add(x int, y int) int {
return x + y
}
当两个或多个连续的参数是同一类型,则除了最后一个类型之外,其他都可以省略。
func add(x, y int) int {
return x + y
}
返回值
函数可以返回任意数量的返回值。这点和很多语言不同:
func swap(x, y string) (string, string) {
return y, x
}
func main() {
a, b := swap("hello", "world")
fmt.Println(a, b)
}
Go 的返回值可以被命名,并且像变量那样使用。
func split(sum int) (x, y int) {
x = sum * 4 / 9
y = sum - x
return
}
2.6 - Getter
备注:摘录自 Effective Go https://golang.org/doc/effective_go.html#Getters
Getters
Go并没有提供对getter和setter的自动支持。自己提供getter和setter并没有错,而且这样做通常是合适的,但在getter的名称中加上Get既不习惯也没有必要。如果你有一个叫做 owner 的字段(小写,未导出),那么 getter 方法应该叫做 Owner(大写,导出),而不是 GetOwner。导出时使用大写的名称提供了区分字段和方法的钩子。如果需要的话,setter函数可能会被称为SetOwner。这两个名字在实践中都很好读。
owner := obj.Owner()
if owner != user {
obj.SetOwner(user)
}
2.7 - slice
Go Slices: usage and internals
https://blog.golang.org/slices-intro
介绍
Go 的 slice 类型为处理类型化数据序列提供了一种方便而有效的方法。分片类似于其他语言中的数组,但有一些不同寻常的特性。本文将介绍什么是切片以及如何使用它们。
数组
分片类型是建立在Go的数组类型之上的一种抽象,所以要理解分片,我们必须先理解数组。
数组类型定义指定了长度和元素类型。例如,类型 [4]int 表示一个由四个整数组成的数组。数组的大小是固定的,它的长度是其类型的一部分([4]int 和 [5]int 是不同的、不兼容的类型)。数组可以用通常的方式进行索引,所以表达式 s[n] 是访问从零开始的第n个元素。
var a [4]int
a[0] = 1
i := a[0]
// i == 1
数组不需要显式初始化,数组的零值是一个可以直接使用的数组,其元素本身是置零的:
// a[2] == 0, the zero value of the int type
[4]int的内存表示是四个整数值线性排列:
Go的数组是数值。一个数组变量表示整个数组;它不是指向第一个数组元素的指针(在 C 语言中是这样)。这意味着,当您分配或传递一个数组值时,您将复制它的内容。(为了避免复制,你可以传递一个指向数组的指针,但那是指向数组的指针,而不是数组)。有一种方法可以把数组看作是一种结构,但它是有索引而不是命名字段:一个固定大小的复合值。
可以像这样指定一个数组文字:
b := [2]string{"Penn", "Teller"}
或者,你可以让编译器为你计算数组元素:
b := [...]string{"Penn", "Teller"}
在这两种情况下,b的类型都是 [2]string 。
Slices
数组有它们的位置,但它们有点不灵活,所以你在 Go 代码中不太常见。不过,Slices却无处不在。它们建立在数组的基础上,提供了强大的功能和便利。
分片的类型规范是 []T,其中T是分片元素的类型。与数组类型不同,分片类型没有指定的长度。
分片文字的声明就像数组文字一样,只是省略了元素数:
letters := []string{"a", "b", "c", "d"}
可以用内置函数make创建切片,它的签名是:
func make([]T, len, cap) []T
其中T代表要创建的分片的元素类型。make函数需要类型、长度和可选的容量。调用时,make会分配一个数组并返回一个指向该数组的分片:
var s []byte
s = make([]byte, 5, 5)
// s == []byte{0, 0, 0, 0, 0}
当省略capacity参数时,它默认为指定的长度。下面是相同代码的一个更简洁的版本:
s := make([]byte, 5)
使用内置的 length 和 capacity 函数可以检查切片的长度和容量:
len(s) == 5
cap(s) == 5
接下来的两节将讨论长度和容量之间的关系。
分片的零值是nil。len 和cap 函数都会对nul分片返回0。
分片也可以通过 “切片” 现有的分片或数组来形成。切片是通过指定一个半开放的范围,用冒号隔开两个指数来完成的。例如,表达式 b[1:4] 创建了一个包含b元素1到3的分片(生成的分片的指数为0到2)。
b := []byte{'g', 'o', 'l', 'a', 'n', 'g'}
// b[1:4] == []byte{'o', 'l', 'a'}, sharing the same storage as b
分片表达式的 start 和 end 指数是可选的,它们分别默认为零和分片的长度:
// b[:2] == []byte{'g', 'o'}
// b[2:] == []byte{'l', 'a', 'n', 'g'}
// b[:] == b
这是创建一个给定数组的分片的语法:
x := [3]string{"Лайка", "Белка", "Стрелка"}
s := x[:] // a slice referencing the storage of x
切片内部
分片是数组段的描述符,它由指向数组的指针、段的长度和它的容量(段的最大长度)组成。
我们前面通过 make([]byte,5) 创建的变量s,结构是这样的:
长度是指分片指针所指的元素数。容量是底层数组中的元素数(从分片指针所指的元素开始)。在接下来的几个例子中,长度和容量之间的区别将变得很清楚。
当我们分片时,观察分片数据结构的变化以及它们与底层数组的关系:
s = s[2:4]
分片并不复制分片的数据,而是创建一个指向原始数组的新分片值。它创建一个新的分片值,指向原始数组。这使得分片操作与操作数组索引一样高效。因此,修改重新分片的元素(不是分片本身)会修改原始分片的元素:
d := []byte{'r', 'o', 'a', 'd'}
e := d[2:]
// e == []byte{'a', 'd'}
e[1] = 'm'
// e == []byte{'a', 'm'}
// d == []byte{'r', 'o', 'a', 'm'}
刚才我们把s切成了比它的容量短的长度。我们可以通过再次将s切成片来增长它的容量:
s = s[:cap(s)]
分片的增长不能超过其容量。试图这样做会引起运行时的恐慌(panic),就像在分片或数组的边界之外索引一样。同样,不能将分片重新分割到零以下以访问数组中的早期元素。
增长切片(copy和append函数)
要增加一个分片的容量,必须创建一个新的、更大的分片,并将原来分片的内容复制到其中。这种技术是其他语言的动态数组在幕后实现的工作方式。下一个例子通过创建一个新的分片t,将s的内容复制到t中,然后将分片值t赋给s,从而使s的容量增加一倍。
t := make([]byte, len(s), (cap(s)+1)*2) // +1 in case cap(s) == 0
for i := range s {
t[i] = s[i]
}
s = t
这个常用操作的循环部分,通过内置的copy函数变得更加简单。顾名思义,copy将数据从一个源片复制到一个目标片。它返回复制的元素数量:
func copy(dst, src []T) int
copy函数支持在不同长度的切片之间进行复制(它将只复制到较小数量的元素)。此外,copy还可以处理共享同一个底层数组的源片和目的片,正确处理重叠的分片。
使用copy,我们可以简化上面的代码片段:
t := make([]byte, len(s), (cap(s)+1)*2)
copy(t, s)
s = t
一个常见的操作是将数据追加到一个分片的末尾。这个函数将字节元素追加到一个字节分片上,如果需要的话,会增加分片,并返回更新后的分片值:
func AppendByte(slice []byte, data ...byte) []byte {
m := len(slice)
n := m + len(data)
if n > cap(slice) { // if necessary, reallocate
// allocate double what's needed, for future growth.
newSlice := make([]byte, (n+1)*2)
copy(newSlice, slice)
slice = newSlice
}
slice = slice[0:n]
copy(slice[m:n], data)
return slice
}
可以这样使用AppendByte:
p := []byte{2, 3, 5}
p = AppendByte(p, 7, 11, 13)
// p == []byte{2, 3, 5, 7, 11, 13}
像AppendByte这样的函数很有用,因为它们提供了对分片生长方式的完全控制。根据程序的特性,可能需要以较小或较大的分片进行分配,或者对重新分配的大小设置一个上限。
但大多数程序不需要完全控制,所以Go提供了一个内置的append函数,对大多数目的都很好,它的签名是:
func append(s []T, x ...T) []T
append函数将元素x追加到分片s的末尾,如果需要更大的容量,则增长分片:
a := make([]int, 1)
// a == []int{0}
a = append(a, 1, 2, 3)
// a == []int{0, 1, 2, 3}
要将一个分片附加到另一个分片上,请使用…将第二个参数扩展为一个参数列表:
a := []string{"John", "Paul"}
b := []string{"George", "Ringo", "Pete"}
a = append(a, b...) // equivalent to "append(a, b[0], b[1], b[2])"
// a == []string{"John", "Paul", "George", "Ringo", "Pete"}
由于分片的零值(nil)就像一个零长度的分片,所以你可以声明一个分片变量,然后在循环中追加到它:
// Filter returns a new slice holding only
// the elements of s that satisfy fn()
func Filter(s []int, fn func(int) bool) []int {
var p []int // == nil
for _, v := range s {
if fn(v) {
p = append(p, v)
}
}
return p
}
一个可能的 “疑难杂症”
如前所述,重新切割一个分片并不会对底层数组进行复制。完整的数组将被保留在内存中,直到它不再被引用。偶尔,这可能会导致程序在只需要一小块数据的时候,将所有数据保存在内存中。
例如,这个FindDigits函数将一个文件加载到内存中,并在内存中搜索第一组连续的数字数字,将它们作为一个新的分片返回。
var digitRegexp = regexp.MustCompile("[0-9]+")
func FindDigits(filename string) []byte {
b, _ := ioutil.ReadFile(filename)
return digitRegexp.Find(b)
}
这段代码的行为很明确,但是返回的 []字节 指向一个包含整个文件的数组。由于分片引用了原来的数组,所以只要分片被保留在周围,垃圾收集器就不能释放数组;文件的几个有用字节将整个内容保留在内存中。
为了解决这个问题,可以在返回之前将感兴趣的数据复制到一个新的分片中:
func CopyDigits(filename string) []byte {
b, _ := ioutil.ReadFile(filename)
b = digitRegexp.Find(b)
c := make([]byte, len(b))
copy(c, b)
return c
}
这个函数的一个更简洁的版本可以通过使用append来构建。这是留给读者的一个练习。
进一步阅读
Effective Go包含了对切片和数组的深入处理,Go语言规范定义了切片及其相关的帮助函数。
Slides in Effective Go
https://golang.org/doc/effective_go.html#slices
https://bingohuang.gitbooks.io/effective-go-zh-en/content/08_Data.html
切片通过对数组进行封装,为数据序列提供了更通用、强大而方便的接口。 除了矩阵变换这类需要明确维度的情况外,Go 中的大部分数组编程都是通过切片来完成的。
切片保存了对底层数组的引用,若你将某个切片赋予另一个切片,它们会引用同一个数组。 若某个函数将一个切片作为参数传入,则它对该切片元素的修改对调用者而言同样可见, 这可以理解为传递了底层数组的指针。
因此,Read 函数可接受一个切片实参 而非一个指针和一个计数;切片的长度决定了可读取数据的上限。以下为 os 包中 File 类型的 Read 方法签名:
func (file *File) Read(buf []byte) (n int, err error)
该方法返回读取的字节数和一个错误值(若有的话)。若要从更大的缓冲区 b 中读取前 32 个字节,只需对其进行切片即可:
n, err := f.Read(buf[0:32])
这种切片的方法常用且高效。若不谈效率,以下片段同样能读取该缓冲区的前 32 个字节:
var n int
var err error
for i := 0; i < 32; i++ {
nbytes, e := f.Read(buf[i:i+1]) // Read one byte.
n += nbytes
if nbytes == 0 || e != nil {
err = e
break
}
}
只要切片不超出底层数组的限制,它的长度就是可变的,只需将它赋予其自身的切片即可。 切片的容量可通过内建函数 cap 获得,它将给出该切片可取得的最大长度。 以下是将数据追加到切片的函数。若数据超出其容量,则会重新分配该切片。返回值即为所得的切片。 该函数中所使用的 len 和 cap 在应用于 nil 切片时是合法的,它会返回 0.
func Append(slice, data []byte) []byte {
l := len(slice)
if l + len(data) > cap(slice) { // reallocate
// Allocate double what's needed, for future growth.
newSlice := make([]byte, (l+len(data))*2)
// The copy function is predeclared and works for any slice type.
copy(newSlice, slice)
slice = newSlice
}
slice = slice[0:l+len(data)]
copy(slice[l:], data)
return slice
}
最终我们必须返回切片,因为尽管 Append 可修改 slice 的元素,但切片自身(其运行时数据结构包含指针、长度和容量)是通过值传递的。
向切片追加东西的想法非常有用,因此有专门的内建函数 append。 要理解该函数的设计,我们还需要一些额外的信息,我们将稍后再介绍它。
二维切片
Go 的数组和切片都是一维的。要创建等价的二维数组或切片,就必须定义一个数组的数组, 或切片的切片,就像这样:
type Transform [3][3]float64 // A 3x3 array, really an array of arrays.
type LinesOfText [][]byte // A slice of byte slices.
由于切片长度是可变的,因此其内部可能拥有多个不同长度的切片。在我们的 LinesOfText 例子中,这是种常见的情况:每行都有其自己的长度。
text := LinesOfText{
[]byte("Now is the time"),
[]byte("for all good gophers"),
[]byte("to bring some fun to the party."),
}
有时必须分配一个二维数组,例如在处理像素的扫描行时,这种情况就会发生。 我们有两种方式来达到这个目的。一种就是独立地分配每一个切片;而另一种就是只分配一个数组, 将各个切片都指向它。采用哪种方式取决于你的应用。若切片会增长或收缩, 就应该通过独立分配来避免覆盖下一行;若不会,用单次分配来构造对象会更加高效。 以下是这两种方法的大概代码,仅供参考。首先是一次一行的:
// 分配顶层切片。
picture := make([][]uint8, YSize) // 每 y 个单元一行。
// 遍历行,为每一行都分配切片
for i := range picture {
picture[i] = make([]uint8, XSize)
}
现在是一次分配,对行进行切片:
// 分配顶层切片,和前面一样。
picture := make([][]uint8, YSize) // 每 y 个单元一行。
// 分配一个大的切片来保存所有像素
pixels := make([]uint8, XSize*YSize) // 拥有类型 []uint8,尽管图片是 [][]uint8.
// 遍历行,从剩余像素切片的前面切出每行来。
for i := range picture {
picture[i], pixels = pixels[:XSize], pixels[XSize:]
}
Go语言规范中的Slice
https://golang.org/ref/spec#Slice_types
分片是针对一个底层数组的连续段的描述符,它提供了对该数组内有序序列元素的访问。分片类型表示其元素类型的数组的所有分片的集合。元素的数量被称为分片长度,且不能为负。未初始化的分片的值为 nil
。
SliceType = "[", "]", ElementType .
分片 s
的长度可以被内置函数 len来发现;和数组不同的是,这个长度可能会在执行过程中改变。元素可以被从 0
索引到 len(s) - 1
的整数所寻址到。一个给定元素的分片索引可能比其底层数组的相同元素的索引要小。
分片一旦初始化便始终关联到存放其元素的底层数组。因此分片会与其数组和其它相同数组的分片共享存储区;相比之下,不同的数组总是代表不同的存储区域。
分片底层的数组可以延伸超过分片的末端。 容量 便是对这个范围的测量:它是分片长度和数组内除了该分片以外的长度的和;不大于其容量长度的分片可以从原始分片再分片新的来创建。分片 a
的容量可以使用内置函数 cap(a) 来找到。
对于给定元素类型 T
的新的初始化好的分片值的创建是使用的内置函数 make,它需要获取分片类型、指定的长度和可选的容量作为参数。使用 make
创建的分片总是分配一个新的隐藏的数组给返回的分片值去引用。也就是,执行
make([]T, length, capacity)
就像分配个数组然后再分片它一样来产生相同的分片,所以如下两个表达式是相等的:
make([]int, 50, 100)
new([100]int)[0:50]
如同数组一样,分片总是一维的但可以通过组合来构造高维的对象。数组间组合时,被构造的内部数组总是拥有相同的长度;但分片与分片(或数组与分片)组合时,内部的长度可能是动态变化的。此外,内部分片必须单独初始化。
3 - 声明
3.1 - Block
备注:摘录自 Golang语言规范 https://golang.org/ref/spec#Blocks
Blocks
Block/块是在匹配的大括号内的声明和语句序列(可能为空):
Block = "{" StatementList "}" .
StatementList = { Statement ";" } .
除了源码中的显性块,还有隐性块:
- universe block (宇宙块)包含了所有的go源代码文本。
- 每个包都有一个 package block (包块),包含该包的所有go源代码文本。
- 每个文件都有一个 file block (文件块),包含该文件中的所有go源代码文本。
- 每个 “if”、“for “和 “switch “语句都被认为是在自己的隐含块中。
- 在 “switch “或 “select “语句中的每个子句都作为一个隐式块。
块可以嵌套,会影响scope/范围。
3.2 - 常量
Constant declaration
https://golang.org/ref/spec#Constant_declarations
常量声明将标识符列表(常量的名称)与常量表达式列表的值绑定。标识符的数量必须等于表达式的数量,左边的第n个标识符与右边的第n个表达式的值绑定。
ConstDecl = "const" ( ConstSpec | "(" { ConstSpec ";" } ")" ) .
ConstSpec = IdentifierList [ [ Type ] "=" ExpressionList ] .
IdentifierList = identifier { "," identifier } .
ExpressionList = Expression { "," Expression } .
如果存在类型,所有常量都采用指定的类型,并且表达式对于该类型必须是 assignable /可分配的。如果类型被省略,则常量取对应表达式各自的类型。如果表达式的值是无类型的常量,则声明的常量保持无类型,而常量标识符表示常量值。例如,如果表达式是浮点字面量,常量标识符表示浮点常量,即使字面量的小数部分为零。
const Pi float64 = 3.14159265358979323846
const zero = 0.0 // untyped floating-point constant
const (
size int64 = 1024
eof = -1 // untyped integer constant
)
const a, b, c = 3, 4, "foo" // a = 3, b = 4, c = "foo", untyped integer and string constants
const u, v float32 = 0, 3 // u = 0.0, v = 3.0
在带括号的const声明列表中,除了第一个ConstSpec之外,表达式列表可以省略。这样的空列表相当于前面第一个非空的表达式列表及其类型(如果有的话)的文本替换。因此,省略表达式列表相当于重复前面的列表。标识符的数量必须等于前一个列表中表达式的数量。与iota常量生成器一起,这种机制允许轻量级的顺序值声明。
const (
Sunday = iota
Monday
Tuesday
Wednesday
Thursday
Friday
Partyday
numberOfDays // this constant is not exported
)
3.3 - 变量
Variable declaration
https://golang.org/ref/spec#Variable_declarations
变量声明创建一个或多个变量,将相应的标识符绑定到它们上面,并给每个变量一个类型和初始值。
VarDecl = "var" ( VarSpec | "(" { VarSpec ";" } ")" ) .
VarSpec = IdentifierList ( Type [ "=" ExpressionList ] | "=" ExpressionList ) .
var i int
var U, V, W float64
var k = 0
var x, y float32 = -1, -2
var (
i int
u, v, s = 2.0, 3.0, "bar"
)
var re, im = complexSqrt(-1)
var _, found = entries[name] // map lookup; only interested in "found"
如果给定了一个表达式列表,则按照赋值规则用表达式初始化变量。否则,每个变量初始化为零值。
如果存在类型,每个变量被赋予该类型。否则,每个变量被赋予赋值中相应初始化值的类型。如果该值是一个无类型的常量,则首先隐式转换为其默认类型;如果是一个无类型的布尔值,则首先隐式转换为类型bool。预先声明的值nil不能用于初始化一个没有显式类型的变量。
var d = math.Sin(0.5) // d is float64
var i = 42 // i is int
var t, ok = x.(T) // t is T, ok is bool
var n = nil // illegal
实现限制:如果一个变量从未被使用,编译器可能会规定在函数体中声明一个变量是非法的。
短变量声明
短变量声明使用这样的语法:
ShortVarDecl = IdentifierList ":=" ExpressionList .
它是有初始化表达式但没有类型的正则变量声明的简写:
"var" IdentifierList = ExpressionList .
i, j := 0, 10
f := func() int { return 7 }
ch := make(chan int)
r, w, _ := os.Pipe() // os.Pipe() returns a connected pair of Files and an error, if any
_, y, _ := coord(p) // coord() returns three values; only interested in y coordinate
与普通变量声明不同,短变量声明可以重新声明变量,但前提是这些变量原来在同一个块(如果块是函数体,则在参数列表中)中早先声明过,类型相同,而且至少有一个非空变量是新的。因此,重声明只能出现在多变量的短声明中。重新声明并不引入一个新的变量,它只是给原来的变量分配一个新的值。
field1, offset := nextField(str, 0)
field2, offset := nextField(str, offset) // redeclares offset
a, a := 1, 2 // illegal: double declaration of a or no new variable if a was declared elsewhere
短变量声明只能出现在函数内部。在某些情况下,例如 “if”、“for “或 “switch “语句的初始化器,它们可以用来声明本地临时变量。
备注:详见 “重新声明” 一节
3.4 - iota
iota
https://golang.org/ref/spec#Iota
在常量声明中,预先声明的标识符 iota 代表连续的非类型整数常量。它的值是该常量声明中各自ConstSpec的索引,从0开始。它可以用来构造一组相关的常量。
const (
c0 = iota // c0 == 0
c1 = iota // c1 == 1
c2 = iota // c2 == 2
)
const (
a = 1 << iota // a == 1 (iota == 0)
b = 1 << iota // b == 2 (iota == 1)
c = 3 // c == 3 (iota == 2, unused)
d = 1 << iota // d == 8 (iota == 3)
)
const (
u = iota * 42 // u == 0 (untyped integer constant)
v float64 = iota * 42 // v == 42.0 (float64 constant)
w = iota * 42 // w == 84 (untyped integer constant)
)
const x = iota // x == 0
const y = iota // y == 0
根据定义,iota在同一个ConstSpec中的多次使用都具有相同的值:
const (
bit0, mask0 = 1 << iota, 1<<iota - 1 // bit0 == 1, mask0 == 0 (iota == 0)
bit1, mask1 // bit1 == 2, mask1 == 1 (iota == 1)
_, _ // (iota == 2, unused)
bit3, mask3 // bit3 == 8, mask3 == 7 (iota == 3)
)
最后一个例子利用了最后一个非空表达式列表的隐式重复。
3.5 - 函数
Function declaration
https://golang.org/ref/spec#Function_declarations
函数声明将标识符,即函数名,与函数绑定。
FunctionDecl = "func" FunctionName Signature [ FunctionBody ] .
FunctionName = identifier .
FunctionBody = Block .
如果函数的签名声明了结果参数,那么函数体的语句列表必须以终止语句结束。
func IndexRune(s string, r rune) int {
for i, c := range s {
if c == r {
return i
}
}
// invalid: missing return statement
}
函数声明可以省略主体。这样的声明提供了在Go之外实现的函数的签名,例如汇编例程。
func min(x int, y int) int {
if x < y {
return x
}
return y
}
func flushICache(begin, end uintptr) // implemented externally
3.6 - 类型
Type declaration
https://golang.org/ref/spec#Type_declarations
类型声明将标识符,即类型名称,绑定到类型上。类型声明有两种形式:别名声明和类型定义。
TypeDecl = "type" ( TypeSpec | "(" { TypeSpec ";" } ")" ) .
TypeSpec = AliasDecl | TypeDef .
Alias declarations
别名声明将标识符绑定到给定的类型上:
AliasDecl = identifier "=" Type .
在标识符的范围内,它作为类型的别称:
type (
nodeList = []*Node // nodeList and []*Node are identical types
Polar = polar // Polar and polar denote identical types
)
Type definitions
类型定义创建了一个新的、独特的类型,它与给定的类型具有相同的底层类型和操作,并为它绑定了一个标识符。
TypeDef = identifier Type .
新类型被称为 defined type / 定义类型。它不同于任何其他类型,包括它创建时来源的类型。
type (
Point struct{ x, y float64 } // Point and struct{ x, y float64 } are different types
polar Point // polar and Point denote different types
)
type TreeNode struct {
left, right *TreeNode
value *Comparable
}
type Block interface {
BlockSize() int
Encrypt(src, dst []byte)
Decrypt(src, dst []byte)
}
defined type / 定义类型可以有与之相关的方法。它不继承任何与给定类型绑定的方法,但接口类型的方法集或复合类型的元素保持不变。
// A Mutex is a data type with two methods, Lock and Unlock.
type Mutex struct { /* Mutex fields */ }
func (m *Mutex) Lock() { /* Lock implementation */ }
func (m *Mutex) Unlock() { /* Unlock implementation */ }
// NewMutex has the same composition as Mutex but its method set is empty.
type NewMutex Mutex
// The method set of PtrMutex's underlying type *Mutex remains unchanged,
// but the method set of PtrMutex is empty.
type PtrMutex *Mutex
// The method set of *PrintableMutex contains the methods
// Lock and Unlock bound to its embedded field Mutex.
type PrintableMutex struct {
Mutex
}
// MyBlock is an interface type that has the same method set as Block.
type MyBlock Block
类型定义可以用来定义不同的布尔、数值或字符串类型,并与它们相关联的方法:
type TimeZone int
const (
EST TimeZone = -(5 + iota)
CST
MST
PST
)
func (tz TimeZone) String() string {
return fmt.Sprintf("GMT%+dh", tz)
}
3.7 - 方法
Method declaration
https://golang.org/ref/spec#Method_declarations
方法是一个带有 receiver (接收者)的函数。方法声明将标识符,即方法名,绑定到一个方法上,并将该方法与接受者的基本类型联系起来。
MethodDecl = "func" Receiver MethodName Signature [ FunctionBody ] .
Receiver = Parameters .
接收者是通过方法名前的额外参数部分指定的。这个参数部分必须声明一个非变量参数,即 receiver(接收者)。它的类型必须是一个 defined type(定义类型)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
}
将方法 Length 和 Scale(接收者类型为*Point)绑定到基本类型Point上。
方法的类型是以接收者为第一参数的函数类型。例如,方法Scale的类型是:
func(p *Point, factor float64)
当然,这样声明的函数不是方法。
备注:可以参考文章 函数——go世界中的一等公民 中的 “方法的本质” 一节
“go里面其实方法就是语法糖,实际上Method就是将receiver作为函数的第一个参数输入的语法糖而已,本质上和函数没有区别”
3.8 - 重新声明
Redeclaration and reassignment
https://golang.org/doc/effective_go.html#redeclaration
f, err := os.Open(name)
if err != nil {
return err
}
d, err := f.Stat()
if err != nil {
f.Close()
return err
}
codeUsing(f, d)
旁白一下。上一节的最后一个例子演示了 := 短声明形式的工作细节。调用os.Open的声明如下。
f, err := os.Open(name)
这条语句声明了两个变量,f和err。几行之后,对f.Stat的调用为:
d, err := f.Stat()
这看起来就像它声明了d和err。但请注意,两个语句中都出现了err。这种重复是合法的:err 是由第一条语句声明的,但只是在第二条语句中重新赋值。这意味着对 f.Stat 的调用使用了上面声明的现有err变量,只是给它一个新的值。
在 := 声明中,即使已经声明了一个变量 v,也可以出现,但前提是:
- 这个声明与v的现有声明在同一个作用域中(如果v已经在外部作用域中声明了,则声明将创建一个新的变量§)。
- 初始化中的相应值可分配给v,并且
- 至少还有一个变量是由声明创建的。
这个不寻常的属性是纯粹的实用主义,使得它很容易使用一个单一的err值,例如,在一个长的if-else链中。你会看到它经常被使用。
§ 这里值得注意的是,在Go中,函数参数和返回值的作用域与函数主体相同,尽管它们在词法上出现在包围主体的括号之外。
4 - 作用域
4.1 - 标签
Label scope
https://golang.org/ref/spec#Label_scopes
标签由标签语句声明,并在 “break”、“continue “和 “goto “语句中使用。定义一个从未使用过的标签是非法的。与其他标识符不同的是,标签没有块范围,也不会与非标签的标识符冲突。
标签的作用域是它声明时所在的函数的主体,不包括任何嵌套函数的主体。
5 - 类型
5.1 - 概述
Type
https://golang.org/ref/spec#Types
类型决定了一组值,以及对这些值的特定操作和方法。如果有类型名称,可以用类型名称来表示,也可以用类型字面量来指定,由现有的类型组成一个类型。
Type = TypeName | TypeLit | "(" Type ")" .
TypeName = identifier | QualifiedIdent .
TypeLit = ArrayType | StructType | PointerType | FunctionType | InterfaceType |
SliceType | MapType | ChannelType .
语言预先声明某些类型名称。其他类型是通过类型声明引入的。复合类型–数组、结构体、指针、函数、接口、切片、映射和通道类型–可以使用类型字面量来构造。
每个类型T都有一个底层类型(underlying type)。如果T是预定义的布尔类型、数值类型、字符串类型之一,或者是类型字面量,那么对应的底层类型就是T本身。否则,T的底层类型是T在其类型声明中引用的类型的底层类型。
type (
A1 = string
A2 = A1
)
type (
B1 string
B2 B1
B3 []B1
B4 B3
)
string、A1、A2、B1、B2的底层类型是string。[]B1、B3和B4的底层类型是[]B1。
方法集
类型可以有一个与之相关的方法集。接口类型的方法集就是它的接口。任何其他类型T的方法集由所有用接收者类型T声明的方法组成。相应的指针类型*T
的方法集是用接收者 *T
或T
声明的所有方法的集合(也就是说,它也包含T的方法集)。更多的规则适用于包含嵌入式字段的结构体,如结构体类型一节所述。任何其他类型的方法集都是空的。在方法集中,每个方法必须有一个唯一的非空方法名。
类型的方法集决定了该类型实现的接口和可以使用该类型的接收者调用的方法。
类型和值的属性
https://golang.org/ref/spec#Properties_of_types_and_values
类型一致性
两个类型要么相同,要么不同。
已定义类型(defined type)总是不同于其他类型。否则,如果它们的底层类型字面量在结构上是等价的,那么两个类型就是相同的;也就是说,它们具有相同的字面量结构,相应的组件具有相同的类型。详言之。
- 如果两个数组类型具有相同的元素类型和相同的数组长度,那么它们就是相同的。
- 如果两个分片类型具有相同的元素类型,则它们是相同的。
- 如果两个结构体类型具有相同的字段序列,并且对应的字段具有相同的名称、相同的类型和相同的标签,则两个结构体类型是相同的。来自不同包的非导出字段名总是不同的。
- 如果两个指针类型具有相同的基础类型,那么它们就是相同的。
- 如果两个函数类型具有相同数量的参数和结果值,对应的参数和结果类型相同,并且两个函数都是变量,或者都不是变量,那么这两个函数类型就是相同的。参数和结果名称不需要匹配。
- 如果两个接口类型具有相同的方法集,名称相同,函数类型相同,则两个接口类型是相同的。来自不同包的非导出方法名总是不同的。方法的顺序无关紧要。
- 如果两个映射类型有相同的键和元素类型,那么它们就是相同的。
- 如果两个通道类型具有相同的元素类型和相同的方向,那么它们就是相同的。
给定声明:
type (
A0 = []string
A1 = A0
A2 = struct{ a, b int }
A3 = int
A4 = func(A3, float64) *A0
A5 = func(x int, _ float64) *[]string
)
type (
B0 A0
B1 []string
B2 struct{ a, b int }
B3 struct{ a, c int }
B4 func(int, float64) *B0
B5 func(x int, y float64) *A1
)
type C0 = B0
这些类型是相同的:
A0, A1, and []string
A2 and struct{ a, b int }
A3 and int
A4, func(int, float64) *[]string, and A5
B0 and C0
[]int and []int
struct{ a, b *T5 } and struct{ a, b *T5 }
func(x int, y float64) *[]string, func(int, float64) (result *[]string), and A5
B0和B1是不同的,因为它们是由不同的类型定义创建的新类型;func(int,float64) *B0和func(x int,y float64) *[]string是不同的,因为B0与[]string不同。
可分配性
如果以下条件之一适用,则值x可分配给类型T的变量("x
is assignable to T
")。
- x的类型与T相同。
- x的类型V和T的底层类型相同,并且V或T中至少有一个不是定义类型。
- T是一个接口类型,x实现了T。
- x是双向通道值,T是通道类型,x的类型V和T具有相同的元素类型,V或T中至少有一个不是定义类型。
- x是预先声明的标识符nil,T是指针、函数、片、映射、通道或接口类型。
- x是一个可由T类型的值表示的非类型常量。
可表示性
如果下列条件之一适用,则常数x可由类型为T的值表示:
- x在由T决定的值集合中。
- T是一个浮点类型,并且x可以被四舍五入到T的精度而不会溢出。四舍五入使用IEEE 754四舍五入到偶数的规则,但将IEEE负零进一步简化为无符号零。注意,常量值永远不会产生IEEE负零、NaN或无穷大。
- T是复数类型,x的分量real(x)和 imag(x)可以用T的分量类型(float32或float64)的值表示。
x T x is representable by a value of T because
'a' byte 97 is in the set of byte values
97 rune rune is an alias for int32, and 97 is in the set of 32-bit integers
"foo" string "foo" is in the set of string values
1024 int16 1024 is in the set of 16-bit integers
42.0 byte 42 is in the set of unsigned 8-bit integers
1e10 uint64 10000000000 is in the set of unsigned 64-bit integers
2.718281828459045 float32 2.718281828459045 rounds to 2.7182817 which is in the set of float32 values
-1e-1000 float64 -1e-1000 rounds to IEEE -0.0 which is further simplified to 0.0
0i int 0 is an integer value
(42 + 0i) float32 42.0 (with zero imaginary part) is in the set of float32 values
x T x is not representable by a value of T because
0 bool 0 is not in the set of boolean values
'a' string 'a' is a rune, it is not in the set of string values
1024 byte 1024 is not in the set of unsigned 8-bit integers
-1 uint16 -1 is not in the set of unsigned 16-bit integers
1.1 int 1.1 is not an integer value
42i float32 (0 + 42i) is not in the set of float32 values
1e1000 float64 1e1000 overflows to IEEE +Inf after rounding
5.2 - 布尔类型
Boolean types
https://golang.org/ref/spec#Boolean_types
布尔类型表示由预先声明的常量 true 和 false 表示的布尔真值的集合。预先声明的布尔类型是bool;它是一个已定义类型。
5.3 - 数字类型
Numeric types
https://golang.org/ref/spec#Numeric_types
数值类型表示整数或浮点值的集合。预先声明的与架构无关的数值类型有:
uint8 the set of all unsigned 8-bit integers (0 to 255)
uint16 the set of all unsigned 16-bit integers (0 to 65535)
uint32 the set of all unsigned 32-bit integers (0 to 4294967295)
uint64 the set of all unsigned 64-bit integers (0 to 18446744073709551615)
int8 the set of all signed 8-bit integers (-128 to 127)
int16 the set of all signed 16-bit integers (-32768 to 32767)
int32 the set of all signed 32-bit integers (-2147483648 to 2147483647)
int64 the set of all signed 64-bit integers (-9223372036854775808 to 9223372036854775807)
float32 the set of all IEEE-754 32-bit floating-point numbers
float64 the set of all IEEE-754 64-bit floating-point numbers
complex64 the set of all complex numbers with float32 real and imaginary parts
complex128 the set of all complex numbers with float64 real and imaginary parts
byte alias for uint8
rune alias for int32
一个n位整数的值是n位宽,用二补码算术(two’s complement arithmetic)表示。
还有一组预先声明的数值类型,其大小与实现有关。
uint either 32 or 64 bits
int same size as uint
uintptr an unsigned integer large enough to store the uninterpreted bits of a pointer value
为了避免可移植性问题,所有的数值类型都是已定义类型,因此除了byte(uint8的别名)和rune(int32的别名)之外,其他类型都是不同的。当在表达式或赋值中混合使用不同的数值类型时,需要进行显式转换。例如,int32和int不是同一类型,即使它们在特定架构上可能具有相同的大小。
5.4 - string类型
String types
https://golang.org/ref/spec#String_types
字符串类型表示字符串值的集合。一个字符串值是一个(可能是空的)字节序列。字节数称为字符串的length/长度,永远不会是负数。字符串是不可改变的:一旦创建,就不可能改变字符串的内容。预先声明的字符串类型是字符串,它是已定义类型。
字符串s的长度可以通过内置函数len来发现。如果字符串是常量,那么长度就是编译时常量。字符串的字节可以通过整数索引0到len(s)-1来访问。取这种元素的地址是非法的;如果 s[i] 是字符串的第 i’th 个字节,&s[i] 是无效的。
参考资料
5.5 - 数组类型
go语言实战
类型 [n]T
是一个数组,有 n
个类型为 T
的值。
数组定义访问如下,需要指定类型和数组大小:
var a [10]int
注意:数组的长度是其类型的一部分,因此数组不能改变大小。
也可以在定义时直接创建数组,数组大小的设置可以有多种方式:
a := [2]string{"a", "b"}
a := []string{"a", "b"}
a := [...]string{"a", "b"}
通过下标访问单个元素:
var a [2]string
a[0] = "Hello"
a[1] = "World"
fmt.Println(a[0], a[1])
fmt.Println(a)
golang语言规范
https://golang.org/ref/spec#Array_types
数组是一个单一类型的元素的计数序列,称为元素类型。元素的数量称为数组的长度,绝不是负数。
ArrayType = "[" ArrayLength "]" ElementType .
ArrayLength = Expression .
ElementType = Type .
长度是数组类型的一部分;它必须计算为一个非负常数,用int类型的值表示。数组a的长度可以通过内置函数len来计算。元素可以用0到len(a)-1的整数索引来寻址。数组类型总是一维的,但可以组成多维类型。
[32]byte
[2*N] struct { x, y int32 }
[1000]*float64
[3][5]int
[2][2][2]float64 // same as [2]([2]([2]float64))
Effective Go
https://golang.org/doc/effective_go.html#arrays
在规划内存的详细布局时,数组很有用,有时可以帮助避免分配,但主要是它们是切片的构建模块,也就是下一节的主题。为了给这个主题打下基础,下面说说关于数组的一些情况。
在Go和C中,数组的工作方式有很大的不同,在Go中:
- 数组就是值。将一个数组赋值给另一个数组会复制所有的元素。
- 特别是,如果你把一个数组传递给一个函数,它将收到一个数组的副本,而不是一个指向它的指针。
- 数组的大小是其类型的一部分。类型 [10]int 和 [20]int 是不同的。
值属性可能很有用,但也很昂贵;如果你想要类似C的行为和效率,你可以传递一个指针给数组。
func Sum(a *[3]float64) (sum float64) {
for _, v := range *a {
sum += v
}
return
}
array := [...]float64{7.0, 8.5, 9.1}
x := Sum(&array) // Note the explicit address-of operator
但即使这种风格也不是 go 的习惯用法。请用分片(slice)代替。
实现原理
参考文章: go语言数组的实现原理
初始化
Go 语言中的数组有两种不同的创建方式,一种是显式的指定数组的大小,另一种是使用 [...]T
声明数组,Go 语言会在编译期间通过源代码对数组的大小进行推断:
arr1 := [3]int{1, 2, 3}
arr2 := [...]int{1, 2, 3}
编译器会对第二种数组的大小进行推导,通过遍历元素的方式来计算数组中元素的数量。
这两种初始化方式在运行时是完全等价的,[...]T
这种初始化方式只是 Go 语言提供的一种语法糖,当不想计算数组中的元素个数时可以减少一些工作。
对于一个由字面量组成的数组,根据数组元素数量的不同,编译器会做两种不同的优化:
- 当元素数量小于或者等于 4 个时,会直接将数组中的元素放置在栈上;
- 当元素数量大于 4 个时,会将数组中的元素放置到静态区并在运行时取出;
访问和赋值
无论是在栈上还是静态存储区,数组在内存中其实就是一连串的内存空间。
表示数组的方法就是:
- 一个指向数组开头的指针
- 数组中元素的数量
- 数组中元素类型占的空间大小
数组访问越界是非常严重的错误,Go 语言中对越界的判断:
- 可以在编译期间由静态类型检查完成的,函数会对访问数组的索引进行验证。数组和字符串的一些简单越界错误都会在编译期间发现,比如我们直接使用整数或者常量访问数组。
- 使用变量去访问数组或者字符串时,需要Go 语言运行时在发现数组、切片和字符串的越界操作触发程序的运行时错误
访问数组:
- 在使用字面量整数访问数组下标时就会生成非常简单的中间代码
- 当编译器无法对数组下标是否越界无法做出判断时才会加入
PanicBounds
指令交给运行时进行判断
参考资料
5.6 - Slice类型
Slice
摘录自 go语言实战
slice 指向一序列的值,并且包含了长度信息。
[]T
是一个元素类型为 T
的 slice。
p := []int{2, 3, 5, 7, 11, 13}
对 slice 切片
slice 可以重新切片,创建一个新的 slice 值指向相同的数组。
表达式s[lo:hi]
表示从 lo
到 hi-1
的 slice 元素,含两端。因此s[lo:lo]
是空的,而s[lo:lo+1]
有一个元素。
p := []int{2, 3, 5, 7, 11, 13}
fmt.Println("p ==", p)
fmt.Println("p[1:4] ==", p[1:4])
// 省略下标代表从 0 开始
fmt.Println("p[:3] ==", p[:3])
// 省略上标代表到 len(s) 结束
fmt.Println("p[4:] ==", p[4:])
构造 slice
slice 由函数 make
创建,第二个参数为数组长度:
a := make([]int, 5) // len(a)=5,cap(a)=5
可以通过第三个参数来指定容量:
b := make([]int, 0, 5) // len(b)=0, cap(b)=5
注意:slice的长度可以在构造时通过参数明确指定,也可以在切片时通过上下两个下标计算而来。而容量则需要考虑左下标开始位置。
func main() {
a := make([]int, 5) // 指定长度为5,容量没有设置,则和长度相同:len=5 cap=5
printSlice("a", a)
b := make([]int, 0, 5) // 指定长度为0,容量为5:len=0 cap=5
printSlice("b", b)
c := b[:2] // 切片时长度为下表差,容量计算时需要考虑左下标开始位置,这里的左下标从0开始:len=2 cap=5
printSlice("c", c)
d := c[2:5] // 切片时长度为下表差,容量计算时需要考虑左下标开始位置,这里的左下标从2开始:len=3 cap=5
printSlice("d", d)
}
func printSlice(s string, x []int) {
fmt.Printf("%s len=%d cap=%d %v\n",
s, len(x), cap(x), x)
}
slice 的零值是 nil
。一个 nil 的 slice 的长度和容量是 0。
var z []int
fmt.Println(z, len(z), cap(z))
向 slice 添加元素
Go 提供了一个内建函数 append
向 slice 添加元素:
func append(s []T, vs ...T) []T
append
的第一个参数s
是一个类型为T
的数组,其余类型为T
的值将会添加到 slice。append
的结果是一个包含原 slice 所有元素加上新添加的元素的 slice。- 如果
s
的底层数组太小,而不能容纳所有值时,会分配一个更大的数组。 返回的 slice 会指向这个新分配的数组。
func main() {
var a []int
printSlice("a", a)
// append works on nil slices.
a = append(a, 0)
printSlice("a", a)
// the slice grows as needed.
a = append(a, 1)
printSlice("a", a)
// we can add more than one element at a time.
a = append(a, 2, 3, 4)
printSlice("a", a)
}
func printSlice(s string, x []int) {
fmt.Printf("%s len=%d cap=%d %v\n",
s, len(x), cap(x), x)
}
Slice types
https://golang.org/ref/spec#Slice_types
分片是底层数组的连续段的描述符,它提供了对该数组中元素的编号序列的访问。分片类型表示其元素类型的数组的所有分片的集合。元素的数量称为分片的长度,并且永远不会是负数。未初始化的分片的值为零。
SliceType = "[" "]" ElementType .
片段s的长度可以通过内置函数len发现;与数组不同,它可能在执行过程中发生变化。元素可以通过0到len(s)-1的整数索引来寻址。一个给定元素的分片索引可能小于底层数组中相同元素的索引。
分片一旦被初始化,总是与持有其元素的底层数组相关联。因此,一个分片与它的数组和同一数组的其他分片共享存储;相反,不同的数组总是代表不同的存储。
分片的底层数组可以延伸到分片的末端。capacity/容量是对该范围的衡量:它是切片的长度和切片之外的数组的长度之和;一个长度不大于该容量的切片可以通过从原始切片中切出一个新的切片来创建。切片的capacity/容量可以通过内置函数cap(a)来发现。
一个新的、初始化的给定元素类型T的分片值是使用内置函数make制作的,它需要一个分片类型和指定长度和可选容量的参数。用make创建的分片总是分配一个新的、隐藏的数组,返回的分片值指向这个数组。也就是说,执行
make([]T, length, capacity)
产生的分片与分配一个数组并对其进行分片一样,所以这两个表达式是等价的:
make([]int, 50, 100)
new([100]int)[0:50]
像数组一样,切片总是一维的,但可以组成更高维的对象。对于数组的数组,根据结构,内部数组总是相同的长度;但是对于切片的切片(或切片的数组),内部长度可能会动态变化。此外,内部切片必须单独初始化。
Slice
https://golang.org/doc/effective_go.html#slices
Slices 包裹了数组,为数据序列提供了一个更通用、更强大、更方便的接口。除了具有显式维度的项目(如变换矩阵),Go 中的大多数数组编程都是通过切片而不是简单的数组来完成的。
切片持有对底层数组的引用,如果您将一个切片分配给另一个切片,则两者都指向同一个数组。如果函数接受切片参数,那么它对切片元素的改变将对调用者可见,类似于传递一个指向底层数组的指针。因此,Read函数可以接受切片参数,而不是一个指针和一个计数;切片中的长度设置了一个读取数据的上限。这里是包os中File类型的Read方法的签名。
func (f *File) Read(buf []byte) (n int, err error)
该方法返回读取的字节数和一个错误值(如果有的话)。要读入一个较大的缓冲区buf的前32个字节,需要对缓冲区进行分片(这里用作动词)。
n, err := f.Read(buf[0:32])
这样的分片很常见,也很高效。事实上,暂且不说效率,下面的代码段也会读取缓冲区的前32个字节。
var n int
var err error
for i := 0; i < 32; i++ {
nbytes, e := f.Read(buf[i:i+1]) // Read one byte.
n += nbytes
if nbytes == 0 || e != nil {
err = e
break
}
}
分片的长度可以改变,只要它仍然适合于底层数组的限制;只要把它分配给自己的一个分片即可。分片的容量,可以通过内置函数cap访问,报告了分片可能承担的最大长度。这里是一个将数据追加到分片的函数。如果数据超过了容量,分片将被重新分配。返回结果的分片。该函数利用len和cap应用于nil slice时是合法的,并返回0。
func Append(slice, data []byte) []byte {
l := len(slice)
if l + len(data) > cap(slice) { // reallocate
// Allocate double what's needed, for future growth.
newSlice := make([]byte, (l+len(data))*2)
// The copy function is predeclared and works for any slice type.
copy(newSlice, slice)
slice = newSlice
}
slice = slice[0:l+len(data)]
copy(slice[l:], data)
return slice
}
我们必须在事后返回slice,因为虽然Append可以修改slice的元素,但slice本身(持有指针、长度和容量的运行时数据结构)是通过值传递的。
对 slice 进行追加的想法非常有用,它被 append 内置函数所捕获。不过要理解那个函数的设计,我们需要更多的信息,所以我们稍后会回到它。
Two-dimensional slices
https://golang.org/doc/effective_go.html#two_dimensional_slices
TODO:后面再细看
参考资料
5.7 - 结构体类型
Struct
摘录自 go语言实战
结构体是字段的集合。结构体定义的语法:
type Vertex struct {
X int
Y int
}
访问范围通过结构体和字段名的首字母大小写来体现:大写为public,小写为private。
构建结构体
可以通过值列表给结构体的各个字段赋值,新分配一个结构体,顺序和结构体定义的字段顺序一致。也可以通过使用 Name:
语法为单个字段赋值,未明确赋值的字段则取缺省值。
var (
v1 = Vertex{1, 2} // 类型为 Vertex
v2 = Vertex{X: 1} // Y:0 被省略
v3 = Vertex{} // X:0 和 Y:0
p = &Vertex{1, 2} // 类型为 *Vertex
)
特殊的前缀 &
返回一个指向结构体的指针。
访问字段
通过点号访问结构体的字段:
v := Vertex{1, 2}
v.X = 4
fmt.Println(v.X)
也可以通过指针访问:
v := Vertex{1, 2}
p := &v
p.X = 1e9
fmt.Println(v)
Struct types
https://golang.org/ref/spec#Struct_types
结构体是命名元素(称为字段)的序列,每个字段都有name/名称和type/类型。字段名可以显式(IdentifierList)或隐式(EmbeddedField)指定。在结构体中,非空白的字段名必须是唯一的。
StructType = "struct" "{" { FieldDecl ";" } "}" .
FieldDecl = (IdentifierList Type | EmbeddedField) [ Tag ] .
EmbeddedField = [ "*" ] TypeName .
Tag = string_lit .
// An empty struct.
struct {}
// A struct with 6 fields.
struct {
x, y int
u float32
_ float32 // padding
A *[]int
F func()
}
一个声明了类型但没有显式字段名的字段称为嵌入式字段。嵌入式字段必须指定为类型名T或指向非接口类型名*T的指针,T本身不能是指针类型。非限定的类型名作为字段名。
// A struct with four embedded fields of types T1, *T2, P.T3 and *P.T4
struct {
T1 // field name is T1
*T2 // field name is T2
P.T3 // field name is T3
*P.T4 // field name is T4
x, y int // field names are x and y
}
以下声明是非法的,因为字段名在结构类型中必须是唯一的。
struct {
T // conflicts with embedded field *T and *P.T
*T // conflicts with embedded field T and *P.T
*P.T // conflicts with embedded field T and *T
}
如果x.f是表示该字段或方法f的合法选择器,那么结构体x中嵌入字段的字段或方法f被称为promoted(提升)。
被提升的字段与结构体的普通字段一样,只是它们不能在结构体的复合字面量中作为字段名使用。
给定一个结构类型S和一个已定义类型T,被提升的方法被包含在结构体的方法集中,具体如下:
- 如果S包含一个嵌入字段T,那么S和
*S
的方法集都包含有接收者T的提升方法。*S的方法集还包括具有接收者*T
的提升方法。 - 如果S包含一个内嵌字段
*T
,那么S和*S
的方法集都包含有接收者T或*T
的被提升方法。
字段声明后面可以有一个可选的字符串字面量标签(tag),它成为对应字段声明中所有字段的属性。空的标签字符串相当于一个不存在的标签。标签通过反射接口变得可见,并参与结构体的类型识别,但在其他方面被忽略。
struct {
x, y float64 "" // an empty tag string is like an absent tag
name string "any string is permitted as a tag"
_ [4]byte "ceci n'est pas un champ de structure"
}
// A struct corresponding to a TimeStamp protocol buffer.
// The tag strings define the protocol buffer field numbers;
// they follow the convention outlined by the reflect package.
struct {
microsec uint64 `protobuf:"1"`
serverIP6 uint64 `protobuf:"2"`
}
参考资料
5.8 - 指针类型
Pointer types
https://golang.org/ref/spec#Pointer_types
PointerType = "*" BaseType .
BaseType = Type .
*Point
*[4]int
参考资料
5.9 - 函数类型
Function types
https://golang.org/ref/spec#Function_types
函数类型表示具有相同参数和结果类型的所有函数的集合。未初始化的函数类型变量的值为零。
FunctionType = "func" Signature .
Signature = Parameters [ Result ] .
Result = Parameters | Type .
Parameters = "(" [ ParameterList [ "," ] ] ")" .
ParameterList = ParameterDecl { "," ParameterDecl } .
ParameterDecl = [ IdentifierList ] [ "..." ] Type .
在参数或结果的列表中,名称(IdentifierList)必须全部存在或全部不存在。如果存在,每个名称代表指定类型的一个项目(参数或结果),并且签名中所有非空白名称必须是唯一的。如果不存在,每个类型代表该类型的一个项目。参数和结果列表总是用括号表示,但如果正好有一个未命名的结果,则可以写成一个未括号的类型。
在函数签名中,最后一个输入的参数可以有一个以…为前缀的类型。带有这样参数的函数被称为 variadic 可变参数,可以用零或多个参数来调用该参数。
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)
5.10 - 接口类型
Interface types
https://golang.org/ref/spec#Interface_types
接口类型指定了一个称为 interface/接口的方法集。接口类型的变量可以存储任何类型的值,其方法集是接口的任何超集。这样的类型被称为实现了接口。未初始化的接口类型变量的值是nil。
InterfaceType = "interface" "{" { ( MethodSpec | InterfaceTypeName ) ";" } "}" .
MethodSpec = MethodName Signature .
MethodName = identifier .
InterfaceTypeName = TypeName .
接口类型可以通过方法规范明确地指定方法,也可以通过接口类型名称嵌入其他接口的方法。
// A simple File interface.
interface {
Read([]byte) (int, error)
Write([]byte) (int, error)
Close() error
}
每个显式指定方法的名称必须是唯一的,不能是空白的。
interface {
String() string
String() string // illegal: String not unique
_(x int) // illegal: method must have non-blank name
}
可以有多个类型实现一个接口。例如,如果两个类型S1和S2的方法集为
func (p T) Read(p []byte) (n int, err error)
func (p T) Write(p []byte) (n int, err error)
func (p T) Close() error
(其中T代表S1或S2),那么File接口是由S1和S2同时实现的,不管S1和S2可能有什么其他方法或共享什么方法。
类型实现了由其方法的任何子集组成的任何接口,因此可以实现几个不同的接口。例如,所有类型都实现空接口。
interface{}
类似地,考虑这个接口规范,它出现在一个类型声明中,定义了一个叫做Locker的接口。
type Locker interface {
Lock()
Unlock()
}
如果S1和S2也实现:
func (p T) Lock() { … }
func (p T) Unlock() { … }
它们实现了Locker接口和File接口。
接口T可以使用一个(可能是限定的)接口类型名E来代替方法规范。这就是所谓的在T中嵌入(embedding)接口E。 T的方法集是T的显式声明方法和T的嵌入式接口的方法集的联合。
type Reader interface {
Read(p []byte) (n int, err error)
Close() error
}
type Writer interface {
Write(p []byte) (n int, err error)
Close() error
}
// ReadWriter's methods are Read, Write, and Close.
type ReadWriter interface {
Reader // includes methods of Reader in ReadWriter's method set
Writer // includes methods of Writer in ReadWriter's method set
}
方法集的联合体包含了每个方法集的(导出的和未导出的)方法,每个方法集只有一次,而且名称相同的方法必须有相同的签名。
type ReadCloser interface {
Reader // includes methods of Reader in ReadCloser's method set
Close() // illegal: signatures of Reader.Close and Close are different
}
接口类型T不得将自己或任何嵌入T的接口类型,递归地嵌入。
// illegal: Bad cannot embed itself
type Bad interface {
Bad
}
// illegal: Bad1 cannot embed itself using Bad2
type Bad1 interface {
Bad2
}
type Bad2 interface {
Bad1
}
Interface
https://golang.org/doc/effective_go.html#interfaces
Go中的接口提供了一种指定对象行为的方法:如果某个东西可以做这个,那么它就可以在这里使用。我们已经看到了几个简单的例子;自定义的打印机可以通过一个String方法实现,而Fprintf可以通过一个Write方法向任何东西生成输出。只有一个或两个方法的接口在Go代码中很常见,通常会被赋予一个由方法派生的名称,比如io.Writer用于实现Write方法的东西。
类型可以实现多个接口。例如,一个集合如果实现了 sort.Interface,就可以通过包 sort 中的例程进行排序,其中包含 Len()、Less(i, j int) bool 和 Swap(i, j int),它还可以有一个自定义的格式器。在这个人为的例子中,Sequence同时满足这两个条件。
type Sequence []int
// Methods required by sort.Interface.
func (s Sequence) Len() int {
return len(s)
}
func (s Sequence) Less(i, j int) bool {
return s[i] < s[j]
}
func (s Sequence) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
// Copy returns a copy of the Sequence.
func (s Sequence) Copy() Sequence {
copy := make(Sequence, 0, len(s))
return append(copy, s...)
}
// Method for printing - sorts the elements before printing.
func (s Sequence) String() string {
s = s.Copy() // Make a copy; don't overwrite argument.
sort.Sort(s)
str := "["
for i, elem := range s { // Loop is O(N²); will fix that in next example.
if i > 0 {
str += " "
}
str += fmt.Sprint(elem)
}
return str + "]"
}
5.11 - Map类型
Map
备注:内容摘录自 go语言实战
map 在使用之前必须用 make
而不是 new
来创建;值为 nil
的 map 是空的,并且不能赋值。
m := make(map[string]string) // 语法是 "map[key的类型]value的类型"
m["key1"] = "value1"
fmt.Println(m["key1"])
或者直接通过指定key、value来创建:
var m = map[string]string{
"key1": "value1",
"key2": "value2",
"key3": "value3", // 注意最后一行的结尾也必须有逗号
}
value的类型可以忽略,比如下面这种写法:
var m = map[string]Vertex{
"Bell Labs": Vertex{
40.68433, -74.39967,
},
"Google": Vertex{
37.42202, -122.08408,
},
}
可以简化为:
var m = map[string]Vertex{
"Bell Labs": {40.68433, -74.39967},
"Google": {37.42202, -122.08408},
}
修改 map:
func main() {
m := make(map[string]int)
m["Answer"] = 42 //在 map 中插入或修改
fmt.Println("The value:", m["Answer"]) // 通过key获取值
m["Answer"] = 48 //在 map 中插入或修改
fmt.Println("The value:", m["Answer"])
delete(m, "Answer") // 从 map 中删除key
fmt.Println("The value:", m["Answer"])
v, ok := m["Answer"] // 双赋值检测某个键存在,如果存在则第二个参数返回true
fmt.Println("The value:", v, "Present?", ok)
}
Map types
https://golang.org/ref/spec#Map_types
Map是由一种类型(称为元素类型)的元素组成的无序组,由另一种类型的一组唯一键(称为键类型)索引。未初始化的map的值为nil。
MapType = "map" "[" KeyType "]" ElementType .
KeyType = Type .
比较运算符 == 和 != 必须为键类型的操作数完全定义;因此键类型不能是函数、映射或分片。如果键类型是接口类型,必须为动态键值定义这些比较运算符,否则会引起运行时的恐慌。
map[string]int
map[*T]struct{ x, y float64 }
map[string]interface{}
Map元素的数量称为其长度。对于map m来说,它可以通过内置函数len来发现,并且在执行过程中可能会改变。在执行过程中可以使用赋值来添加元素,并使用索引表达式来检索;可以使用内置的 delete 函数来删除元素。
使用内置函数make制作一个新的、空的map值,该函数将map类型和一个可选的容量提示作为参数。
make(map[string]int)
make(map[string]int, 100)
初始容量并不约束其大小:为了容纳其中存储的项目数量map可以增长,但 nil map除外。nil map相当于一个空map,但不能添加任何元素。
Maps
https://golang.org/doc/effective_go.html#maps
Map是一种方便而强大的内置数据结构,它将一种类型的值(键)与另一种类型的值(元素或值)关联起来。键可以是任何定义了 equality 运算符的类型,如整数、浮点和复数、字符串、指针、接口(只要动态类型支持equality)、结构体和数组。切片不能用作映射键,因为在它们上面没有定义equality。和切片一样,map 也持有对底层数据结构的引用。如果你把一个map传给一个函数,该函数改变了map的内容,那么这些改变将在调用者中可见。
Map可以使用通常的复合文字语法和冒号分隔的键值对来构建,所以在初始化时很容易构建它们。
var timeZone = map[string]int{
"UTC": 0*60*60,
"EST": -5*60*60,
"CST": -6*60*60,
"MST": -7*60*60,
"PST": -8*60*60,
}
分配和获取映射值在语法上看起来就像对数组和切片做同样的事情一样,只是索引不需要是一个整数。
offset := timeZone["EST"]
试图用map中不存在的键来获取一个map值,将返回map中条目类型的0值。例如,如果map中包含整数,查找一个不存在的键将返回0。集合可以实现为一个值类型为bool的map。将map条目设置为true,将值放入集合中,然后通过简单的索引进行测试。
attended := map[string]bool{
"Ann": true,
"Joe": true,
...
}
if attended[person] { // will be false if person is not in the map
fmt.Println(person, "was at the meeting")
}
有时你需要区分缺失的条目和零值。是否有 “UTC “的条目,还是因为map中根本没有这个条目而为0?你可以用一种多重赋值的形式来区分。
var seconds int
var ok bool
seconds, ok = timeZone[tz]
出于明显的原因,这被称为 “逗号 ok “习惯用法。在这个例子中,如果tz存在,秒数将被适当设置,ok将为true;如果不存在,秒数将被设置为0,ok将为false。这里有一个函数,把它和一个漂亮的错误报告放在一起。
func offset(tz string) int {
if seconds, ok := timeZone[tz]; ok {
return seconds
}
log.Println("unknown time zone:", tz)
return 0
}
为了测试map中是否存在,而不用担心实际值,你可以使用空白标识符(_)来代替通常的变量的值。
_, present := timeZone[tz]
要删除一个map条目,使用delete内置函数,其参数是map和要删除的键。即使key已经不在map上,这样做也是安全的。
delete(timeZone, "PDT") // Now on Standard Time
参考资料
5.12 - Channel类型
Channel types
https://golang.org/ref/spec#Channel_types
通道为并发执行的函数提供了一种机制,通过发送和接收指定元素类型的值进行通信。未初始化的通道的值为nil。
ChannelType = ( "chan" | "chan" "<-" | "<-" "chan" ) ElementType .
可选的 <-
操作符指定通道方向,发送或接收。如果没有给出方向,则通道是双向的。通道可以通过赋值或显式转换来限制只能发送或只能接收。
chan T // can be used to send and receive values of type T
chan<- float64 // can only be used to send float64s
<-chan int // can only be used to receive ints
<-操作符与最左边的chan可能关联。
chan<- chan int // same as chan<- (chan int)
chan<- <-chan int // same as chan<- (<-chan int)
<-chan <-chan int // same as <-chan (<-chan int)
chan (<-chan int)
可以使用内置的函数make制作一个新的、初始化的通道值,该函数将通道类型和一个可选的容量作为参数。
make(chan int, 100)
capacity,以元素数为单位,设置通道中缓冲区的大小。如果容量为零或不存在,则通道是无缓冲的,只有当发送方和接收方都准备好时,通信才会成功。否则,通道被缓冲,如果缓冲区没有满(发送)或不空(接收),则通信成功而不阻塞。nil通道永远不会为通信做好准备。
可以用内置函数close关闭通道。接收运算符的多值赋值形式报告在通道关闭前是否有接收值被发送。
单个通道可以用于发送语句、接收操作,以及任何数量的goroutine对内置函数cap和len的调用,而无需进一步同步。通道是先入先出(first-in-first-out)的队列。例如,如果goroutine在通道上发送数值,而第二个goroutine接收数值,那么数值将按照发送的顺序接收。
6 - 表达式
Expressions
https://golang.org/ref/spec#Expressions
表达式指定了通过对操作数应用运算符和函数来计算值。
操作数
操作数表示表达式中的基本值。操作数可以是字面量,一个代表常量、变量、函数的非空标识符(可能是限定的),或者带括号内的表达式。
空白的标识符只能作为操作数出现在赋值的左侧。
Operand = Literal | OperandName | "(" Expression ")" .
Literal = BasicLit | CompositeLit | FunctionLit .
BasicLit = int_lit | float_lit | imaginary_lit | rune_lit | string_lit .
OperandName = identifier | QualifiedIdent .
Qualified identifiers
限定的标识符是用包名前缀限定的标识符。包名和标识符都不能为空。
QualifiedIdent = PackageName "." identifier .
限定的标识符访问不同包中的标识符,这个标识符必须被导入。该标识符必须被导出,并在该包的包块中声明。
math.Sin // denotes the Sin function in package math
7 - 语句
7.1 - 语句概述
https://golang.org/ref/spec#Statements
语句控制执行。
Statement =
Declaration | LabeledStmt | SimpleStmt |
GoStmt | ReturnStmt | BreakStmt | ContinueStmt | GotoStmt |
FallthroughStmt | Block | IfStmt | SwitchStmt | SelectStmt | ForStmt |
DeferStmt .
SimpleStmt = EmptyStmt | ExpressionStmt | SendStmt | IncDecStmt | Assignment | ShortVarDecl .
7.2 - 终止语句
终止语句
https://golang.org/ref/spec#Terminating_statements
终止语句可以防止执行同一块中在它之后出现的所有词法上的语句。以下语句是终止语句。
- “return “或 “goto “语句。
- 对内置函数panic的调用。
- 语句列表以终止语句结束的块。
- “if “语句,其中
- “else “分支存在,并且
- 两个分支都是终止语句。
- “for “语句,其中。
- 没有指向 “for “语句的 “break “语句,并且:
- 循环条件不存在。
- “switch “语句,其中。
- 没有 “break “语句指的是 “switch “语句。
- 有一个default case,和
- 语句列表中的每一种情况,包括默认情况,都以终止语句结束,或可能标有 “fallthrough"语句。
- “select"语句,其中:
- 没有指向 “select “语句的 “break “语句,并且:
- 在每种情况下,包括默认情况下的语句列表,都以终止语句结束。
- 标签语句标注一个终止语句。
所有其他语句都不是终止语句。
如果语句列表不是空的,并且它的最后一条非空语句是终止语句,则该语句列表以终止语句结束。
7.3 - 表达式语句
表达式语句
https://golang.org/ref/spec#Expression_statements
除了特定的内置函数外,函数和方法的调用以及接收操作都可以在语句上下文中出现。这种语句可以用括号。
ExpressionStmt = Expression .
语句上下文中不允许使用以下内置函数:
append cap complex imag len make new real
unsafe.Alignof unsafe.Offsetof unsafe.Sizeof
h(x+y)
f.Close()
<-ch
(<-ch)
len("foo") // illegal if len is the built-in function
7.4 - Send语句
Send语句
https://golang.org/ref/spec#Send_statements
发送语句在通道上发送一个值。通道表达式必须是通道类型,通道方向必须允许发送操作,要发送的值的类型必须可以分配给通道的元素类型。
SendStmt = Channel "<-" Expression .
Channel = Expression .
在通信开始之前,通道和值表达式都会被评估。通信会被阻塞,直到发送可以继续进行。如果接收者准备好了,在无缓冲通道上的发送就可以进行。在缓冲通道上的发送,如果缓冲区有空间,就可以进行。在已关闭通道上的发送会引起运行时恐慌。在nil通道上的发送会永远阻塞。
ch <- 3 // send value 3 to channel ch
7.5 - 自增自减语句
IncDec/自增自减语句
“++“和”–“语句以无类型常数1来递增或递减操作数。和赋值一样,操作数必须是可寻址的(addressable),或者是一个映射索引表达式。
IncDecStmt = Expression ( "++" | "--" ) .
下列赋值语句在语义上是等价的:
IncDec statement Assignment
x++ x += 1
x-- x -= 1
7.6 - 赋值语句
Assignment/赋值
https://golang.org/ref/spec#Assignments
Assignment = ExpressionList assign_op ExpressionList .
assign_op = [ add_op | mul_op ] "=" .
每个左操作数必须是可寻址的(addressable),是一个映射索引表达式,或者是(仅对于=赋值)空白标识符。操作数可以加括号。
x = 1
*p = f()
a[i] = 23
(k) = <-ch // same as: k = <-ch
赋值操作 x op= y
,其中op是一个二进制算术运算符,相当于 x = x op (y)
,但只对x进行一次评估。op=
结构是一个单一的Token。在赋值操作中,左手和右手的表达式列表必须正好包含一个单值表达式,而且左手表达式不能是空白标识符。
a[i] <<= 2
i &^= 1<<n
元组(tuple)赋值将多值操作的各个元素赋值到变量列表中。有两种形式。在第一种形式中,右手的操作数是一个单一的多值表达式,如函数调用、通道或map操作、或类型断言。左手操作数的数量必须与值的数量相匹配。例如,如果f是一个返回两个值的函数。
x, y = f()
将第一个值赋给x,第二个值赋给y,在第二种形式中,左边的操作数必须等于右边的表达式数量,每个表达式必须是单值,右边的第n个表达式被赋给左边的第n个操作数。
one, two, three = '一', '二', '三'
空白标识符提供了一种在赋值中忽略右侧值的方法。
_ = x // evaluate x but ignore it
x, _ = f() // evaluate f() but ignore second result value
赋值分两个阶段进行。首先,左边的索引表达式的操作数和指针直指(包括选择器中的隐式指针直指)以及右边的表达式都按照通常的顺序进行评估。第二,按照从左到右的顺序进行赋值。
a, b = b, a // exchange a and b
x := []int{1, 2, 3}
i := 0
i, x[i] = 1, 2 // set i = 1, x[0] = 2
i = 0
x[i], i = 2, 1 // set x[0] = 2, i = 1
x[0], x[0] = 1, 2 // set x[0] = 1, then x[0] = 2 (so x[0] == 2 at end)
x[1], x[3] = 4, 5 // set x[1] = 4, then panic setting x[3] = 5.
type Point struct { x, y int }
var p *Point
x[2], p.x = 6, 7 // set x[2] = 6, then panic setting p.x = 7
i = 2
x = []int{3, 5, 7}
for i, x[i] = range x { // set i, x[2] = 0, x[0]
break
}
// after this loop, i == 0 and x == []int{3, 5, 3}
在赋值中,每个值都必须可以赋给被赋值的操作数的类型,但有以下特殊情况:
- 任何类型的值都可以分配给空白标识符。
- 如果一个非类型的常量被赋值给接口类型的变量或空白标识符,那么该常量首先被隐式转换为其默认类型。
- 如果一个非类型的布尔值被分配给接口类型的变量或空白标识符,它首先被隐式转换为布尔类型。
8 - 控制流程
流程控制语句属于特殊的语句。
8.1 - if 语句
If语句
类似for循环,go中的if语句也是同样,if后面不能有括号,而if里面必须要有花括号:
if x < 0 {
return sqrt(-x) + "i"
}
跟 for
一样,if
语句可以在条件之前执行一个简单的语句:
if v := math.Pow(x, n); v < lim {
// v在这里可以访问
return v
}
// v在这里不可以访问
注意:这个语句定义的变量的作用域仅在 if
范围之内,包括else:
if v := math.Pow(x, n); v < lim {
return v
} else {
// else这里可以访问
fmt.Printf("%g >= %g\n", v, lim)
}
If statements
https://golang.org/ref/spec#If_statements
“if “语句根据布尔表达式的值指定两个分支的条件执行。如果表达式的值为真,则执行 “if “分支,否则,如果存在,则执行 “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
}
if
https://golang.org/doc/effective_go.html#if
在 go 中,一个简单的if是这样的:
if x > 0 {
return y
}
强制括号鼓励在多行上写简单的if语句。无论如何这样做都是好的风格,特别是当正文中包含一个控制语句,如返回或break时。
由于if和switch接受一个初始化语句,所以通常会看到一个用来设置局部变量的语句。
if err := file.Chmod(0664); err != nil {
log.Print(err)
return err
}
在 Go 库中,你会发现,当一个 if 语句没有流入下一个语句时–也就是说,正文以 break、continue、goto 或 return 结尾时,不必要的 else 会被省略。
f, err := os.Open(name)
if err != nil {
return err
}
codeUsing(f)
这是一个常见情况的例子,代码必须防范一连串的错误情况。如果成功的控制流向下运行,在出现错误情况时消除错误情况,那么代码的阅读效果就会很好。由于错误情况往往以return语句结束,因此产生的代码不需要 else语句。
f, err := os.Open(name)
if err != nil {
return err
}
d, err := f.Stat()
if err != nil {
f.Close()
return err
}
codeUsing(f, d)
8.2 - switch 语句
go语言实战
switch的语法和for、if类似,同样的括号和花括号使用规则,同样的容许在switch前执行一个简单的语句,同样的变量访问范围限制:
switch os := runtime.GOOS; os {
case "darwin":
fmt.Println("OS X.")
case "linux":
fmt.Println("Linux.")
default:
fmt.Printf("%s.", os)
}
特别需要支出的是,和c、java中的switch语句不同,golang中的switch在命中某个case子语句并执行完成之后,会自动终结分支并结束switch语句。这是默认行为,和c,java中会自动继续下一个分支匹配,需要明确break才能退出不同。
如果想继续执行后面的case子语句,需要在case子语句最后使用 fallthrough
语句。
执行顺序
switch 的条件从上到下顺序执行,当匹配成功的时候终止。
switch i {
case 0:
case f():
}
当 i==0
时不会调用 f
。
if变体
没有条件的 switch 等同于 switch true
,这个变体可以用更清晰的形式来编写多个判断条件的 if 语句:
t := time.Now()
// 等同于 switch true {
switch {
case t.Hour() < 12:
fmt.Println("Good morning!")
case t.Hour() < 17:
fmt.Println("Good afternoon.")
default:
fmt.Println("Good evening.")
}
golang语言规范
https://golang.org/ref/spec#Switch_statements
“switch “语句提供多向执行。表达式或类型指定符与 “switch “内的 “case “进行比较,以确定执行哪个分支。
SwitchStmt = ExprSwitchStmt | TypeSwitchStmt .
有两种形式:表达式switch和类型switch。在表达式switch中,case包含表达式,与switch表达式的值进行比较。在类型switch中,case中包含的类型是与特别注释的switch表达式的类型进行比较的。switch表达式在一个switch语句中只被评估一次。
表达式switch
在表达式switch中,对switch表达式进行评估,并从左到右、从上到下评估case表达式,case表达式不一定是常量,第一个等于switch表达式的会触发执行相关case的语句,其他case则跳过。如果没有匹配的情况,并且有一个 “default"情况,则执行它的语句。最多只能有一个 default case,它可能出现在 “switch “语句的任何地方。缺少的switch表达式相当于布尔值true。
ExprSwitchStmt = "switch" [ SimpleStmt ";" ] [ Expression ] "{" { ExprCaseClause } "}" .
ExprCaseClause = ExprSwitchCase ":" StatementList .
ExprSwitchCase = "case" ExpressionList | "default" .
如果switch表达式的值是一个无类型常量,则首先隐式转换为其默认类型;如果是一个无类型布尔值,则首先隐式转换为布尔类型。预先声明的无类型值nil不能作为switch表达式使用。
如果一个case表达式是无类型的,它首先被隐式转换为switch表达式的类型。对于每个(可能转换的)case表达式x和switch表达式的值t,x == t必须是一个有效的比较。
换句话说,switch表达式被当作是用来声明和初始化一个没有显式类型的临时变量t;正是t的那个值与每个case表达式x进行了相等性测试。
在一个case或default子句中,最后一个非空语句可以是一个(可能被标记为)“fallthrough “语句,以表明控制权应该从这个子句的末尾流向下一个子句的第一个语句。否则控制流向 “switch “语句的末尾。“fallthrough “语句可以作为一个表达式switch的所有分句的最后一条语句出现,但最后一个分句除外。
switch表达式之前可以有一个简单的语句,该语句在表达式被评估之前执行。
switch tag {
default: s3()
case 0, 1, 2, 3: s1()
case 4, 5, 6, 7: s2()
}
switch x := f(); { // missing switch expression means "true"
case x < 0: return -x
default: return x
}
switch {
case x < y: f1()
case x < z: f2()
case x == 4: f3()
}
实现限制。编译器可能不允许多个case表达式求值于同一个常量。例如,目前的编译器不允许在case表达式中使用重复的整数、浮点或字符串常量。
类型switch
类型switch比较的是类型而不是值。在其他方面,它类似于表达式switch。它由一个特殊的switch表达式标记,它具有使用保留字类型而不是实际类型的类型断言形式。
switch x.(type) {
// cases
}
然后,case将实际类型T与表达式x的动态类型进行匹配,与类型断言一样,x必须是接口类型,案例中列出的每个非接口类型T必须实现x的类型,类型switch的case中列出的类型必须全部不同。
TypeSwitchStmt = "switch" [ SimpleStmt ";" ] TypeSwitchGuard "{" { TypeCaseClause } "}" .
TypeSwitchGuard = [ identifier ":=" ] PrimaryExpr "." "(" "type" ")" .
TypeCaseClause = TypeSwitchCase ":" StatementList .
TypeSwitchCase = "case" TypeList | "default" .
TypeList = Type { "," Type } .
TypeSwitchGuard可以包括一个简短的变量声明。当使用这种形式时,变量被声明在每个子句隐含块的TypeSwitchCase的末尾。在有一个case的子句中,正好列出一个类型,变量具有该类型;否则,变量具有TypeSwitchGuard中表达式的类型。
case可以使用预先声明的标识符nil代替类型;当TypeSwitchGuard中的表达式是一个nil接口值时,就会选择该case。最多可以有一个nil case。
给定一个类型为interface{}的表达式x,下面的类型switch。
switch i := x.(type) {
case nil:
printString("x is nil") // type of i is type of x (interface{})
case int:
printInt(i) // type of i is int
case float64:
printFloat64(i) // type of i is float64
case func(int) float64:
printFunction(i) // type of i is func(int) float64
case bool, string:
printString("type is bool or string") // type of i is type of x (interface{})
default:
printString("don't know the type") // type of i is type of x (interface{})
}
可以重写:
v := x // x is evaluated exactly once
if v == nil {
i := v // type of i is type of x (interface{})
printString("x is nil")
} else if i, isInt := v.(int); isInt {
printInt(i) // type of i is int
} else if i, isFloat64 := v.(float64); isFloat64 {
printFloat64(i) // type of i is float64
} else if i, isFunc := v.(func(int) float64); isFunc {
printFunction(i) // type of i is func(int) float64
} else {
_, isBool := v.(bool)
_, isString := v.(string)
if isBool || isString {
i := v // type of i is type of x (interface{})
printString("type is bool or string")
} else {
i := v // type of i is type of x (interface{})
printString("don't know the type")
}
}
在类型switch保护之前可以有一个简单的语句,在保护被评估之前执行。
在类型转换中不允许使用 “fallthrough “语句。
Effective Go
https://golang.org/doc/effective_go.html#switch
go的switch比C语言的switch更通用。表达式不需要是常数,甚至不需要是整数,case从上到下进行评估,直到找到匹配的情况,如果switch没有表达式,它就会切换到true。因此,把一个if-else-if-else链写成switch是可能的,也是很习惯的。
func unhex(c byte) byte {
switch {
case '0' <= c && c <= '9':
return c - '0'
case 'a' <= c && c <= 'f':
return c - 'a' + 10
case 'A' <= c && c <= 'F':
return c - 'A' + 10
}
return 0
}
没有自动跌破,但 case 可以用逗号分隔的列表呈现。
func shouldEscape(c byte) bool {
switch c {
case ' ', '?', '&', '=', '#', '+', '%':
return true
}
return false
}
虽然它们在Go中并不像其他一些类似C语言那样常见,但break语句可以用来提前终止一个开关。不过有时候,需要脱离周围的循环,而不是switch,在Go中,可以通过给循环加上一个标签,然后对这个标签进行 “break “来实现。这个例子展示了这两种用法。
Loop:
for n := 0; n < len(src); n += size {
switch {
case src[n] < sizeOne:
if validateOnly {
break
}
size = 1
update(src[n])
case src[n] < sizeTwo:
if n+1 >= len(src) {
err = errShortInput
break Loop
}
if validateOnly {
break
}
size = 2
update(src[n] + src[n+1]<<shift)
}
}
当然,continue语句也接受一个可选的标签,但它只适用于循环。
在本节的最后,这里有一个 byte slice 的比较例程,它使用了两个switch语句。
// Compare returns an integer comparing the two byte slices,
// lexicographically.
// The result will be 0 if a == b, -1 if a < b, and +1 if a > b
func Compare(a, b []byte) int {
for i := 0; i < len(a) && i < len(b); i++ {
switch {
case a[i] > b[i]:
return 1
case a[i] < b[i]:
return -1
}
}
switch {
case len(a) > len(b):
return 1
case len(a) < len(b):
return -1
}
return 0
}
类型switch
也可以使用swtich来发现接口变量的动态类型。这样的类型切换使用了类型断言的语法,括号内有关键字type。如果switch在表达式中声明了一个变量,那么该变量在每个子句中都会有相应的类型。在这种情况下重用名称也是一种习惯,实际上是在每个情况下声明一个新的变量,名称相同,但类型不同。
var t interface{}
t = functionOfSomeType()
switch t := t.(type) {
default:
fmt.Printf("unexpected type %T\n", t) // %T prints whatever type t has
case bool:
fmt.Printf("boolean %t\n", t) // t has type bool
case int:
fmt.Printf("integer %d\n", t) // t has type int
case *bool:
fmt.Printf("pointer to boolean %t\n", *t) // t has type *bool
case *int:
fmt.Printf("pointer to integer %d\n", *t) // t has type *int
}
8.3 - for 语句
go语言实战
for loop
Go 只有一种循环结构 for
循环。
sum := 0
for i := 0; i < 10; i++ {
sum += i
}
和Java的语法相反:
- for 后面没有括号
()
,注意是强制一定不能有 - 循环体必须有
{}
跟 C 或者 Java 中一样,可以让前置、后置语句为空:
sum := 1
for ; sum < 1000; {
sum += sum
}
这就非常类似C、Java中的while循环了,因此干脆继续简写,省略掉分括号:
sum := 1
for sum < 1000 {
sum += sum
}
更绝一点,死循环:
for {
}
golang语言规范
https://golang.org/ref/spec#For_statements
“for “语句指定重复执行块。有三种形式。迭代可以由一个条件(condition)、一个 “for “子句或一个 “range “子句控制。
ForStmt = "for" [ Condition | ForClause | RangeClause ] Block .
Condition = Expression .
单一条件的 for 语句
在最简单的形式中,“for “语句指定重复执行一个块,只要一个布尔条件评估为真。该条件在每次迭代前都会被评估。如果条件不存在,则相当于布尔值为true。
for a < b {
a *= 2
}
带有for子句的for语句
带有 ForClause 的 “for “语句也是由它的条件(condition)控制的,但除此之外,它还可以指定一个 init 和一个post语句,如赋值、增量或减量语句。init 语句可以是一个简短的变量声明,但post语句不能。init语句声明的变量在每次迭代中都会被重复使用。
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() }
Effective Go
https://golang.org/doc/effective_go.html#for
Go for循环与C的循环类似-但不一样。它统一了for和while,而且没有do-while。有三种形式,其中只有一种有分号。
// Like a C for (除了不容许用括号)
for init; condition; post { }
// Like a C while
for condition { }
// Like a C for(;;)
for { }
短声明使其很容易在循环中直接声明索引变量。
sum := 0
for i := 0; i < 10; i++ {
sum += i
}
最后,Go没有逗号运算符,++和–是语句而不是表达式。因此,如果你想在for中运行多个变量,你应该使用并行赋值(尽管这排除了++和–)。
// Reverse a
for i, j := 0, len(a)-1; i < j; i, j = i+1, j-1 {
a[i], a[j] = a[j], a[i]
}
8.4 - for range 语句
go语言规范
For statements with range
clause
https://golang.org/ref/spec#For_statements
带有 “range"子句的 “for” 语句会遍历数组、切片、字符串或map的所有条目,或通道(channel)上接收的值。对于每个条目,如果存在,它将迭代值分配给相应的迭代变量,然后执行该块。
RangeClause = [ ExpressionList "=" | IdentifierList ":=" ] "range" Expression .
“range"子句中右边的表达式称为range表达式,它可以是数组、数组指针、分片、字符串、map或允许接收操作的通道。与赋值一样,如果存在,左边的操作数必须是可寻址或映射索引表达式;它们表示迭代变量。如果范围表达式是通道,最多允许一个迭代变量,否则最多可以有两个。如果最后一个迭代变量是空白标识符,那么范围子句就相当于没有该标识符的同一个子句。
范围表达式x在开始循环之前被评估一次,但有一个例外:如果最多存在一个迭代变量,并且len(x)是常数,则不评估范围表达式。
左边的函数调用在每次迭代时都会被评估一次。对于每一次迭代,如果存在各自的迭代变量,就会产生如下的迭代值。
Range expression 1st value 2nd value
array or slice a [n]E, *[n]E, or []E index i int a[i] E
string s string type index i int see below rune
map m map[K]V key k K m[k] V
channel c chan E, <-chan E element e E
-
对于数组、数组指针或切片值a,从元素索引0开始,按递增顺序产生索引迭代值。如果最多存在一个迭代变量,则 range 循环产生从0到
len(a)-1
的迭代值,并且不对数组或分片本身进行索引。对于nil slice,迭代次数为0. 对于字符串值,“range循环"会产生从 0 到len(a)-1
的迭代值,并且不对数组或分片本身进行索引。 -
对于字符串值,“range"子句从字节索引 0 开始对字符串中的Unicode code point 进行迭代。在连续迭代时,索引值将是字符串中连续的UTF-8编码 code point 的第一个字节的索引,第二个值,类型为 rune,将是对应code point的值。如果迭代遇到无效的UTF-8序列,第二个值将是
0xFFFD
,即Unicode replacement(替换)字符,下一次迭代将推进字符串中的一个字节。 -
对map的迭代顺序没有指定,也不能保证每次迭代的顺序相同。如果在迭代过程中删除了一个尚未到达的map条目,将不会产生相应的迭代值。如果在迭代过程中创建了一个map条目,该条目可能在迭代过程中产生,也可能被跳过。对于每个创建的条目,以及从一个迭代到下一个迭代,选择可能不同。如果map为nil,则迭代次数为0。
-
对于通道,产生的迭代值是通道上连续发送的值,直到通道关闭。如果通道为nil,则range表达式永远阻塞。
迭代值像赋值语句一样被赋值到相应的迭代变量中。
迭代变量可以由 “range “子句使用短变量声明的形式(:=)来声明。在这种情况下,它们的类型被设置为各自迭代值的类型,它们的作用域是 “for"语句的块;它们在每次迭代中被重复使用。如果迭代变量是在 “for"语句之外声明的,那么在执行后它们的值将是最后一次迭代的值。
var testdata *struct {
a *[7]int
}
for i, _ := range testdata.a {
// testdata.a is never evaluated; len(testdata.a) is constant
// i ranges from 0 to 6
f(i)
}
var a [10]string
for i, s := range a {
// type of i is int
// type of s is string
// s == a[i]
g(i, s)
}
var key string
var val interface{} // element type of m is assignable to val
m := map[string]int{"mon":0, "tue":1, "wed":2, "thu":3, "fri":4, "sat":5, "sun":6}
for key, val = range m {
h(key, val)
}
// key == last map key encountered in iteration
// val == map[key]
var ch chan Work = producer()
for w := range ch {
doWork(w)
}
// empty a channel
for range ch {}
Effective Go
https://golang.org/doc/effective_go.html#for
如果你正在循环数组、切片、字符串或map,或者从一个通道读取,range子句可以管理循环。
for key, value := range oldMap {
newMap[key] = value
}
如果你只需要range内的第一项(键或索引),就放弃第二项。
for key := range m {
if key.expired() {
delete(m, key)
}
}
如果你只需要范围中的第二项(值),使用空白的标识符(下划线)来丢弃第一项。
sum := 0
for _, value := range array {
sum += value
}
对于字符串,range 为你做了更多的工作,通过解析UTF-8分解出各个Unicode code point。错误的编码会消耗一个字节,并产生替换符U+FFFD。(名称(与相关的内置类型) rune是单个Unicode code point的go术语。详情请参见语言规范。) 。循环
for pos, char := range "日本\x80語" { // \x80 is an illegal UTF-8 encoding
fmt.Printf("character %#U starts at byte position %d\n", char, pos)
}
打印:
character U+65E5 '日' starts at byte position 0
character U+672C '本' starts at byte position 3
character U+FFFD '�' starts at byte position 6
character U+8A9E '語' starts at byte position 7
参考资料
8.5 - range 语句
rang遍历
range
关键字用来遍历 list
、array
或者 map
。为了方便理解,可以认为 range
等效于 for earch index of
。
对于 arrays
或者 slices
, 将会返回整型的下标;
// 对于数组,rang返回index
a := [...]string{"a", "b", "c", "d"}
for i := range a {
fmt.Println("Array item", i, "is", a[i])
}
支持返回单值或者两个值, 如果返回一个值,那么为下标,否则为下标和下标所对应的值。
a := [...]string{"a", "b", "c", "d"}
for i, v := range a {
fmt.Println("Array item", i, "is", v)
}
对于 map
,将会返回下一个键值对的 key
。
// 对于map, range 返回 key
capitals := map[string] string {"France":"Paris", "Italy":"Rome", "Japan":"Tokyo" }
for key := range capitals {
fmt.Println("Map item: Capital of", key, "is", capitals[key])
}
同样支持返回两个值, 直接拿到key和对应的value:
capitals := map[string] string {"France":"Paris", "Italy":"Rome", "Japan":"Tokyo" }
for key, value := range capitals {
fmt.Println("Map item: Capital of", key, "is", value)
}
8.6 - go 语句
golang语言规范
https://golang.org/ref/spec#Go_statements
“go"语句在同一地址空间内,以独立的并发控制线程或goroutine的形式开始执行一个函数调用。
GoStmt = "go" Expression .
表达式必须是函数或方法调用,不能用括号。内建函数的调用与表达式语句一样受到限制。
函数值和参数在调用goroutine中像往常一样被评估,但与常规调用不同的是,程序的执行不会等待被调用的函数完成,而是在新的goroutine中开始独立执行。相反,函数在新的goroutine中开始独立执行。当函数终止时,它的goroutine也会终止。如果函数有任何返回值,则在函数完成时将其丢弃。
go Server()
go func(ch chan<- bool) { for { sleep(10); ch <- true }} (c)
8.7 - select 语句
go语言实战
select是Golang中的控制语句,语法类似于switch语句。但是select只用于通信,要求每个case必须是IO操作。
不带default语句的select会阻塞直到某个case满足:
ch1 := make (chan int, 1)
ch2 := make (chan int, 1)
select {
case <-ch1:
fmt.Println("ch1 pop one element")
case <-ch2:
fmt.Println("ch2 pop one element")
}
如果两个case同时满足,则随机执行某个case语句,其他case语句不会执行。
如果不想阻塞,则可以带上default子语句:
select {
case <-ch1:
fmt.Println("ch1 pop one element")
case <-ch2:
fmt.Println("ch2 pop one element")
default:
fmt.Println("not ready yet")
}
如果两个case条件都不满足,则直接跳到 default 流程而不阻塞。
golang语言规范
https://golang.org/ref/spec#Select_statements
“select"语句选择一组可能的发送或接收操作中的某一个进行。它看起来类似于 “switch"语句,但情况都是指通信操作(指IO操作)。
SelectStmt = "select" "{" { CommClause } "}" .
CommClause = CommCase ":" StatementList .
CommCase = "case" ( SendStmt | RecvStmt ) | "default" .
RecvStmt = [ ExpressionList "=" | IdentifierList ":=" ] RecvExpr .
RecvExpr = Expression .
带有RecvStmt的情况下,可以将RecvExpr的结果分配给一个或两个变量,这些变量可以使用短变量声明。RecvExpr必须是一个(可能是括号)接收操作。最多只能有一个default缺省情况,它可以出现在case列表中的任何地方。
“select “语句的执行分几个步骤进行:
-
对于语句中的所有case,在进入 “select “语句后,接收操作的通道操作数和发送语句的通道和右侧表达式都会按照源代码顺序精确地评估一次。其结果是一组可以接收或发送的通道,以及相应的发送值。无论选择哪种(如果有的话)通信操作进行,该评估中的任何副作用都会发生。RecvStmt左侧带有短变量声明或赋值的表达式尚未被评估。
-
如果有一个或多个通信可以进行,则通过统一的伪随机选择选择一个可以进行的单一通信。否则,如果有默认case,则选择该case。如果没有default case,“select “语句就会阻塞,直到至少有一个通信可以继续进行。
-
除非选择的case是默认情况,否则会执行相应的通信操作。
-
如果选择的case是一个带有短变量声明或赋值的RecvStmt,则左手边的表达式被评估,接收到的值(或数值)被赋值。
-
所选case的语句列表被执行。
由于在nul通道上的通信永远无法进行,所以只有nil通道而没有 default case 的 select 永远阻塞。
var a []int
var c, c1, c2, c3, c4 chan int
var i1, i2 int
select {
case i1 = <-c1:
print("received ", i1, " from c1\n")
case c2 <- i2:
print("sent ", i2, " to c2\n")
case i3, ok := (<-c3): // same as: i3, ok := <-c3
if ok {
print("received ", i3, " from c3\n")
} else {
print("c3 is closed\n")
}
case a[f()] = <-c4:
// same as:
// case t := <-c4
// a[f()] = t
default:
print("no communication\n")
}
for { // send random sequence of bits to c
select {
case c <- 0: // note: no statement, no fallthrough, no folding of cases
case c <- 1:
}
}
select {} // block forever
8.8 - return 语句
golang语言规范
https://golang.org/ref/spec#Return_statements
函数F中的 “return"语句会终止F的执行,并可选地提供一个或多个结果值。在F返回给调用者之前,任何被F deferred 的函数都会被执行。
ReturnStmt = "return" [ ExpressionList ] .
在没有结果类型的函数中,“return"语句不能指定任何结果值。
func noResult() {
return
}
有三种方法可以从一个具有结果类型的函数中返回值:
-
返回值可以在 “return"语句中明确列出。每个表达式必须是单值的,并且可以分配给函数结果类型的相应元素。
func simpleF() int { return 2 } func complexF1() (re float64, im float64) { return -7.0, -4.0 }
-
“return"语句中的表达式列表可以是对一个多值函数的单次调用。其效果就好比该函数返回的每个值都被分配到一个临时变量中,其类型为相应的值,然后用 “return"语句列出这些变量,这时就适用前一种情况的规则。
func complexF2() (re float64, im float64) { return complexF1() }
-
如果函数的结果类型为其结果参数指定了名称,那么表达式列表可以为空。结果参数作为普通的局部变量,函数可以根据需要为它们赋值。return 语句返回这些变量的值。
func complexF3() (re float64, im float64) { re = 7.0 im = 4.0 return } func (devnull) Write(p []byte) (n int, _ error) { n = len(p) return }
无论如何声明,所有的结果值在进入函数时都初始化为其类型的零值。指定结果的 “return” 语句会在执行任何deferred 函数之前设置结果参数。
实现限制:如果在返回的地方有不同的实体(常量、类型或变量)与结果参数同名,编译器可以不允许在 “return” 语句中使用空表达式列表。
8.9 - break 语句
golang语言规范
https://golang.org/ref/spec#Break_statements
“break “语句终止了同一函数中最内层的 “for”、“switch “或 “select “语句的执行。
BreakStmt = "break" [ Label ] .
如果有标签,则标签必须包围住 “for”、“switch “或 “select “语句,而且标签是执行终止。
8.10 - continue 语句
golang语言规范
https://golang.org/ref/spec#Continue_statements
“continue"语句在其post语句处开始最里面的 “for"循环的下一次迭代。“for"循环必须在同一个函数内。
ContinueStmt = "continue" [ Label ] .
如果有标签,则标签必须包含 “for” 语句。
RowLoop:
for y, row := range rows {
for x, data := range row {
if data == endOfRow {
continue RowLoop
}
row[x] = data + bias(x, y)
}
}
8.11 - goto 语句
golang语言规范
“goto” 语句将控制权转移到同一函数中带有相应标签的语句。
GotoStmt = "goto" Label .
goto Error
执行 “goto"语句不能导致在goto之后有任何变量进入它还没有进行的范围。例如,这个例子:
goto L // BAD
v := 3
L:
是错误的,因为跳转到标签L时跳过了v的创建。
块外的 “goto"语句不能跳转到该块内的标签。例如,这个例子:
if n%2 == 1 {
goto L1
}
for n > 0 {
f()
n--
L1:
f()
n--
}
是错误的,因为标签L1在 “for"语句的块内,而goto却不在。
8.12 - fallthrough 语句
golang语言规范
https://golang.org/ref/spec#Fallthrough_statements
“fallthrough"语句将控制权转移到 “switch” 语句中下一个case子句的第一条语句。它只能作为这种子句中的最后一个非空语句使用。
参考资料
- go语言fallthrough的用法心得:
- Go里面 switch 默认相当于每个case最后带有break,匹配成功后不会自动向下执行其他case,而是跳出整个switch, 但是可以使用fallthrough强制执行后面的case代码。
- fallthrough不能用在switch的最后一个分支
- fallthrough到下一个case块时,不执行case匹配检查!不执行case匹配检查!不执行case匹配检查!
特别注意最后一条,有点和常识不符合(我原本理解的fallthrough只是取消break,然后继续做下一个case的匹配,但实际fallthrough把case匹配检查也取消了):
switch {
case true:
fmt.Println("The integer was <= 5")
fallthrough
case false:
fmt.Println("The integer was <= 6")
fallthrough
default:
fmt.Println("default case")
}
打印结果:
The integer was <= 5
The integer was <= 6
default case
8.13 - defer 语句
go语言实战
defer 语句会延迟函数的执行直到外层函数返回,通常用于执行清理操作。
func CopyFile(dstName, srcName string) (written int64, err error) {
src, err := os.Open(srcName)
if err != nil {
return
}
defer src.Close()
dst, err := os.Create(dstName)
if err != nil {
return
}
defer dst.Close()
return io.Copy(dst, src)
}
其他使用场景,如释放mutex:
mu.Lock()
defer mu.Unlock()
打印footer:
printHeader()
defer printFooter()
printContent1()
printContent2()
使用事项:
-
调用所需的参数会立刻评估
func a() { i := 0 defer fmt.Println(i) i++ return }
这里会打印0,因为
defer fmt.Println(i)
执行时i为0,参数在此时确定,后面的改动不会影响defer语句的参数。 -
多个defer调用会入栈,后进先出
func b() { for i := 0; i < 4; i++ { defer fmt.Print(i) } }
这里会打印3210,顺序和defer语句的顺序相反。
-
defer语句有机会修改函数返回值
func c() (i int) { defer func() { i++ }() return 1 }
这里的函数返回值会被defer修改,从而返回2。
golang语言规范
“defer"语句调用函数,该函数的执行被推迟到外围函数返回的那一刻,这可能是因为外围函数执行了一个返回语句,到达了其函数体的终点,或者是因为相应的goroutine发生panic/恐慌。
DeferStmt = "defer" Expression .
表达式必须是函数或方法调用,不能用括号。内置函数的调用与表达式语句一样受到限制。
每次执行 “defer"语句时,函数值和调用的参数都会像往常一样被评估并重新保存,但实际函数不会被调用。相反,deferred的函数在外围函数return之前立即被调用,其顺序与defer的顺序相反。也就是说,如果外围函数通过一个显式return语句返回,则在该return语句设置了任何结果参数之后,但在函数return给调用者之前,defer函数会被执行。如果一个defer函数的值评价为nil,则在函数被调用时,而不是在 “defer"语句被执行时,执行就会panic/慌乱。
例如,如果defer函数是一个函数字面量,而外围的函数有命名的结果参数,这些结果参数在字面量的范围内,那么defer函数可以在结果参数被返回之前访问和修改它们。如果defer函数有任何返回值,那么当函数完成时,它们将被丢弃。(也请参见关于处理恐慌的章节。)
lock(l)
defer unlock(l) // unlocking happens before surrounding function returns
// prints 3 2 1 0 before surrounding function returns
for i := 0; i <= 3; i++ {
defer fmt.Print(i)
}
// f returns 42
func f() (result int) {
defer func() {
// result is accessed after it was set to 6 by the return statement
result *= 7
}()
return 6
}
Effective Go
https://golang.org/doc/effective_go.html#defer
Go 的 defer 语句安排在执行 defer 的函数返回之前立即运行函数调用(defer函数)。这是一种不寻常但有效的方法,用于处理一些情况,例如无论函数走哪条路径返回,都必须释放资源。规范的例子是解锁mutex或关闭文件。
// Contents returns the file's contents as a string.
func Contents(filename string) (string, error) {
f, err := os.Open(filename)
if err != nil {
return "", err
}
defer f.Close() // f.Close will run when we're finished.
var result []byte
buf := make([]byte, 100)
for {
n, err := f.Read(buf[0:])
result = append(result, buf[0:n]...) // append is discussed later.
if err != nil {
if err == io.EOF {
break
}
return "", err // f will be closed if we return here.
}
}
return string(result), nil // f will be closed if we return here.
}
defer对Close这样的函数的调用有两个好处。首先,它保证你永远不会忘记关闭文件,如果你后来编辑函数添加了新的返回路径,就很容易犯这个错误。第二,它意味着close坐在open的附近,这比把它放在函数的最后要清楚得多。
defer函数的参数(如果函数是方法,则包括接收方)是在defer执行时评估的,而不是在调用执行时评估的。除了避免担心变量在函数执行时改变值之外,这意味着单个defer调用点可以defer多个函数的执行。下面是一个例子:
for i := 0; i < 5; i++ {
defer fmt.Printf("%d ", i)
}
defer函数是按照 LIFO 顺序执行的,所以这段代码会在函数返回时导致 4 3 2 1 0 被打印出来。更有价值的例子是通过程序跟踪函数执行的简单方法。我们可以写几个简单的跟踪例程,比如这样:
func trace(s string) { fmt.Println("entering:", s) }
func untrace(s string) { fmt.Println("leaving:", s) }
// Use them like this:
func a() {
trace("a")
defer untrace("a")
// do something....
}
我们可以更好地利用 defer 函数的参数在 defer 执行时被评估这一事实。追踪例程可以设置未追踪例程的参数。这个例子:
func trace(s string) string {
fmt.Println("entering:", s)
return s
}
func un(s string) {
fmt.Println("leaving:", s)
}
func a() {
defer un(trace("a"))
fmt.Println("in a")
}
func b() {
defer un(trace("b"))
fmt.Println("in b")
a()
}
func main() {
b()
}
打印:
entering: b
in b
entering: a
in a
leaving: a
leaving: b
对于习惯了其他语言的块级资源管理的程序员来说,defer可能看起来很奇特,但它最有趣和最强大的应用恰恰来自于它不是基于块而是基于函数。