go笔记(简要总结,来源:go语言圣经)

go课堂笔记

学习了go语言圣经之后,收益匪浅,很多小细节之前都没留意,现在对go的语法以及注意的事项,容易出错的知识点进行整理(理论知识较多),方便日后温习。

易遗忘点

import声明必须跟在文件的package声明之后.。随后,则是组成程序的函数、变量、常量、类型的声明语句(分别由关键字func, var, const, type定义)。这些内容的声明顺序并不重要.

Go语言不需要在语句或者声明的末尾添加分号,除非一行上有多条语句。实际上,编译器会主动把特定符号后的换行符转换为分号, 因此换行符添加的位置会影响Go代码的正确解析。(在表达式x + y中,可在+后换行,不能在+前换行, 以+结尾的话不会被插入分号分隔符,但是以x结尾的话则会被分号分隔符,从而导致编译错误。)
函数的有右小括弧也可以另起一行缩进,同时为了防止编译器在行尾自动插入分号而导致的编译 错误,可以在末尾的参数变量后面显式插入逗号。

os包以跨平台的方式,提供了一些与操作系统交互的函数和变量。程序的命令行参数可从os包的Args变量获取。os.Args变量是一个字符串(string)的切片(slice)。
os.Args的第一个元素,os.Args[0], 是命令本身的名字;其它的元素则是程序启动时传给它的参数。

和大多数编程语言类似,区间索引时,Go言里也采用左闭右开形式, 即,区间包括第一个索引元素,不包括最后一个, 因为这样可以简化逻辑。比如s[m:n]这个切片,0 ≤ m ≤ n ≤ len(s),包含n-m个元素

j = i++非法,而且++和- -都只能放在变量名后面,因此- -i也非法

Go语言不允许使用无用的局部变量(local variables),因为这会导致编译错误。Go语言中这种情况的解决方法是用空标识符(blank identifier),即_(也就是下划线)。空标识符可用于任何语法需要变量名但程序逻辑不需要的时候。

+=连接原字符串、空格和下个参数,产生新字符串, 并把它赋值。如果连接涉及的数据量很大,这种方式代价高昂。一种简单且高效的解决方案是使用strings包的Join函数

Printf有一大堆这种转换,Go程序员称之为动词(verb)。下面的表格虽然远不是完整的规范,但展示了可用的很多特性:
%d 十进制整数
%x, %o, %b 十六进制,八进制,二进制整数。
%f, %g, %e 浮点数: 3.141593 3.141592653589793 3.141593e+00
%t 布尔:true或false
%c 字符(rune) (Unicode码点)
%s 字符串
%q 带双引号的字符串"abc"或带单引号的字符’c’
%v 变量的自然形式(natural format)
%T 变量的类型
%% 字面上的百分号标志(无操作数)

函数和包级别的变量(package-level entities)可以任意顺序声明,并不影响其被调用。(译注:最好还是遵循一定的规范)。

bufio.Scanner,ioutil.ReadFile和ioutil.WriteFile使用的都是*os.File的Read和Write方法。

main函数也是运行在一个goroutine。

Go语言里的switch还可以不带操作对象(译注:switch不带操作对象时默认用true值代替,然后将每个case的表达式和true值进行比较).这种形式叫做无tag switch(tagless switch);这和switch true是等价的。

Go语言,指针是可见的内存 地址,&操作符可以返回一个变量的内存地址,并且*操作符可以获取指针指向的变量内容,但是在Go语 言里没有指针运算

Go语言中的函数名、变量名、常量名、类型名、语句标号和包名等所有的命名,都遵循一个简单的命名 规则:一个名字必须以一个字母(Unicode字母)或下划线开头,后面可以跟任意数量的字母、数字或下 划线大写字母和小写字母是不同的

Go语言主要有四种类型的声明语句:var、 const、type和func,分别对应变量、常量、类型和函数实体对象的声明

另一个创建变量的方法是调用用内建的new函数。表达式new(T)将创建一个T类型的匿名变量,初始化为T 类型的零值,然后返回变量地址,返回的指针类型为*T

零值初始化机制可以确保每个声明的变量总是有一个良好定义的值,因此在Go语言中不存在未初始化的 变量

在包级别声明的变量会在main入口函数执行前完成初始化,局部变量将在声明语句被执行到的时候完成初始化

