Go常见知识点汇总


前言

对于工作和学习中遇到,容易忘记的知识点进行汇总,方面以后使用的时候,能够及时查到相关点,节约重新学习和搜索的时间。


GO工程布局

此部分主要总结关于Go语言开发过程中,实现工程化方面知识点。

同一个目录下怎样声明源码包

在同一个目录下的源码文件都需要被声明为属于同一个代码包,这有这有才能通过编译。

源码文件代码包声明的基本规则

第一条:同目录下的源码文件的代码包声明语句要一致。也就是说,他们要同属于一个代码包。这对于所有源码文件都是适用的。
第二条:源码文件声明的代码包的名词可以与其所在的目录的名称不同。在针对代码包进行构建时,生成的结果文件的主名称与其父目录的名称一致。

源文件声明代码包与目录名不同如何使用

源文件所在的目录相对于src目录的相对路径就是他的代码包导入路径,而是将使用其程序实体是给定的限定符(lib.Func,其中lib是限定符,Func是实体)要与它声明所属的代码包名称对应。

实体的首字母大小写有何区别

名称的首字母为大写的程序实体才可以被当前包外的代码引用,否则它就只能被当前包内的其他代码引用。


GO实体

什么是程序实体

在go语言中,程序实体是变量、常量、函数、结构体和接口的统称。使用之前必须先定义程序实体,让后再去使用。程序实体的名字呗统称为标识符,使用来被程序标识和使用的。

类型推断的好处

Go语言的类型推断可以明显提升程序的灵活性,使得代码重构变得更加容易,同时又不会给代码的维护带来额外负担(实际上,它恰恰可以避免散弹式的代码修改),更不会损失程序运行效率。

变量重声明前提条件

  1. 由于变量的类型在初始化时就已经确定了,所以对它再次声明对赋予的类型必须与其原本的类型相同,否则会产生编译错误。
  2. 变量的重声明只可能发生在一个代码块中。如果与当前的变量重名的是外层代码块中的变量,那么久是另外一种含义了,覆盖。
  3. 变量的重声明只有在使用短变量声明时才会发生,否则无法通过编译。如果要在此处声明全新的变量,那么就应该使用包含关键字var的声明语句,但是这时就不能与同一代码快中的任何变量由重名。
  4. 被”声明并赋值”的变量必须是多个,并且其中至少有一个是新的变量。这时我们才可以说对其中的旧变量进行了重声明。
  5. 样例var err error;n,err := Func()

引用程序实体时,查找过程

  • 首先,代码引用变量的时候总会最优先查找当前代码块中的那个变量。不包含任何子代码块。
  • 其次,如果当前代码块中没有声明以此为名的变量,那么程序会沿着代码块的嵌套关系,从直接包含当前代码块的那个代码开始,一层层向上查找。
  • 一般情况下,程序会一直查到当前代码包代表的代码块。如果仍然找不到,那么Go语言的编译器就会报错,由于限定符的原因。
  • 特殊情况,如果我们把代码包导入语句写成import . XXX形式,就会让这个“XXX”包中公开的程序实体,被当前源码代码中的代码,视为当前代码包中的程序实体。

类型断言表达式

  • 类型断言表达式的语法形式为x.(T),其中的x代表要被判定类型的值,这个值必须为接口类型,不过具体哪个接口类型其实无所谓,T为类型自变量。
  • 常用断言表达式为:value, ok := interface{}(countainer).([]string)value := interface{}(countainer).([]string)其中前者表示将结果赋值给两个变量,valueok,ok为布尔值,true时,类型判定正确,被判定的值江北自动转换为[]string类型值赋予value,否okfalsevaluenil;后者当判定为否时将会引起panic异常。
  • 核心图片:

{}含义

  • 一对不包裹任何东西的花括号,除了可以代表空的代码块之外,还可以用于表示不包含任何内容的数据结构(或者说数据类型)。
  • 举例:struct{},代表不包含任何字段和方法的,空的数据体类型;interface{},代表不包含任何方法定义的,空的接口类型;对于一些集合类的数据类型来说,{}还可以用来表示其值不包含任何元素,比如空的切片值[]string{},以及空的字典值map[int]string{}

类型转换规则

  • 类型转换表达式的基本写法为T(x),其中的T为类型,x可以是一个变量,也可以是一个代表值得字面量(比如1.23和struct{}),还可以是一个表达式。x称为源值,类型是源类型,而T代表的类型就是目标类型。
  • 类型转换常见坑:
    1. 首先,对于整数类型值,整数常量之间的类型转换,原则上只要源值在目标类型的可表示范围内就是合法的。
    2. 把一个 整数值转换为一个string类型的值是可行的,但值得关注的是,被转换的整数值应该可以代表一个有效的Unicode代码点,否则转换结果将会是“�”(仅由高亮的问好组成的字符串值)。
    3. string类型与各切片类型之间的互换,常见切片为[]byte[]rune

别名类型和潜在类型

  • 核心图片:
  • 别名类型与源类型的区别只在名字上,它们完全是相同的。别名类型主要是为了代码重构而存在的。
  • 类型再定义,完全定义了一个不同的类型,被重定义类型为潜在类型,其含义是某个类型在本质上市那个类型,或者是那个类型的集合。如何两个潜在类型相同,却属于不同类型,他们之间是可以进行类型转换的,但是不能进行判等或比较,它们的变量之间也不能赋值。再定义类型与潜在类型的值,也可以使用类型转换表达式进行转换。


