1. 跨平台、有垃圾回收机制;
2. 支持Unicode字符集(符号集)以及utf-8编解码(存储格式),Go语言源文件总是用UTF8编码;
3. 默认使用了静态编译,不依赖任何动态链接库;
4. 必须恰当导入需要的包,缺少了必要的包或者导入了不需要的包,程序都无法编译通过。go语言编译过程中只有报错,没有警告;
5. Go语言不需要在语句或者声明的末尾添加分号,除非一行上有多条语句;
6. gofmt工具把代码格式化为标准格式;
7. 只有i++,没有++i。i++是语句而非表达式,j = i++是错误的;
8. Go语言只有for循环这一种循环语句。for循环有多种形式,其中一种如下所示:
for initialization; condition; post {
// zero or more statements
}
for循环的这三个部分每个都可以省略,如果省略initialization和post,分号也可以省略:
// a traditional "while" loop
for condition {
// ...
}
如果连condition也省略了,像下面这样:
// a traditional infinite loop
for {
// ...
}
这就变成一个无限循环
for循环的另一种形式,在某种数据类型的区间(range)上遍历,如字符串或切片
9. 每次循环迭代,range产生一对值;索引以及在该索引处的元素值;
10. 空标识符(blank identifier),即_(也就是下划线)。空标识符可用于在任何语法需要变量名但程序逻辑不需要的时候(如:在循环里)丢弃不需要的循环索引,并保留元素值;
11. 声明一个变量有好几种方式,下面这些都等价:
s := ""
var s string
var s = ""
var s string = ""
用哪种不用哪种,为什么呢?第一种形式,是一条短变量声明,最简洁,但只能用在函数内部,而不能用于包变量。第二种形式依赖于字符串的默认初始化零值机制,被初始化为""。第三种形式用得很少,除非同时声明多个变量。第四种形式显式地标明变量的类型,当变量类型与初值类型相同时,类型冗余,但如果两者类型不同,变量类型就必须了。实践中一般使用前两种形式中的某个,初始值重要的话就显式地指定变量的类型,否则使用隐式初始化。var形式的声明语句往往用于全局变量,:= 简短变量声明往往用于局部变量;
12. Go语言并不需要显式地在每一个case后写break,语言默认执行完case后的逻辑语句会自动退出。当然了,如果你想要相邻的几个case都执行同一逻辑的话,需要自己显式地写上一个fallthrough语句来覆盖这种默认行为。
13. Go语言里的switch还可以不带操作对象(译注:switch不带操作对象时默认用true值代替,然后将每个case的表达式和true值进行比较);可以直接罗列多种条件,像其它语言里面的多个if else一样,下面是一个例子:
func Signum(x int) int {
switch {
case x > 0:
return +1
default:
return 0
case x < 0:
return -1
}
}
这种形式叫做无tag switch(tagless switch);这和switch true是等价的。
14. 指针是可见的内存地址,&操作符可以返回一个变量的内存地址,并且*操作符可以获取指针指向的变量内容,但是在Go语言里没有指针运算,也就是不能像c语言里可以对指针进行加或减操作。
15. 在你开始写一个新程序之前,最好先去检查一下是不是已经有了现成的库可以帮助你更高效地完成这件事情。你可以在 https://golang.org/pkg 和 https://godoc.org 中找到标准库和社区写的package;
16. godoc这个工具可以让你直接在本地命令行阅读标准库的文档。比如
go doc http.ListenAndServe
17. Go语言中类似if和switch的关键字有25个:
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
此外,还有大约30多个预定义的名字:
内建常量:
true false iota nil
内建类型:
int int8 int16 int32 int64
uint uint8 uint16 uint32 uint64 uintptr
float32 float64 complex128 complex64
bool byte rune string error
内建函数:
make len cap new append copy close delete
complex real imag
panic recover
18.名字的开头字母的大小写决定了名字在包外的可见性。如果一个名字是大写字母开头的(译注:必须是在函数外部定义的包级名字;包级函数名本身也是包级名字),那么它将是导出的,也就是说可以被外部的包访问,例如fmt包的Printf函数就是导出的,可以在fmt包外部访问。包本身的名字一般总是用小写字母。
19. 在习惯上,Go语言程序员推荐使用 驼峰式 命名,当名字由几个单词组成时优先使用大小写分隔,而不是优先用下划线分隔;
20. 函数外部的名字(var、const、type和func),可以先使用后声明,但是函数内部的名字则必须先声明之后才能使用;
21.数值类型变量对应的零值是0,布尔类型变量对应的零值是false,字符串类型对应的零值是空字符串,接口或引用类型(包括slice、指针、map、chan和函数)变量对应的零值是nil。数组或结构体等聚合类型对应的零值是每个元素或字段都是对应该类型的零值。
22. 假设pAdd是数组指针,那么pAdd[0] 是 (*pAdd)[0] 的缩写,跟C语言一致;
23. 简短变量声明语句中必须至少要声明一个新的变量。简短变量声明左边的变量可能并不是全部都是刚刚声明的。如果有一些已经在相同的词法域声明过了,那么简短变量声明语句对这些已经声明过的变量就只有赋值行为了
24. go语言编译器会自动决定把一个变量放在栈还是放在堆,编译器会做逃逸分析(escape analysis),当发现变量的作用域没有跑出函数范围,就可以在栈上,反之则必须分配在堆。所以不用担心会不会导致memory leak,因为GO语言有强大的垃圾回收机制。go语言声称这样可以释放程序员关于内存的使用限制,更多的让程序员关注于程序功能逻辑本身。对于动态new出来的局部变量,go语言编译器也会根据是否有逃逸行为来决定是分配在堆还是栈,而不是直接分配在堆中。总之,函数内部局部变量,无论是动态new出来的变量还是创建的局部变量,它被分配在堆还是栈,是由编译器做逃逸分析之后做出的决定。
25. 用new创建变量和普通变量声明语句方式创建变量没有什么区别,除了不需要声明一个临时变量的名字外,我们还可以在表达式中使用new(T)。换言之,new函数类似是一种语法糖,而不是一个新的基础概念。
下面的两个newInt函数有着相同的行为:
func newInt() *int {
return new(int)
}
func newInt() *int {
var dummy int
return &dummy
}
26.变量的生命周期指的是在程序运行期间变量有效存在的时间段。对于在包一级声明的变量来说,它们的生命周期和整个程序的运行周期是一致的。而相比之下,局部变量的生命周期则是动态的:每次从创建一个新变量的声明语句开始,直到该变量不再被引用为止,然后变量的存储空间可能被回收。函数的参数变量和返回值变量都是局部变量。它们在函数每次被调用的时候创建。
27. 交换两个变量:x, y = y, x
28. type 类型名字 底层类型。
一个类型声明语句创建了一个新的类型名称,和现有类型具有相同的底层结构。新命名的类型提供了一个方法,用来分隔不同概念的类型,这样即使它们底层类型相同也是不兼容的,因此它们不可以被相互比较或混在一个表达式运算;
29.一个包的源代码保存在一个或多个以.go为文件后缀名的源文件中,通常一个包所在目录路径的后缀是包的导入路径;例如包gopl.io/ch1/helloworld对应的目录路径是$GOPATH/src/gopl.io/ch1/helloworld。如果一个名字是大写字母开头的,那么该名字是导出的(译注:因为汉字不区分大小写,因此汉字开头的名字是没有导出的)。而包级别的名字,例如在一个文件声明的类型和常量,在同一个包的其他源文件也是可以直接访问的,就好像所有代码都在一个文件一样。
30. Go语言的习惯是在if中处理错误然后直接返回,这样可以确保正常执行的语句不需要代码缩进。
31.
var cwd string
func init() {
cwd, err := os.Getwd() // compile error: unused: cwd
if err != nil {
log.Fatalf("os.Getwd failed: %v", err)
}
}
虽然cwd在外部已经声明过,但是:=语句还是将cwd和err重新声明为新的局部变量。因为内部声明的cwd将屏蔽外部的声明,因此上面的代码并不会正确更新包级声明的cwd变量。
32. 布尔型、数字类型和字符串等基本类型都是可比较的,字符串的比较通过逐个字节比较完成的;
33. &^ 位清空(AND NOT)
这个操作符通常用于清空对应的标志位,例如 a = 0011 1010,如果想清空第二位,则可以这样操作
a &^ 0000 0010 = 0011 1000
34.尽管Go语言提供了无符号数的运算,但即使数值本身不可能出现负数,我们还是倾向于使用有符号的int类型,就像数组的长度那样,虽然使用uint无符号类型似乎是一个更合理的选择。事实上,内置的len函数返回一个有符号的int。无符号数往往只有在位运算或其它特殊的运算场景才会使用。
35. 内置的len函数可以返回一个字符串中的字节数目(不是rune字符数目);
36. 一个原生的字符串面值形式是`…`,使用反引号代替双引号。原生字符串面值用于编写正则表达式会很方便,因为正则表达式往往会包含很多反斜杠。
37. 一个字符串是包含只读字节的数组,一旦创建,是不可变的。相比之下,一个字节slice的元素则可以自由地修改。
字符串和字节slice之间可以相互转换:
s := "abc"
b := []byte(s)
s2 := string(b)
从概念上讲,一个[]byte(s)转换是分配了一个新的字节数组用于保存字符串数据的拷贝,然后引用这个底层的字节数组。编译器的优化可以避免在一些场景下分配和复制字符串数据,但总的来说需要确保在变量b被修改的情况下,原始的s字符串也不会改变。将一个字节slice转换到字符串的string(b)操作则是构造一个字符串拷贝,以确保s2字符串是只读的。
38. 在一个const声明语句中,在第一个声明的常量所在的行,iota将会被置为0,然后在每一个有常量声明的行加一。
39. 和数组对应的类型是Slice(切片),它是可以增长和收缩的动态序列;
40. 在数组字面值中,如果在数组的长度位置出现的是“…”省略号,则表示数组的长度是根据初始化值的个数来计算,如:q := […]int{1, 2, 3}。
数组的长度是数组类型的一个组成部分,因此[3]int和[4]int是两种不同的数组类型。数组的长度必须是常量表达式,因为数组的长度需要在编译阶段确定。
41. 禁止对map元素取址的原因是map可能随着元素数量的增长而重新分配更大的内存空间,从而可能导致之前的地址无效。map类型的零值是nil,也就是没有引用任何哈希表。
42. 如果结构体成员名字是以大写字母开头的,那么该成员就是导出的;这是Go语言导出规则决定的。一个结构体可能同时包含导出和未导出的成员。
43. 函数声明格式:
func name(parameter-list) (result-list) {
body
}
下面,我们给出4种方法声明拥有2个int型参数和1个int型返回值的函数:
func add(x int, y int) int {return x + y}
func sub(x, y int) (z int) { z = x - y; return}
func first(x int, _ int) int { return x }
func zero(int, int) int { return 0 }
- 切片
数组和slice之间有着紧密的联系。一个slice是一个轻量级的数据结构,提供了访问数组子序列(或者全部)元素的功能,而且slice的底层确实引用一个数组对象。一个slice由三个部分构成:指针、长度和容量。指针指向第一个slice元素对应的底层数组元素的地址,要注意的是slice的第一个元素并不一定就是数组的第一个元素。长度对应slice中元素的数目;长度不能超过容量,容量一般是从slice的开始位置到底层数据的结尾位置。内置的len和cap函数分别返回slice的长度和容量。
如果切片操作超出cap(s)的上限将导致一个panic异常,但是超出len(s)则是意味着扩展了slice,因为新slice的长度会变大。
另外,字符串的切片操作和[]byte字节类型切片的切片操作是类似的。都写作x[m:n],并且都是返回一个原始字节序列的子序列,底层都是共享之前的底层数组,因此这种操作都是常量时间复杂度。x[m:n]切片操作对于字符串则生成一个新字符串,如果x是[]byte的话则生成一个新的[]byte。
和数组不同的是,slice之间不能比较,因此我们不能使用==操作符来判断两个slice是否含有全部相等元素。
slice唯一合法的比较操作是和nil比较。
如果你需要测试一个slice是否是空的,使用len(s) == 0来判断,而不应该用s == nil来判断。
45. 数组与切片的区别
这是数组:
var a [3]int
这是切片:
var a []int
在 Go 语言中,数组是一种值类型,而且不同长度的数组属于不同的类型。例如 [2]int
和 [20]int
属于不同的类型。
当值类型作为参数传递时,参数是该值的一个拷贝,因此更改拷贝的值并不会影响原值。
而将切片作为参数时,拷贝了一个新切片,即拷贝了构成切片的三个值,包括底层数组的指针。对切片中某个元素的修改,实际上是修改了底层数组中的值,因此原切片也发生了改变。
46.我们并不知道append调用是否导致了内存的重新分配,因此我们也不能确认新的slice和原始的slice是否引用的是相同的底层数组空间。同样,我们不能确认在原先的slice上的操作是否会影响到新的slice。因此,通常是将append返回的结果直接赋值给输入的slice变量:
runes = append(runes, r)
47. map
map中的元素并不是一个变量,因此我们不能对map的元素进行取址操作:
_ = &ages["bob"] // compile error: cannot take address of map element
禁止对map元素取址的原因是map可能随着元素数量的增长而重新分配更大的内存空间,从而可能导致之前的地址无效。
Map的迭代顺序是不确定的,并且不同的哈希函数实现可能导致不同的遍历顺序。在实践中,遍历的顺序是随机的,每一次遍历的顺序都不相同。这是故意的,每次都使用随机的遍历顺序可以强制要求程序不会依赖具体的哈希函数实现。如果要按顺序遍历key/value对,我们必须显式地对key进行排序,可以使用sort包的Strings函数对字符串slice进行排序。
在向map存数据前必须先创建map。
通过key作为索引下标来访问map将产生一个value。如果key在map中是存在的,那么将得到与key对应的value;如果key不存在,那么将得到value对应类型的零值。
但是有时候可能需要知道对应的元素是否真的是在map之中:
if age, ok := ages["bob"]; !ok { /* ... */ }
在这种场景下,map的下标语法将产生两个值;第二个是一个布尔值,用于报告元素是否真的存在。布尔变量一般命名为ok,特别适合马上用于if条件判断部分。
48.点操作符也可以和指向结构体的指针一起工作:
var employeeOfTheMonth *Employee = &dilbert
employeeOfTheMonth.Position += " (proactive team player)"
相当于下面语句
(*employeeOfTheMonth).Position += " (proactive team player)"
49. 在go语言,->和.都被.替代,编译器知道这类型
50. 匿名成员
Go语言有一个特性让我们只声明一个成员对应的数据类型而不指名成员的名字;这类成员就叫匿名成员。匿名成员的数据类型必须是命名的类型或指向一个命名的类型的指针。下面的代码中,Circle和Wheel各自都有一个匿名成员。我们可以说Point类型被嵌入到了Circle结构体,同时Circle类型被嵌入到了Wheel结构体。
type Point struct {
X, Y int
}
type Circle struct {
Point
Radius int
}
type Wheel struct {
Circle
Spokes int
}
得益于匿名嵌入的特性,我们可以直接访问叶子属性而不需要给出完整的路径:
var w Wheel
w.X = 8 // equivalent to w.Circle.Point.X = 8
w.Y = 8 // equivalent to w.Circle.Point.Y = 8
w.Radius = 5 // equivalent to w.Circle.Radius = 5
w.Spokes = 20
51. 结构体的成员Tag
type Movie struct {
Title string
Year int `json:"released"`
Color bool `json:"color,omitempty"`
Actors []string
}
结构体的成员Tag可以是任意的字符串面值,但是通常是一系列用空格分隔的key:"value"键值对序列;因为值中含有双引号字符,因此成员Tag一般用原生字符串面值的形式书写。json开头键名对应的值用于控制encoding/json包的编码和解码的行为,并且encoding/…下面其它的包也遵循这个约定。成员Tag中json对应值的第一部分用于指定JSON对象的名字,比如将Go语言中的TotalCount成员对应到JSON中的total_count对象。Color成员的Tag还带了一个额外的omitempty选项,表示当Go语言结构体成员为空或零值时不生成该JSON对象(这里false为零值)
52. 在Go中,错误处理有一套独特的编码风格。检查某个子函数是否失败后,我们通常将处理失败的逻辑代码放在处理成功的代码之前。如果某个错误会导致函数返回,那么成功时的逻辑代码不应放在else语句块中,而应直接放在函数体中。
53. 在Go中,函数被看作第一类值(first-class values):函数像其他值一样,拥有类型,可以被赋值给其他变量,传递给函数,从函数返回。对函数值(function value)的调用类似函数调用。
函数值可以与nil比较,但是函数值之间是不可比较的,也不能用函数值作为map的key。
54. 匿名函数可以访问完整的词法环境(lexical environment),这意味着在函数中定义的内部函数可以引用该函数的变量,如下例所示:
// squares返回一个匿名函数。
// 该匿名函数每次被调用时都会返回下一个数的平方。
func squares() func() int {
var x int
return func() int {
x++
return x * x
}
}
func main() {
f := squares()
fmt.Println(f()) // "1"
fmt.Println(f()) // "4"
fmt.Println(f()) // "9"
fmt.Println(f()) // "16"
}
函数值不仅仅是一串代码,还记录了状态。在squares中定义的匿名内部函数可以访问和更新squares中的局部变量,这意味着匿名函数和squares中,存在变量引用。这就是函数值属于引用类型和函数值不可比较的原因。
55. Go词法作用域的一个陷阱
考虑这样一个问题:你被要求首先创建一些目录,再将目录删除。在下面的例子中我们用函数值来完成删除操作。下面的示例代码需要引入os包。为了使代码简单,我们忽略了所有的异常处理。
var rmdirs []func()
for _, d := range tempDirs() {
dir := d // NOTE: necessary!
os.MkdirAll(dir, 0755) // creates parent directories too
rmdirs = append(rmdirs, func() {
os.RemoveAll(dir)
})
}
// ...do some work…
for _, rmdir := range rmdirs {
rmdir() // clean up
}
你可能会感到困惑,为什么要在循环体中用循环变量d赋值一个新的局部变量,而不是像下面的代码一样直接使用循环变量dir。需要注意,下面的代码是错误的。
var rmdirs []func()
for _, dir := range tempDirs() {
os.MkdirAll(dir, 0755)
rmdirs = append(rmdirs, func() {
os.RemoveAll(dir) // NOTE: incorrect!
})
}
问题的原因在于循环变量的作用域。在上面的程序中,for循环语句引入了新的词法块,循环变量dir在这个词法块中被声明。在该循环中生成的所有函数值都共享相同的循环变量。需要注意,函数值中记录的是循环变量的内存地址,而不是循环变量某一时刻的值。以dir为例,后续的迭代会不断更新dir的值,当删除操作执行时,for循环已完成,dir中存储的值等于最后一次迭代的值。这意味着,每次对os.RemoveAll的调用删除的都是相同的目录。
通常,为了解决这个问题,我们会引入一个与循环变量同名的局部变量,作为循环变量的副本。比如下面的变量dir,虽然这看起来很奇怪,但却很有用。
for _, dir := range tempDirs() {
dir := dir // declares inner dir, initialized to outer dir
// ...
}
这个问题不仅存在基于range的循环,在下面的例子中,对循环变量i的使用也存在同样的问题:
var rmdirs []func()
dirs := tempDirs()
for i := 0; i < len(dirs); i++ {
os.MkdirAll(dirs[i], 0755) // OK
rmdirs = append(rmdirs, func() {
os.RemoveAll(dirs[i]) // NOTE: incorrect!
})
}
如果你使用go语句(第八章)或者defer语句(5.8节)会经常遇到此类问题。这不是go或defer本身导致的,而是因为它们都会等待循环结束后,再执行函数值。
56. 可变参数
如果原始参数已经是切片类型,我们该如何传递给sum?只需在最后一个参数后加上省略符。下面的代码功能与上个例子中最后一条语句相同。
values := []int{1, 2, 3, 4}
fmt.Println(sum(values...)) // "10"
57. Deferred
你只需要在调用普通函数或方法前加上关键字defer,就完成了defer所需要的语法。当执行到该条语句时,函数和参数表达式得到计算,但直到包含该defer语句的函数执行完毕时,defer后的函数才会被执行,不论包含defer语句的函数是通过return正常结束,还是由于panic导致的异常结束。你可以在一个函数中执行多条defer语句,它们的执行顺序与声明顺序相反。
defer语句经常被用于处理成对的操作,如打开、关闭、连接、断开连接、加锁、释放锁。通过defer机制,不论函数逻辑多复杂,都能保证在任何执行路径下,资源被释放。释放资源的defer应该直接跟在请求资源的语句后。
需要注意一点:不要忘记defer语句后的圆括号,否则本该在进入时执行的操作会在退出时执行,而本该在退出时执行的,永远不会被执行。
在循环体中的defer语句需要特别注意,因为只有在函数执行完毕后,这些被延迟的函数才会执行。
在关闭文件时,我们没有对f.close采用defer机制,因为这会产生一些微妙的错误。许多文件系统,尤其是NFS,写入文件时发生的错误会被延迟到文件关闭时反馈。如果没有检查文件关闭时的反馈信息,可能会导致数据丢失,而我们还误以为写入操作成功。如果io.Copy和f.close都失败了,我们倾向于将io.Copy的错误信息反馈给调用者,因为它先于f.close发生,更有可能接近问题的本质。
58. panic
对于大部分漏洞,我们应该使用Go提供的错误机制,而不是panic,尽量避免程序的崩溃。在健壮的程序中,任何可以预料到的错误,如不正确的输入、错误的配置或是失败的I/O操作都应该被优雅的处理,最好的处理方式,就是使用Go的错误机制。
将panic机制类比其他语言异常机制的读者可能会惊讶,runtime.Stack为何能输出已经被释放函数的信息?在Go的panic机制中,延迟函数的调用在释放堆栈信息之前。
59. 方法
在函数声明时,在其名字之前放上一个变量,即是一个方法。这个附加的参数会将该函数附加到这种类型上,即相当于为这种类型定义了一个独占的方法。
在Go语言中,我们并不会像其它语言那样用this或者self作为接收器;我们可以任意的选择接收器的名字。由于接收器的名字经常会被使用到,所以保持其在方法间传递时的一致性和简短性是不错的主意。这里的建议是可以使用其类型的第一个字母。
我们可以给同一个包内的任意命名类型定义方法,只要这个命名类型的底层类型不是指针或者interface。
在声明方法时,如果一个类型名本身是一个指针的话,是不允许其出现在接收器中的。
不管你的method的receiver是指针类型还是非指针类型,都是可以通过指针/非指针类型进行调用的,编译器会帮你做类型转换。
在声明一个method的receiver该是指针还是非指针类型时,你需要考虑两方面的因素,第一方面是这个对象本身是不是特别大,如果声明为非指针变量时,调用会产生一次拷贝;第二方面是如果你用指针类型作为receiver,那么你一定要注意,这种指针类型指向的始终是一块内存地址,就算你对其进行了拷贝。
60. Go语言只有一种控制可见性的手段:大写首字母的标识符会从定义它们的包中被导出,小写字母的则不会。这种限制包内成员的方式同样适用于struct或者一个类型的方法。因而如果我们想要封装一个对象,我们必须将其定义为一个struct。
61. 接口
type 接口类型名 interface{
方法名1( 参数列表1 ) 返回值列表1
方法名2( 参数列表2 ) 返回值列表2
…
}
接口是计算机系统中多个组件共享的边界,不同的组件能够在边界上交换信息1。如下图所示,接口的本质是引入一个新的中间层,调用方可以通过接口与具体实现分离,解除上下游的耦合,上层的模块不再需要依赖下层的具体模块,只需要依赖一个约定好的接口。
C++ 定义接口的方式称为“侵入式”,而 Go 采用的是 “非侵入式”,不需要显式声明,只需要实现接口定义的函数,编译器自动会识别。
62. Go 语言的整型、数组、结构体、指针都是值传递的,也就是在调用函数时会对内容进行拷贝。
63. 如果我们没有这么强制使用指针进行调用,Go的编译器自动会帮我们取指针,以满足接收者的要求。同样的,如果是一个值接收者的方法,使用指针也是可以调用的,Go编译器自动会解引用,以满足接收者的要求。
64. 所谓鸭子类型说的是:只要走起路来像鸭子、叫起来也像鸭子,那么就可以把它当作鸭子。Go语言中的面向对象就是如此,如果一个对象只要看起来像是某种接口类型的实现,那么它就可以作为该接口类型使用。
65. Go语言中有对指针类型变量求值的语法糖,但没有对值类型变量求指针的语法糖。
66.空接口是指没有定义任何方法的接口。因此任何类型都实现了空接口。
空接口类型的变量可以存储任意类型的变量。
使用空接口实现可以接收任意类型的函数参数。
使用空接口实现可以保存任意值的字典。
67. channels
var chanName chan ElementType
一个基于无缓存Channels的发送操作将导致发送者goroutine阻塞,直到另一个goroutine在相同的Channels上执行接收操作,当发送的值通过Channels成功传输之后,两个goroutine可以继续执行后面的语句。反之,如果接收操作先发生,那么接收者goroutine也将阻塞,直到有另一个goroutine在相同的Channels上执行发送操作。
基于无缓存Channels的发送和接收操作将导致两个goroutine做一次同步操作。因为这个原因,无缓存Channels有时候也被称为同步Channels。
68. 匿名函数
定义:
func(paramters)(returnvals){
//do something
}
调用定义:
func(paramters)(returnvals){
//do something
}(realParamters)
如果该匿名函数最后不加 (),那么该匿名函数就不会被执行。
参考:
前言 · Go语言圣经
Go 语言设计与实现 | Go 语言设计与实现