简短变量声明语句(:=)中必须至少要声明一个新的变量,否则代码将不能编译通过。
简短变量声明语句只有对已经在同级词法域声明过的变量才和赋值操作语句等价,如果变量是在外部词 法域声明的,那么简短变量声明语句将会在当前词法域重新声明一个新的变量。

一个指针对应变量在内存中的存储位置。并不是每一个值都会有一 个内存地址,但是对于每一个变量必然有对应的内存地址。任何类型的指针的零值都是nil

变量有时候被称为可寻址的值。即使变量由表达式临时生成,那么表达式也必须能接受&取地址操作。

元组赋值是另一种形式的赋值语句,它允许同时更新多个变量的值。在赋值之前,赋值语句右边的所有 表达式将会先进行求值,然后再统一更新左边对应变量的值。

x, y = y, x 
a[i], a[j] = a[j], a[i]

对于每一个类型T,都有一个对应的类型转换操作T(x),用于将x转为T类型(译注:如果T是指针类型,可能会需要用小括弧包装T,比如(*int)(0))。只有当两个类型的底层基础类型相同时,才允许这种转 型操作,或者是两者都是指向相同底层结构的指针类型,这些转换只改变类型而不会影响值本身。

包的初始化首先是解决包级变量的依赖顺序,然后安照包级变量声明出现的顺序依次初始化。如果包中含有多个.go源文件,它们将按照发给编译器的顺序进行初始化,Go语言的构建工具首先会 将.go文件根据文件名排序,然后依次调用编译器编译。

每个文件都可以包含多个init初始化函数。在每个文件中的init初始 化函数,在程序开始执行时按照它们声明的顺序被自动调用。 每个包在解决依赖的前提下,以导入声明的顺序初始化,每个包只会被初始化一次。初始化工作是自下而上进行的, main包最后被初始化。以这种方式,可以确保在main函数执行之前,所有依然的包都已经完成初始化工 作了。

Go语言的闪电般的编译速度主要得益于三个 语言特性。
第一点,所有导入的包必须在每个文件的开头显式声明,这样的话编译器就没有必要读取和 分析整个源文件来判断包的依赖关系。
第二点,禁止包的环状依赖,因为没有循环依赖,包的依赖关系 形成一个有向无环图,每个包可以被独立编译,而且很可能是被并发编译。
第三点,编译后包的目标文 件不仅仅记录包本身的导出信息,目标文件同时还记录了包的依赖关系。

生命周期与作用域:

变量的生命周期指的是在程序运行期间变量有效存在的时间间隔。对于在包一级声明的变量来说,它们 的生命周期和整个程序的运行周期是一致的。而相比之下,在局部变量的声明周期则是动态的:从每次 创建一个新变量的声明语句开始,直到该变量不再被引用为止,然后变量的存储空间可能被回收。

因为一个变量的有效周期只取决于是否可达,因此一个循环迭代内部的局部变量的生命周期可能超出其 局部作用域

如果将指向短生命周期对象的指针保存到具有长生命周期的对象中,特别是保存到全局变量时,会阻止 对短生命周期对象的垃圾回收(从而可能影响程序的性能)。

声明语句的作用域是指源代 码中可以有效使用这个名字的范围。不要将作用域和生命周期混为一谈。声明语句的作用域对应的是一个源代码的文本区域;它是一个编译 时的属性一个变量的生命周期是指程序运行时变量存在的有效时间段,在此时间区域内它可以被程序 的其他部分引用;是一个运行时的概念

基础数据类型

Go语言将数据类型分为四类:基础类型、复合类型、引用类型和接口类型。基础类型,包括: 数字、字符串和布尔型。复合数据类型——数组和结构体——是通过组合简单类 型,来表达更加复杂的数据结构。引用类型包括指针、切片、字典、函 数、通道

Unicode字符rune类型是和int32等价的类型,通常用于表示一个Unicode码点。这两个名称可以互换使 用。同样byte也是uint8类型的等价类型,byte类型一般用于强调数值是一个原始的数据而不是一个小的整数。 最后,还有一种无符号的整数类型uintptr,没有指定具体的bit大小但是足以容纳指针。uintptr类型只 有在底层编程是才需要。

在Go语言中,%取模运算符的符号和被取模数的符号总 是一致的,因此-5%3和-5%-3结果都是-2。

如果一个算术运算的结果,不管是有符号或者是无符号的,如果需要更多的bit位才能正确表示的话,就 说明计算结果是溢出了。(可了解计算机组成原理-运算)