数组与切片

表示方式不同之处

  • 切片可以看做是对数组的一层简单的封装,因为在每个切片的底层数据结构中,一定会包含一个数组。数组可以被叫做切片的底层数组,而切片也可以被看做是对数组某个连续片段的引用。
  • Go语言的切片类型属于引用类型,同属于引用类型的还有字典类型、通道类型、函数类型;而Go语言的数组类型则属于值类型,同属于值类型的有基础数据类型以及结构体类型。
  • 数组和切片之上都可以应用索引表达式,得到的都会是某个元素。在它们之上也都可以应用切片表达式,也都会得到一个新的切片。
  • 核心图片:

切片与数组之间的关系

  • 有一窗口,可以通过这个窗口看到一个数组,但是不一定能看到该数组中的所有元素,有时候只能看到连续的一部分数据。其窗口就是切片是对数组的封装,窗口的长度,就是切片本身的长度。
  • 核心图:

切片的底层数组什么时候被替换

  • 确切地说,一个切片的底层数组永远不会被替换,因为只要有切片的创建和数据的追加都是产生一个新的切片类型,来执行新的底层数组或旧的底层数组。每次的操作都是把新的切片作为底层数组的窗口,而没有对原切片进行任何改动。
  • 举例:在无需扩容时,append函数返回的是指向源底层数组的新切片,而在需要扩容时,append函数返回的是指向新底层数组的新切片。只要长度不会超过切片的原容量,那么使用append函数对其追加元素的时候就不会引起扩容,这只会使紧邻切片窗口右边的(底层数组中的)元素被新的元素替换掉。


字典类型

字典的键类型不能使那些类型

  • Go语言规范规定,在键类型的值之间必须可以施加操作符==!=(处理哈希碰撞)。换句话说,键类型的值必须要支持判等操作。由于函数类型、字典类型和切片类型的值并不支持判等操作,所以字典的键类型不能是这些类型。 另外,如果键的类型是接口类型,那么键值的实际类型也不能是上述三种类型,否则在程序运行过程中会引发panic(即运行时恐慌)。
  • 如果键的类型是数组类型,那么还要确保该类型的元素类型不是函数类型、字典类型或切片类型。
  • 如果键值类型是结构体类型,那么还要保证其中字段的类型的合法性。

优先考虑哪些类型作为字典的键类型

  • 从性能的角度来看,在整个映射过程中,“把键值转换为哈希值”以及“把查找到的哈希值与哈希桶中的键值做对比”,是两个重要耗时操作,因此可知,求哈希和判等操作的速度越快,对应的类型就越合适作为键类型。
  • 对于所有的基本类型、指针类型、以及数组类型、结构体类型和接口类型,Go语言都有一套算法。这套算法就包含哈希和判等,以求哈希的操作为例,宽度(单个值需要占用的字节数)越小的类型速度通常越快。
  • 对于数组类型的值求哈希实际上是依次求得它的每个元素的哈希值并进行合并,所以速度就取决于它的元素类型以及它的长度。
  • 对于结构体类型的值求哈希实际上就是对它的所有字段值求哈希并进行合并,所以在于它的各个字段的类型以及字段的数量。
  • 对于接口类型,具体的哈希算法,则有值得实际类型决定。

在值为nil的字典上执行那些操作成功与失败

  • 对于仅声明而不初始化的字典,它的值是nil,因此叫做nil字典。除了添加键-元素对,在一个键值为nil的字典上做任何操作都不会引起错误。若试图添加键-元素对,运行时系统就会抛出一个panic
  • 对于空map类型可以进行添加,而对于切片类型的nil和空都可以用append进行添加。


通道类型

不要通过共享内存来通信,而应该通过通信来共享内存。

通道基础知识

  • 在声明并初始化一个通道的时候,需要用到Go语言内建函数make,其要传入的参数必须含有通道类型的元素类型字面量(chan int)。在其后还可以接受一个int类型的参数(不能小于0),用来缓存通道(大于0)和非缓存通道。
  • 一个通道相当于一个先进先出(FIFO)的队列。元素发送和接受都需要用到操作符<-,可叫做接送操作符,一个左尖括号紧接着一个减号形象地代表了元素值得传输方向。