var u uint8 = 255 
fmt.Println(u, u+1, u*u) // "255 0 1" 
var i int8 = 127
fmt.Println(i, i+1, i*i) // "127 -128 1"

Go语言还提供了以下的bit位操作运算符,前面4个操作运算符并不区分是有符号还是无符号数:

& 位运算 AND 
| 位运算 OR 
^ 位运算 XOR 
&^ 位清空 (AND NOT) 
<< 左移 
>> 右移 

具体的位运算可参考:https://blog.csdn.net/skh2015java/article/details/78451033

fmt.Printf("%d %[1]x %#[1]x %#[1]X\n", x)

请注意fmt的两个使用技巧。通常Printf格式化字符串包含多个%参数时将会包含对应相同数量的额外操 作数,但是%之后的[1]副词告诉Printf函数再次使用第一个操作数。第二,%后的#副词告诉Printf在 用%o、%x或%X输出时生成0、0x或0X前缀。

Go语言提供了两种精度的复数类型:complex64和complex128,分别对应float32和float64两种浮点数精度。内置的complex函数用于构建复数,内建的real和imag函数分别返回复数的实部和虚部。

一个字符串是一个不可改变的字节序列

Go语言的源文件采用UTF8编码,并且Go语言处理UTF8编码的文本也很出色。unicode包提供了诸多处理 rune字符相关功能的函数(比如区分字母和数组,或者是字母的大写和小写转换等),unicode/utf8包 则提供了用于rune字符序列的UTF8编码和解码的功能。

标准库中有四个包对字符串处理尤为重要:bytes、strings、strconv和unicode包strings包提供了许 多如字符串的查询、替换、比较、截断、拆分和合并等功能。
bytes包也提供了很多类似功能的函数,但是针对和字符串有着相同结构的[]byte类型。因为字符串是只 读的,因此逐步构建字符串会导致很多分配和复制。在这种情况下,使用bytes.Buffer类型将会更有效。
strconv包提供了布尔型、整型数、浮点数和对应字符串的相互转换,还提供了双引号转义相关的转换。
unicode包提供了IsDigit、IsLetter、IsUpper和IsLower等类似功能,它们用于给字符分类。每个函数 有一个单一的rune类型的参数,然后返回一个布尔值。而像ToUpper和ToLower之类的转换函数将用于 rune字符的大小写转换。所有的这些函数都是遵循Unicode标准定义的字母、数字等分类规范。strings 包也有类似的函数,它们是ToUpper和ToLower,将原始字符串的每个字符都做相应的转换,然后返回新 的字符串。

bytes包还提供了Buffer类型用于字节slice的缓存。一个Buffer开始是空的,但是随着string、byte或 []byte等类型数据的写入可以动态增长,一个bytes.Buffer变量并不需要处理化,因为零值也是有效 的。

常量表达式的值在编译期计算,而不是在运行期。每种常量的潜在类型都是基础类型:boolean、string 或数字

常量声明可以使用iota常量生成器初始化,它用于生成一组以相似规则初始化的常量,但是不用每行都 写一遍初始化表达式。在一个const声明语句中,在第一个声明的常量所在的行,iota将会被置为0,然 后在每一个有常量声明的行加一。

常量不同于变量的在运行期分配内存,常量通常会被编译器在预处理阶段直接展开,作为指令数据使用,不能取址。

复合数据类型

数组和结构体是聚合类型;它们的值由许多元素或成员字段的值组成。数组是由同构的元素组成——每 个数组元素都是完全相同的类型——结构体则是由异构的元素组成的。数组和结构体都是有固定内存大 小的数据结构。相比之下,slice和map则是动态的数据结构,它们将根据需要动态增长。

  1. 数组
    数组是一个由固定长度的特定类型元素组成的序列,一个数组可以由零个或多个元素组成。因为数组的 长度是固定的,因此在Go语言中很少直接使用数组。和数组对应的类型是Slice(切片),它是可以增长 和收缩动态序列,slice功能也更灵活。
    在数组字面值中,如果在数组的长度位置出现的是“...”省略号,则表示数组的长度是根据初始化值的 个数来计算。
    数组的长度必须 是常量表达式,因为数组的长度需要在编译阶段确定

  2. Slice(切片)
    Slice(切片)代表变长的序列,序列中每个元素都有相同的类型。一个slice类型一般写作[]T,其中T 代表slice中元素的类型;slice的语法和数组很像,只是没有固定长度而已。
    一个slice是一个轻量级的数据结构,提供了访问数组子序列(或者 全部)元素的功能,而且slice的底层确实引用一个数组对象。一个slice由三个部分构成:指针、长度 和容量。指针指向第一个slice元素对应的底层数组元素的地址,要注意的是slice的第一个元素并不一 定就是数组的第一个元素。长度对应slice中元素的数目;长度不能超过容量,容量一般是从slice的开 始位置到底层数据的结尾位置。内置的len和cap函数分别返回slice的长度和容量。
    slice的切片操作s[i:j],其中0 ≤ i≤ j≤ cap(s),用于创建一个新的slice,引用s的从第i个元素开 始到第j-1个元素的子序列。
    如果切片操作超出cap(s)的上限将导致一个panic异常,但是超出len(s)则是意味着扩展了slice,因为新slice的长度会变大
    字符串的切片操作和[]byte字节类型切片的切片操作是类似的。它们都写作x[m:n],并且都是返 回一个原始字节系列的子序列,底层都是共享之前的底层数组,因此切片操作对应常量时间复杂度。
    和数组不同的是,slice之间不能比较,因此我们不能使用==操作符来判断两个slice是否含有全部相等 元素。不过标准库提供了高度优化的bytes.Equal函数来判断两个字节型slice是否相等([]byte),但 是对于其他类型的slice,我们必须自己展开每个元素进行比较。
    为何 slice不直接支持比较运算符呢?这方面有两个原因。第一个原因,一个slice的元素是间接引用的,一 个slice甚至可以包含自身。第二个原因,因为slice的元素是间接引用的,一个固定值的slice在不同的时间可能包含不同的元素, 因为底层数组的元素可能会被修改。
    slice唯一合法的比较操作是和nil比较,一个零值的slice等于nil。一个nil值的slice并没有底层数组。
    内置的copy函数可以方便地将一个slice复制另一个相同类型的 slice。copy函数的第一个参数是要复制的目标slice,第二个参数是源slice,目标和源的位置顺序和 dst = src赋值语句是一致的。两个slice可以共享同一个底层数组,甚至有重叠也没有问题。
    为了提高内存使用效率,新分配的数组一般略大于保存x和y所需要的最低大小。通过在每次扩展数组时 直接将长度翻倍从而避免了多次内存分配,也确保了添加单个元素操的平均时间是一个常数时间。

  3. Map
    哈希表是一种巧妙并且实用的数据结构。它是一个无序的key/value对的集合,其中所有的key都是不同 的,然后通过给定的key可以在常数时间复杂度内检索、更新或删除对应的value。
    一个map就是一个哈希表的引用,map类型可以写为map[K]V,其中K和V分别对应key和 value。map中所有的key都有相同的类型,所以的value也有着相同的类型,但是key和value之间可以是 不同的数据类型。其中K对应的key必须是支持==比较运算符的数据类型,所以map可以通过测试key是否 相等来判断是否已经存在。
    使用内置的delete函数可以删除元素:
    delete(ages, "alice")
    map中的元素并不是一个变量,因此我们不能对map的元素进行取址操作。禁止对map元素取址的原因是map可能随着元素数量的增长而重新分配更大的内存空间,从而可能导致之 前的地址无效。
    Map的迭代顺序是不确定的,并且不同的哈希函数实现可能导致不同的遍历顺序。在实践中,遍历的顺序 是随机的,每一次遍历的顺序都不相同。这是故意的,每次都使用随机的遍历顺序可以强制要求程序不 会依赖具体的哈希函数实现。如果要按顺序遍历key/value对,我们必须显式地对key进行排序,可以使 用sort包的Strings函数对字符串slice进行排序。
    map类型的零值是nil,也就是没有引用任何哈希表。
    map上的大部分操作,包括查找、删除、len和range循环都可以安全工作在nil值的map上,它们的行为和 一个空的map类似。但是向一个nil值的map存入元素将导致一个panic异常。
    在向map存数据前必须先创建map。
    map是一个由make函数创建的数据结构的引用。map作为为参数传递给某函数时,该函数接收这个引用的一份拷贝(copy,或译为副本),被调用函数对map底层数据结构的任何修改,调用者函数都可以通过持有的map引用看到,所以你在函数里对map里的值进行修改时,原始的map内的值也会改变。

  4. struct结构体
    结构体是一种聚合的数据类型,是由零个或多个任意类型的值聚合成的实体。每个值称为结构体的成 员。
    结构体成员的输入顺序也有重要的意义。交换字段出现的先后顺序,那样的话就是定义了不同的结构体类型
    一个命名为S的结构体类型将不能再包含S类型的成员:因为一个聚合的值不能包含它自身。(该限制同 样适应于数组。)但是S类型的结构体可以包含*S指针类型的成员
    Go语言有一个特性让我们只声明一个成员对应的数据类型而不指名成员的名字;这类成员就叫匿名成 员。匿名成员的数据类型必须是命名的类型或指向一个命名的类型的指针。
    结构体的成员Tag可以是任意的字符串面值,但是通常是一系列用空格分隔的key:"value"键值对序列; 因为值中含义双引号字符,因此成员Tag一般用原生字符串面值的形式书写。omitempty选项,表示当Go语言结构体成员为空或 零值时不生成JSON对象(这里false为零值)。