通道的发送和接受存在哪些基本操作

  1. 对于同一个通道,发送操作之间是互斥的,接受操作之间也是互斥的。
    在同一时刻,即使在并发情况下,运行时系统只会执行,对同一个痛的的任意个接收(发送)操作中的某一个,直到这个元素值被完全复制(副本)进(出)该通道之后,其它针对该通道的接收(发送)操作才可能被执行。
  2. 发送操作和接受操作中对元素值的处理都是不可分割的。
    对于通道中的同一个元素值来说,发送操作和接收操作之间也是互斥的,某一个处理元素都是一气呵成,绝对不会被打断。例如发送操作要么还没有复制元素值,要么已经复制完毕,绝不会出现只复制 了一部分的情况,接收操作在准备好元素的副本之后,一定会删除掉通道中的原值,决不会出现通道中仍有残留的情况。这即使为了保证通道中元素值的完整性,也是为了保证通道操作的唯一性。对于通道中的同一个元素值来说,它只可能是某一个发送操作放入的,同时也只可能被某一个接受操作取出。
  3. 发送操作在完全完成之前会被阻塞,接受操作也是如此。
    一般情况下,发送操作包括了“复制元素值”和“放置副本到通道内部”,而接受操作通常包含了“复制通道内的元素值”、“放置副本到接收方”、“删掉原值”,在所有这些步骤完成之前,发起该操作的代码会一直阻塞,直到所有步骤完成后,运行时系统会通知这句代码所在的goroutine,以使它争取据需运行代码的机会。
    阻塞代码其实是为了实现操作的互斥和元素值的完整。

通道不一定会作为数据传输的中转通道

  • 对于非缓冲通道,无论是发送操作还是接收操作,一开始执行就会被阻塞,直到配对的操作也开始执行,才会继续传递,数据直接从发送方赋值到接受方,中间并不会用非缓冲通道做中转,其中方式为同步传递数据。也就是说只要双方对接上,数据才会传递。
  • 对于缓冲通道,大多数情况下作为双方的中间件,元素值会先从发送方复制到缓冲通道,之后再由缓冲通道复制给接收方。但是,当发送操作在执行的时候发现空的通道中,正好有等待的接受操作,那么它会直接把元素值复制给接收方。

缓冲通道满时如何处理

通道已满,那么对他的所有发送操作都会被阻塞,它所有的goroutine会顺序第进入通道内部的发送队列,当通道中有元素值被接受走,这时,通道会有限通知最早因此而等待的,那个发送操作所在的goroutine,使其进行发送操作,所有的通知顺序总是公平的。

操作nil通道现象

对于值为nil的通道,不论它的具体类型是什么,对它的发送操作和接受操作都会永远地处于阻塞状态。他们所属的goroutine中的任何代码,都不再被执行。由于通道类型是引用类型,当我们只声明该类型的变量没有用make函数对其进行初始化时,该变量的值会是nil,因此一定不要忘记初始化通道。

发送和接收操作在什么时候会引发panic

  • 对于一个寂静初始化,但并未关闭的通道来说,收发操作一定不会引起panic,但是通道一旦关闭,再对它进行发送操作,就会引起panic
  • 如果试图关闭一个已经关闭了的通道,也会引发panic。注意,接收操作可以感知到通道的关闭,并且安全退出。
  • 当把接收表达式的结果同时赋值给两个变量时,第二个变量的类型是bool类型,它的值如果是false就说明通道已经关闭,并且再没有元素可取。
  • 如果通道关闭时,里面还有元素值未被取出,那么接收表达式的第一个结果,仍会是通道中的某一个元素值,而第二个结果值一定会是true,因此,通过接收表达式的第二个结果值,来判断通道是否关闭时可能有延时的。
  • 有上述通道收发操作的特性,所以除非有特殊的保障措施,我们千万不要让接收方关闭通道,而应该让发送方做这件事。

单向通道的应用价值

  • 单向通道最主要的用途就是约束其它代码的行为。例如:接口类型声明一个方法,其只会接收一个发送通道作为参数,所以,在该接口的所有实现类型中的此方法都会受到限制。对于编写模块代码或者可扩展的程序库的时候很有用。对于调用只接受发送的函数,只需要把一个元素类型匹配的双向通道传给它就行,没有必要用发送通道,因为Go语言在这种情况下会自动地把双向通道转换为函数所需的单向通道。

通过for range取通道数据

  1. for语句会不断尝试从通道中取出元素值,即使通道被关闭,它也会在取出所有剩余元素值之后再结束执行。
  2. 当通道中没有元素值时,他会被阻塞在由for关键字的那一行,直到有新的元素值可取。
  3. 通道的值为nil,那么它会被永远阻塞在由for关键字的那一行。

使用select语句注意事情

  1. select语句的分支分为两种,一种叫做候选分支,另一种叫做默认分支。候选分支总是以关键字case开头,后跟一个case表达式和一个冒号,然后可以从下一行开始写入分支被选中时需要执行的语句。默认分支为default:
  2. 由于select语句转为通道二设计的,所以每个case表达式中都只能包含操作通道的表达式,比如接收表达式。
  3. 若加入默认分支,无论涉及通道操作的表达式是否有阻塞,select语句都不会被阻塞。如果过若有表达式都阻塞了,或者都没有满足要求的条件,那么默认分支会被选中并执行。
  4. 如果没有加入默认分支,那么一旦所有的case表达式都没有满足求职条件,那么select语句就会被阻塞,直到至少有一个case表达式满足条件为止。
  5. 我们可能因为通道被关闭,而直接从通道接受到一个其元素类型的零值,因此,我们需要通过接收表达式的第二个结果值来判断通道是否已经关闭。一旦发现某个通道关闭了,我们就应该及时屏蔽掉对应的分支或者采取其他措施。者对于程序逻辑和性能是有好处。
    select语句只能对其中的每一个case表达式各求值一次。若要是连续或定时第操作其中的通道的话,就需要通过在for语句中嵌套select语句的方式实现。注意若select配合break语句,其只能结束当前select语句的执行,而并不会对外层的for语句产生作用。

select语句的分支选择规则

  1. 对于每一个case表达式,都至少会包含一个代表发送(接收)操作的发送(接收)表达式,同时也可能会包含其它的表达式。比如,如果case表达式是包含了接收表达式的短变量声明时,那么在赋值符号左边的就可能是一个或两个表达式,不过此处的表达式的结果必须是可以被赋值的。当这样的case表达式被求值时,它包含的多个表达式总会以从左到右的顺序被求值。
  2. select语句包含的候选分支中的case表达式都会在该语句执行开始时先被求值,并且求值的顺序是依从代码编写的顺序从上到下。结合上一条规则,在select语句开始执行时,排在最上边的候选分支中最左边的表达式会最先被求值,然后是他右边的表达式。仅当最上边的候选分支中所有表达都被求值完毕后,从上边数第二个候选分支中的表达式才会被求值,顺序同样从左到右,然后第三个候选分支,第四个候选分支,依次类推。
  3. 对于每一个case表达式,如果其中的接收或发送表达式再被求值时,若操作处于阻塞状态,那么case表达式的求值不成功,此条case表达式所在的候选分支不满足选择条件。
  4. 仅当select语句中所有case表达式被求值完毕后,它才开始选择候选分支。如果所有的候选分支都不满足选择条件,那么走默认分支,若无默认分支,select语句立即进入阻塞状态,直到至少有一个候选分支满足选择条件为止,select语句的goroutine被唤醒,执行此分支。
  5. 若select语句发现同事有多个候选分支满足选择条件,那么它就会用一种伪随机的算法,从这些分支中选择一个并执行。注意,即使select语句时在被唤醒时发现这种情况,也是这么做。
  6. 一条select语句中只能够有一个默认分支,并且,默认分支只在无候选分支可选时才会被执行,且与它的编写位置无关。
  7. select语句的每次执行,包括case表达式求值和分支选择,都是独立的。不过至于执行是否并发安全,要看其中的case表达式以及分支中,是否包含并发不安全的代码。


函数类型

在Go语言中,函数是一等公民,函数类型也是一等的函数类型。这意味着函数不但可以用于封装代码、分割功能、解耦逻辑,还可以作为普通值,在其它函数间传递、赋予变量、做类型判断和转换等。函数值可以由此成为能够被随意传播的独立逻辑组件。函数类型是引用类型,其零值为nil。

函数签名

函数签名,是函数的参数列表和结果列表的统称,它定义了可用来鉴别不同函数的那些特征,同时也定义了函数交换的方式。各个参数和结构的名称不能算作函数签名的一部分,甚至结果也可以没有名字。

高阶函数满足的条件

只要满足一下任意一个特点,就是一个高阶函数。

  1. 接收其它的函数作为参数传入。
  2. 把其它的函数作为结果返回。

闭包

  • 闭包,在一个函数中存在对外来标识符的引用,此标识符不属于函数的任何参数和结果,也不是函数内部声明的,此标识符称为“自由变量” ,对于“自由变量”呈现一种不确定状态,因此说它内部逻辑并不完整,“自由变量”到底代表什么在闭包函数被定义的时候是未知的,只知道其类型。
  • 闭包的意义:可以动态生成那部分程序逻辑。
  • 核心图:

核心原则

既不要把你程序的细节暴露给外界,也尽量不要让外界的变动影响到你的程序。
使用一个变量给另一个变量赋值,那么真正赋给后者的,并不是前者持有的那个值,而是该值得一个副本。


结构体类型

结构体类型表示是对一些数据的封装,其包含若干个字段,每个字段通常需要有确切的名字和类型。

struct{}

struct{}为空结构体,用来节省空间, 同时在向别人表明,这里并不需要一个值。例如:

  1. 在map里面节省资源用途:set := make(map[string]struct{})
  2. 在struct{}可以向人展示对象中部需要任何数据,仅包含需要方法。
  3. 使用channel,但并不需要附带任何数据。

函数与方法的区别

  • 函数是独立的程序实体,有名无名即可,还可以作为普通值传递。把具有相同签名的函数抽象成类型,作为一类逻辑组件的代表。
  • 方法必须要有名字,不能当做值看待,它必须属于某个类型,方法所属的类型会通过其声明的接收者(receiver,声明在关键字func和方法名之间圆括号内,必须包含名称和类型字面量)声明体现出来。

方法的基本特性

  • 接收者的类型起始就是当前方法所属的类型,接收者的名称,则用于在当前方法中引用它所属的类型的当前值(字段和方法包括自己方法)。
  • 方法隶属于的类型并不局限于结构体类型,但必须是自定义的数据类型,并且不能是接口类型。
  • 一个数据类型关联的所有方法,共同组成该类型的方法集合,同一个方法集合中的方法不能出现重名。若它们属于一个结构体类型,那么它们的名字与该类型中任何字段的名字也不能重复。