函数

函数声明包括函数名、形式参数列表、返回值列表(可省略)以及函数体

在函数体中,函数的形参作为局部变量,被初始化为调用者提供的值。函数的形参和有名返回值作为函 数最外层的局部变量,被存储在相同的词法块中。

实参通过值的方式传递,因此函数的形参是实参的拷贝。对形参进行修改不会影响实参。但是,如果实 参包括引用类型,如指针,slice(切片)、map、function、channel等类型,实参可能会由于函数的简介 引用被修改

如果一个函数将所有的返回值都显示的变量名,那么该函数的return语句可以省略操作数。这称之为 bare return。当一个函数有多处return语句以及许多返回值时,bare return 可以减少代码的重复,但是使得代码难 以被理解。

拥有函数名的函数只能在包级语法块中被声明,通过函数字面量(function literal),我们可绕过这 一限制,在任何表达式中表示一个函数值。函数字面量的语法和函数声明相似,区别在于func关键字后 没有函数名。函数值字面量是一种表达式,它的值被成为匿名函数(anonymous function)

在声明可变参数函数时,需要在参数列表的最后一个参数类型之前加上省略符号“...”,这表示该函数 会接收任意数量的该类型参数

defer语句经常被用于处理成对的操作,如打开、关闭、连接、断开连接、加锁、释放锁。通过defer机 制,不论函数逻辑多复杂,都能保证在任何执行路径下,资源被释放。释放资源的defer应该直接跟在请 求资源的语句后。