嵌入字段的基本特性

  • 如果一个字段声明中只有字段的类型名,而没有字段名称,则成为嵌入字段,也被称为匿名字段。通过匿名类型变量名后跟“.”(选择表达式),再跟匿名内部字段,可进行匿名内部字段访问。因此嵌入字段的类型及时类型也是名称。
  • 嵌入字段的方法集会被无条件地合并被嵌入类型的方法集合中。这样可以像访问被嵌入类型的字段那样,直接访问嵌入字段的字段。
  • 被嵌入类型和嵌入类型,方法只要名称相同,无论这两个方法的签名是否一致,被嵌入类型的方法都会“屏蔽”掉嵌入字段的同名方法。
  • 由于嵌入字段的字段和方法都会“嫁接”到被嵌入类型上,所以即使在两个同名的成员一个是字段,另一个是方法的情况下,这种“屏蔽”现象依然会存在。
  • 即使屏蔽了,也可以通过链式的选择表达式。
  • “屏蔽”现象会以嵌入的层级为依据,嵌入层级越深的字段或方法约有可能被“屏蔽”。
  • 在同一个层级的多个嵌套字段拥有同名的字段或方法,那么从被嵌入类型的值哪,选择此名称时会引起一个编译错误。

值方法和指针方法之间的不同点

  1. 值方法的接收者是该方法所属的那个类型值的一个副本。对该方法内对该副本的修改一般不会体现在原值上,除非这个类型本身是引用类型。值方法的接收者,是该方法所属的那个基本类型的指针值的一个副本,在这个方法内对该副本指向的值进行修改,一定体现在原值上。
  2. 一个自定义数据类型的方法集合中仅会包含它的所有值方法,而该类型的指针类型的方法集合,包括所有前者的值方法和所有指针方法。严格地讲,这样的接班类型的值上只能调用它的值方法,但是,Go语言会适时地转译(&)后,调用它的指针方法。
  3. 一个类型的方法集合有那些方法与它能实现那些结构类型息息相关。比如,一个指针类型实现了某个某个接口类型,但它的基本类型却不一定能够实现该接口的实现类型。


接口类型

接口基础

  • 接口类型无法被实例化,例如通new函数或make函数创建出一个接口类型值,也无法用字面量来表示一个接口类型值。
  • 对于任何类型,只要它的方法集合中完全包含了一个接口的全部特征,那么它就一定是这个接口的实现类型。
  • 对于一个接口类型的变量,赋给它的值成为实际值(动态值),而该值得类型成为变量的实际类型(动态类型),接口类型为静态类型。

判定数据类型某方法是否为接口的

两个充分必要条件:

  1. 两个方法的前面需要完全一致。
  2. 两个方法的名称一模一样。

接口类型存储方式

  • 接口类型本身无法被实例化,因此在赋予它实际的值前,它的值一定会是nil,即它的零值。
  • 接口类型中会包含两个指针,一个是指向类型信息的指针,一个是指向动态值得指针。这里的类型信息是由另一个专用数据结构的实例承载的,其中包含了动态值的类型,以及使它实现了接口的方法和调用它们的途径,等。
  • 接口变量被赋予动态值时,存储的是包含了这个动态副本的一个结构更加复杂的值。

关于nil接口

把一个值为nil的某个实现类型的变量赋值给接口类型,后者值也不可能是真正的nil。虽然这时它的动态值为nil,但它的动态类型确是存在的。除非只声明而不初始化,或者显示地赋给他nil,否则接口变量的值就不会为nil。

接口组合

  • 接口类型嵌入(组合)比结构体类型更简单一些,因为它不会涉及方法间的“屏蔽”。只要组合的接口之间有同名的方法就会产生冲突,从而无法通过编译,即使同名方法的签名彼此不同也会是如此。

使用接口准则

  • 使用小接口和接口组合总是有益的,可形成接口矩阵,进而搭起灵活的程序框架。如果在实现接口时再配合运用结构体类型间的嵌入手法,那么接口组合就可以发挥更大的效用。


指针操作

Go语言中不可寻址的值

  1. 常量的值。
  2. 基本类型值的字面量。
  3. 算术操作的结果值。
  4. 对各种字面量的索引表达式和切片表达式的结果值。有一例外,对切片字面量的索引结果值确实可以寻址的(其底层存在数组,底层数组中的每个元素都是有一个确切的内存地址的)。
  5. 对字符串变量的索引表达式和切片表达式的结果值。
  6. 对字典变量的索引表达式结果。
  7. 函数字面量和方法字面量,以及对它们的调用表达式的结果值。
  8. 结构体字面量的字段值,也就是对结构体字面量的选择表达式结果值。
  9. 类型转换表达式的结果值。
  10. 类型断言表达式的结果值。
  11. 接收表达式的结果值。

不可寻址值的特点

  1. 不可变:不可修改
    • 常量的值存储到确切的内存区域中,并且这种值肯定是不可变的。
    • 基本类型的字面量被视为常量,没有任何标识代表它们。
    • 字符串值也是不可变,对于一个字符串类型的变量,基于它的索引或切片的结果值也都是不可寻址的,及时拿到了这种值的内存地址,也不能改变。
  2. 临时结果:把结果值赋给任何变量和常量之前
    • 对某个值字面量施加的表达式的求值结果都看做是临时结果。例如,对数组字面量和字典字面量的索引结果值。
    • 对切片字面量的切片不可寻址,因为切片表达式总会返回一个新的切片值,而这个新的切片值在被赋给变量之前属于临时结果。
    • 对于数组值、切片值的字面量的表达式会产生临时结果,但对于它们类型的变量,那么索引或切片的结果值就不属于临时结果了。
  3. 不安全:破坏程序的一致性,引发不可预知的错误
    • 由于字典内部的机制,对字典的索引结果的取址操作不安全。