一般而言,当panic异常发生时,程序会中断运行,并立即执行在该goroutine中被延迟的函数(defer 机制)。随后,程序崩溃并输出日志信息。(defer在panic输出错误之前执行。

方法

当调用一个函数时,会对其每一个参数值进行拷贝,如果一个函数需要更新一个变量,或者函数的其中 一个参数实在太大我们希望能够避免进行这种默认的拷贝,这种情况下我们就需要用到指针了。

func (p *Point) ScaleBy(factor float64) { 
p.X *= factor 
p.Y *= factor 
}

这个方法的名字是(Point).ScaleBy。这里的括号是必须的;没有括号的话这个表达式可能会被理解为 (Point.ScaleBy)。

幸运的是,go语言本身在这种地方会帮到我们。如果接收器p是一个Point 类型的变量,并且其方法需要一个Point指针作为接收器,我们可以用下面这种简短的写法。
p.ScaleBy(2)
编译器会隐式地帮我们用&p去调用ScaleBy这个方法。这种简写方法只适用于“变量`。

不管你的method的receiver是指针类型还是非指针类型,都是可以通过指针/非指针类型进行调用 的,编译器会帮你做类型转换
在声明一个method的receiver该是指针还是非指针类型时,你需要考虑两方面的内部,第一方面是这 个对象本身是不是特别大,如果声明为非指针变量时,调用会产生一次拷贝;第二方面是如果你用指 针类型作为receiver,那么你一定要注意,这种指针类型指向的始终是一块内存地址,就算你对其进 行了拷贝。

一个 struct类型的字段对同一个包的所有代码都有可见性,无论你的代码是写在一个函数还是一个方法里

封装提供了三方面的优点
首先,因为调用方不能直接修改对象的变量值,其只需要关注少量的语句并 且只要弄懂少量变量的可能的值即可。
第二,隐藏实现的细节,可以防止调用方依赖那些可能变化的具体实现,这样使设计包的程序员在不破 坏对外的api情况下能得到更大的自由。
第三个优点也是最重要的优点,是阻止了外部调用方对对象内部的值任意地进行修改。

接口

接口类型是对其它类型行为的抽象和概括;因为接口类型不会和特定的实现细节绑定在一起,通过这种 抽象的方式我们可以让我们的函数更加灵活和更具有适应能力。

接口类型具体描述了一系列方法的集合,一个实现了这些方法的具体类型是这个接口类型的实例。

io.Writer类型是用的最广泛的接口之一,因为它提供了所有的类型写入bytes的抽象,包括文件类型, 内存缓冲区,网络链接,HTTP客户端,压缩工具,哈希等等。

一个类型如果拥有一个接口需要的所有方法,那么这个类型就实现了这个接口。

接口指定的规则非常简单:表达一个类型属于某个接口只要这个类型实现这个接口。

type Human interface {
	Say(string) string
	Write(string)
	Read(string) string
}

type Man struct{
	Human
}
func (m *Man)Say(str string)string{
	return ""
}
func (m *Man)Write(str string){
}
func (m *Man)Read(str string)string{
	return ""
}

实际上interface{}被称为空接口类型是不可或缺的。因为空接口类型对实现它 的类型没有要求,所以我们可以将任意一个值赋给空接口类型。

接口值,在Go语言中,变量总是被一个定义明确的值初始化,即使接口类型也不例外。对于一个接口的零值就是 它的类型和值的部分都是nil。

一个不包含任何值的nil接口值和一个刚好包含nil指针的接口值是不同的。

Goroutines

在Go语言中,每一个并发的执行单元叫作一个goroutine。

当一个程序启动时,其主函数即在一个单独的goroutine中运行,我们叫它**main goroutine**。新的 goroutine会用go语句来创建。

如果说goroutine是Go语音程序的并发体的话,那么channels它们之间的通信机制。一个channels是一个 通信机制,它可以让一个goroutine通过它给另一个goroutine发送值信息。

使用内置的make函数,我们可以创建一个channel。和map类似,channel也一个对应make创建的底层数据结构的引用

一个channel有发送和接受两个主要操作,都是通信行为。一个发送语句将一个值从一个goroutine通过 channel发送到另一个执行接收操作的goroutine。发送和接收两个操作都是用<-运算符。在发送语句 中,<-运算符分割channel和要发送的值。在接收语句中,<-运算符写在channel对象之前。一个不使用 接收结果的接收操作也是合法的。

Channel还支持close操作,用于关闭channel,随后对基于该channel的任何发送操作都将导致panic异 常。对一个已经被close过的channel之行接收操作依然可以接受到之前已经成功发送的数据;如果 channel中已经没有数据的话讲产生一个零值的数据。

以最简单方式调用make函数创建的时一个无缓存的channel,但是我们也可以指定第二个整形参数,对应 channel的容量。如果channel的容量大于零,那么该channel就是带缓存的channel。

一个基于无缓存Channels的发送操作将导致发送者goroutine阻塞,直到另一个goroutine在相同的Channels上执行接收操作,当发送的值通过Channels成功传输之后,两个goroutine可以继续执行后面的 语句。反之,如果接收操作先发生,那么接收者goroutine也将阻塞,直到有另一个goroutine在相同的 Channels上执行发送操作。

基于无缓存Channels的发送和接收操作将导致两个goroutine做一次同步操作。因为这个原因,无缓存 Channels有时候也被称为同步Channels。当通过一个无缓存Channels发送数据时,接收者收到数据发生 在唤醒发送者goroutine之前(译注:happens before,这是Go语言并发内存模型的一个关键术语!)。在讨论并发编程时,当我们说x事件在y事件之前发生(happens before),我们并不是说x事件在时间上 比y时间更早;我们要表达的意思是要保证在此之前的事件都已经完成了。

基于channels发送消息有两个重要方面。首先每个消息都有一个值,但是有时候通讯的事实和发生的时 刻也同样重要。当我们更希望强调通讯发生的时刻时,我们将它称为消息事件。有些消息事件并不携带 额外的信息,它仅仅是用作两个goroutine之间的同步,这时候我们可以用struct{}空结构体作为 channels元素的类型,虽然也可以使用bool或int类型实现同样的功能,done <- 1语句也比done <- struct{}{}更短。

ch := make(chan int)
	go func() {
		fmt.Println("1")
		ch <- 1
		go func() {
			fmt.Println("2")
		}()
	}()
<-ch  //priint 1 2

Channels也可以用于将多个goroutine链接在一起,一个Channels的输出作为下一个Channels的输入。这 种串联的Channels就是所谓的管道(pipeline)

Go语言的类型系统提供了单方向的channel类型,分别用于只发送或只 接收的channel。类型chan<- int表示一个只发送int的channel,只能发送不能接收。相反,类型<-chan int表示一个只接收int的channel,只能接收不能发送。(箭头<-和关键字chan的相对位置表明了 channel的方向。)这种限制将在编译期检测。

带缓存的Channel内部持有一个元素队列。队列的最大容量是在调用make函数创建channel时通过第二个 参数指定的。

缓存Channel的发送操作就是向内部缓存队列的尾部插入原因,接收操作则是从队列的头部删除元素。 如果内部缓存队列是满的,那么发送操作将阻塞直到因另一个goroutine执行接收操作而释放了新的队列 空间。相反,如果channel是空的,接收操作将阻塞直到有另一个goroutine执行发送操作而向队列插入 元素。

两个慢的goroutines将会因为没有人接收而被永远卡住。这种情 况,称为goroutines泄漏,这将是一个BUG。和垃圾变量不同,泄漏的goroutines并不会被自动回收,因 此确保每个不再需要的goroutine能正常退出是重要的。

这个 计数器需要在多个goroutine操作时做到安全并且提供提供在其减为零之前一直等待的一种方法。这种计 数类型被称为sync.WaitGroup

每一个OS线程都有一个固定大小的内存块(一般会是2MB)来做栈,这个栈会用来存储当前正在被调用或挂 起(指在调用其它函数时)的函数的内部变量。

一个goroutine会以一个很小的栈开始其生命周期,一般只需要2KB。一个goroutine的栈,和操作 系统线程一样,会保存其活跃或挂起的函数调用的本地变量,但是和OS线程不太一样的是一个goroutine 的栈大小并不是固定的;栈的大小会根据需要动态地伸缩。而goroutine的栈的最大值有1GB,比传统的 固定大小的线程栈要大得多,尽管一般情况下,大多goroutine都不需要这么大的栈。

OS线程会被操作系统内核调度。每几毫秒,一个硬件计时器会中断处理器,这会调用一个叫作scheduler 的内核函数。这个函数会挂起当前执行的线程并保存内存中它的寄存器内容,检查线程列表并决定下一 次哪个线程可以被运行,并从内存中恢复该线程的寄存器信息,然后恢复执行该线程的现场并开始执行 线程。

Go的运行时包含了其自己的调度器,这个调度器使用了一些技术手段,比如m:n调度,因为其会在n个操 作系统线程上多工(调度)m个goroutine。

Go的调度器使用了一个叫做GOMAXPROCS的变量来决定会有多少个操作系统的线程同时执行Go的代码。其 默认的值是运行机器上的CPU的核心数,所以在一个有8个核心的机器上时,调度器一次会在8个OS线程上 去调度GO代码。(GOMAXPROCS是前面说的m:n调度中的n)。在休眠中的或者在通信中被阻塞的goroutine是 不需要一个对应的线程来做调度的。

goroutine没有可以被程序员获取到的身份(id)的概念。这一点是设计上故意而为之,由于thread-local storage总是会被滥用。

共享变量的并发

竞争条件指的是程序在多个goroutine交叉执行操作时,没有给出正确的结果。

数据竞争会在两个以上的goroutine并发访问相同 的变量且至少其中一个为写操作时发生。根据上述定义,有三种方式可以避免数据竞争:
第一种方法是不要去写变量。
第二种避免数据竞争的方法是,避免从多个goroutine访问变量。
第三种避免数据竞争的方法是允许很多goroutine去访问变量,但是在同一个时刻最多只有一个 goroutine在访问。这种方式被称为“互斥”。

sync.Mutex互斥锁,在Lock和Unlock之间的代码段中的内容goroutine可以随便读取或者修改,这个代码段叫做临界区。 goroutine在结束后释放锁是必要的。

sync.RWMutex读写锁,其允许多个 只读操作并行执行,但写操作会完全互斥。这种锁叫作“多读单写”锁(multiple readers, single writer lock),Go语言提供的这样的锁是sync.RWMutex。

发布了8 篇原创文章 · 获赞 0 · 访问量 257

猜你喜欢

转载自blog.csdn.net/tankyHua/article/details/101206896