Go语言中的常用表达式

  • 用于获取某个元素的索引表达式。
  • 用于获取某个切片(片段)的切片表达式。
  • 用于访问某个字段的选择表达式。
  • 用于调用某个函数或方法的调用表达式。
  • 用于转换值类型的类型转换表达式。
  • 用于判断值的类型的类型断言表达式。
  • 向通道发送元素值或从通道哪里接收元素值的接收表达式。

字典类型与寻址

由于字典中总会有若干个哈希桶均匀第存储键值-元素对,当满足一定条件时,字典会改变哈希桶的数量,并适时地把其中的键-元素对搬运到对应的新的哈希桶中,所以获取字典中任何元素值得都是无意义的也是不安全的,但是可以对其值进行瞬间赋值和操作。

  1. 虽然对字典字面量和字典变量索引表达式的结果都是不能寻址的,但是这样的表达式却可以被用在自增语句和自减语句中。
  2. 在赋值操作符左边的表达式的结果值别虚可以寻址的,但是对字典的索引结果也是可以的。
  3. 在带有range字句的for语句中,在range关键字左边的表达式的结果值都必须是可寻址的,不过对字典的索引结果值同样也可以被用在这里。

unsafe.Pointer

  • 对于指针值和uintptr类型值之间的转换,必须使用unsafe.Pointer类型的值作为中转。


go函数

进程与线程

  • 一个进程至少包含一个线程。若只包含一个,那么它里面的所有代码都会串行地执行。每个进程的第一个线程都会随着该进程的启动而被创建,其被称为所属进程的主线程。若一个进程包含多个线程,其中的代码可以被并发执行,除了进程的第一个线程外,其它的线程都是由进程中存在的现场创建出来的。
  • 主线程之外的其他线程都是由代码显示地创建和销毁。这些要编写代码时手动控制,操作系统执行。

用户级线程

用户级线程指构建在系统级线程之上,由用户层面完全控制代码执行流程,包括创建、销毁、调度、状态变更以及其中的代码和数据都要完全需要用户层面程序自己去实现和处理。即用户层自己实现一套底层的线程处理流程。

  • 优势:其创建和销毁不用通过操作系统去做,速度快;不用等着操作系统去调度他们运行,容易灵活控制。
  • 劣势:复杂,若用底层的只需要调用指令即可;若用户层面需要自己维护线程的处理流程和实现;还必须与底层线程正确对接,否则用户级线程就无法被并发地,正确执行。

go协程

Go语言有这独特的并发编程模型,以及用户级线程goroutine,强大的调度goroutine、对接系统级线程的调度器。

  • 调度器主要负责统筹调配,其组成部分:G(goroutine)用户级线程;P(processor)可以承载若干个G,且能够使这些G实时地与M进行对接,并得到真正运行的中介;M(machine)系统级别线程。
  • M、P、G关系简化核心图:

go的异步执行过程

  • 每条go语句一般都会携带一个函数调用,这个被调用的函数常常被成为go函数。而主goroutine的go函数就是main函数,程序运行准备工作完成后,自动地启用。
  • go函数真正被执行的时间,总会与其所属的go语句执行的时间不同。当程序执行到一条go语句时,runtime运行时系统,会先试图从某个存放空闲的G的队列中获取一个G(goroutine),它只有没有找到空闲G的情况下才会创建一个新的G。
  • 创建G的成本也是非常低的。创建一个G不会像新建一个进程或者一个系统级线程那样,必须通过操作系统的系统调用来完成。
  • 拿到一个空闲G之后,runtime会用这个G去包装当前的那个go函数(函数中的那些代码),然后再把这个G追加到某个存放可运行的G的队列中。
  • 这类队列中的G总是会按照先入先出的顺序,很快地由运行时系统内部的调度器安排运行。虽然很快,但是由于上面所说的那些准备工作还是不可避免的,所以耗时环视存在的。
  • go函数的执行时间总是会明显滞后于它所属的go语句的执行时间。此处相对于CPU时钟和GO程序来说的,我们在大多数时候都不会有明显的感觉。
  • go语句本身执行完毕后,Go程序完全不用等待go函数的执行,他会立刻执行后面的语句。
  • Go语言并不会去保证这些goroutine会以怎样的顺序运行,其中主goroutine和我们手动启用的其它goroutine会一起接受公平的调度。


for与switch语句

for range特性

  • 当for语句执行的实时,range关键字右边会先被求值,此位置称为range表达式。range表达式的结果值可以是数组、数组的指针、切片、字符串、字典或允许接收操作的通道中的某一个,并且结果值只能有一个。
  • range左边的称为迭代变量,根据不同种类range表达式结果值,for语句的迭代变量的数据可以有所不同。例如切片,两个的话,左边第一个迭代变量是索引值,第二个是元素值;仅有一个则为索引值。
  • range表达式只会在for语句开始执行时被求值一次,无论后边会有多少次迭代。
  • range表达式的求值结果会被复制,也就是说,被迭代的对象时range表达式结果值的副本而不是原值。

switch表达式与case表达式关系

  • 被夹在switch关键字和左花括号{之间的表达式式子,这个位置上的代码被称为switch表达式;被加载case和:之间的表达式,若多个用逗号隔开,此位置为case表达式。
  • 被选中的case子局附带的语句列表中包含了fallthrough语句,紧挨着它的按个case子句附带的语句也会被执行。
  • switch表达式结果类型,以及各个case表达式中子表达式的结果类型都要一致。只有类型相同的值之间才能有可能被允许进行判等操作。
  • 若case表达式结果值是无类型的常量,那么他的类型会被自动的转化为switch表达式的结果类型;若swtich表达式为无类型常量,那么这个常量会自动被转换为此种变量的默认类型,3->int 3.14->float64。
  • 若表达式的结果类型有某个接口类型,一定要检查他们的动态值是否都具有可比性(判等操作),否则可能引发panic。
  • 对于case表达式,不允许其中子表达式结果值存在相等的情况,不论是否存在不同的case表达式中。此条约束只针对结果值由字面量直接表示的子表达式,不包括变量。


错误处理

error类型

error类型其实是一个接口类型,也是GO的一个内建类型,这个接口类型声明了一个Error方法,不接受任何参数,返回一个string类型的结果。

错误判断技巧

  1. 对类型在已知范围内的一系列错误值,一般使用类型断言表达式或类型switch语句来判断。
  2. 对于已有相应变量且类型相同的一些列错误值,一般直接使用判等操作来判断。
  3. 对于没有相应变量且类型未知的一些列错误值,只能使用其错误信息的字符串表示形式来做判断。

构建错误体系的基本方法

  1. 创建立体的错误类型体系,例如网络错误处理。
  2. 创建扁平的错误值列表。

panic、recover、defer语句

panic执行流程

  • 某个函数的某行代码有意或无意地引发了一个panic,这时,初始的panic详情会被建立起来,并且该程序的控制权会立即从此行代码转移至调用其所属函数哪行代码上,控制权如此一级一级地沿着调用栈的反方向传播至顶端。随后,程序崩溃并终止,承载程序这从运行的进程也会随之死亡并消失。与此同时,在这个控制权传播的过程中,panic详情会被逐渐地积累和完善,并会在程序终止之前被打印出来。
  • 核心图:

panic传入参数

  • panic函数的唯一参数是空接口(interface{})类型,从语法上讲它可以接受任何类型值,但,最好传入error类型错误值,或者更易读地去表示形式转换(有效序列化)。
  • 对于fmt包下的各种打印函数来说,error类型值得Error方法与其他类型值得String方法都是等价的,他们唯一结果都是string类型,可以为不同的数据类型编写这两种方法。

recover 捕获 panic

  • Go语言的内建函数recover专用于恢复panic,函数无需任何参数,并且返回一个空接口类型值。
  • panic一旦发生,控制权就会迅速地沿着调用栈的反方向传播,所有panic函数之后的代码都没有执行机会,因此recover应该书写在panic函数之前,并且要配合defer语句使用。
  • Go语言运行时系统自动抛出的panic都属于致命错误,都是无法被恢复的,调用recover函数对它们不起任何作用。例如,一旦产生死锁,程序必崩溃。

defer语句

  • defer语句被用来延迟执行代码的,延迟到该语句所在的函数即将执行结束的那一刻,无论结束执行的原因是什么,也包括panic引起的。
  • Go语句有些类似,一个defer语言总会由一个defer关键字和一个调用表达式(函数)组成。有些调用表达式不能再这里出现,包括针对Go语言内建函数的调用表达式,以及针对unsafe包中的函数的调用表达式。
  • defer被调用的可以是名的也可以是匿名的,这里的函数叫做defer函数或延迟函数。注意被延迟执行的是defer函数,而不是defer语句。

defer语句执行时发生的事情

  • defer语句每次执行的时候,Go语言会把他携带的defer函数以及参数值另外存储到一个队列中。
  • 这个队列与该defer语句所属的函数是对应的,并且,它是先进先出(FILO)的,相当于一个栈。
  • 在需要执行某个函数中的defer函数调用时候,Go语言会先拿到对应的队列,然后从该队列中一个一个地取出defer函数及其参数值,并逐个执行。

go测试

go测试程序分类

  • 功能测试(test)、性能测试(benchmark)、以及示例测试(example)。其中示例测试严格来讲也是一种功能测试,只不过它更关注程序打印出来的内容。

对测试函数的名称和签名规定

  • 对于功能测试函数来说,其名称必须以Test为前缀,并且参数列表中应有一个*testing.T类型的参数声明。
  • 对于性能测试函数来说,其名称必须以Benchmark为前缀,并且唯一参数的类型必须是*testing.B类型的。
  • 对于示例测试函数来说,其名称必须以Example为前缀,但对函数的参数列表没有强制规定。

go test命令执行主要流程

  • 在开始运行时,会先做一些准备工作,比如,确定内部需要用到的命令,代码包和源码包文件的有效性,判定给予的标记是否合法。
  • 对每个被测代码包,依次进行构建、执行包中符合要求的测试函数,清理临时文件,打印测试结果。
  • go test命令会串行地执行测试流程中的每个步骤,为了加速测试速度,它通常会并发地对多个被侧代码进行功能测试,但最后打印测试结果时,会依照测试程序给定的顺序逐个执行,给人的感觉它是完全串行地执行测试流程。
  • 性能测试,却是完全的真正意义上的串行。

sync.Mutex与sync.RWMutex

同步用途

  1. 避免多个线程在同一时刻操作同一个数据块。
  2. 协调多个线程,以避免它们在同一时刻执行同一个代码块。

同步保护手段

  • 实现某种同步机制的工具,称为同步工具。
  • 核心图片:

使用互斥锁注意事项

对一个已经被锁定的互斥锁进行锁定,它会立即阻塞在当前的goroutine的。这个goroutine所执行的流程,会一直停滞在调用该互斥锁的Lock方法的哪行代码上。

  1. 不要重复锁定互斥锁。
  2. 不要忘记解互斥锁,必要时使用defer语句。
  3. 不要对尚未锁定或者已解锁的互斥锁解锁。
  4. 不要在多个函数之间直接传递互斥锁。
  5. 不要传递互斥锁,它会产生它的副本。
    核心图片:

读写锁

  • 读写锁是读/写互斥锁的简称,一个读写锁包含两个锁,即:读锁和写锁,sync.RWMutex类型中的Lock和Unloc方法分别用于对写锁进行锁定和解锁,而它的Rlock和RUnlock方法分别用于对读锁进行锁定和解锁。
  • 读写锁规则:
    1. 在写锁已被锁定的情况下再试图锁定写锁,会阻塞当前的goroutine。
    2. 在写锁已被锁定的情况下试图锁定读锁,也会阻塞当前的goroutine。
    3. 在读锁已被锁定的情况下试图锁定写锁,同样会阻塞当前的goroutine。
    4. 在读锁已被锁定的情况下再试图锁定读锁,并不会阻塞当前的goroutine。
  • 对于多个写操作不能同时进行,写操作和读操作也不能同时进行,但多个读操作却可以同时进行。
  • 对写锁进行解锁,会唤醒“所有试图锁定读锁,而被阻塞的goroutine”,并且,这通常会是它们都成功完成对读锁的锁定。然而,对读锁进行解锁,只会在没有其它读锁锁定的前提下,唤醒“因试图锁定写锁,而被阻塞的goroutine”,并且,最终只会有一个等待时间长的被唤醒。

条件变量sync.Cond

条件变量

  • 条件变量时基于互斥锁的,它必须有互斥锁的支持才能发挥作用。
  • 条件变量并不是被用来保护临界区和共享资源的,他是用来协调想要访问共享资源的那些线程的。当共享资源的状态发生变化是,它可以被用来通知被互斥锁阻塞的线程。
  • 条件变量提供的方法有三个:等待通知(wait)、单发通知(signal)和广播通知(broadcast)。

条件变量Wait方法所做事情

  1. 把调用它的goroutine(也就是当前的goroutine)加入到当前条件变量的通知队列中。
  2. 解锁当前的条件变量基于的那个互斥锁。
  3. 让当前的goroutine处于等待状态,等到通知到来再决定是否唤醒它。此时,这个goroutine就会阻塞在调用这个Wait方法的按行代码上。
  4. 如果通知到来并且决定唤醒这个goroutine,那么就在唤醒它之后重新锁定当前条件变量基于的互斥锁。自此之后,当前的goroutine就会继续执行后面的代码。
    • 核心代码如下:
      func (c *Cond) Wait() {    
       c.checker.check()    
       t := runtime_notifyListAdd(&c.notify)    
       c.L.Unlock()    
       runtime_notifyListWait(&c.notify, t)    
       c.L.Lock()    
      }
      

Signal和Broadcast方法

  • 条件变量Signal方法和Broadcast方法都是被用来发送通知的,前者的通知只会唤醒一个因此而等待的goroutine,而后者的通知却会唤醒所有为此等待的goroutine。
  • 条件变量的Wait方法总会把当前的goroutine添加到通知队列的队尾,而它的Signal方法总会从通知队列的队首开始,查找可被唤醒goroutine。所以,因Signal方法的通知,而被唤醒的goroutine一般都是最早等待的那一个。
  • 与Wait方法不同,条件变量Signal和Broadcast方法并不需要在互斥锁的保护下执行,最好在解锁条件变量基于的那个互斥锁之后,再去调用它的两个方法。这更有利于程序的运行效率。
  • 通知的及时性,如果发送通知的时候没有goroutine为此等待,那么该通知就会被直接丢弃,在这之后才开始等待的goroutine只可能被后面的通知唤醒。


持续更新……


转载请注明:HunterYuan的博客 » Go常见知识点汇总

打赏一个呗

取消

感谢您的支持,我会继续努力的!

扫码支持
扫码支持
扫码打赏,你说多少就多少

打开支付宝扫一扫,即可进行扫码打赏哦