八 错误机制

Go中,使用error类型来表示错误,不过不再是一个整数类型,而是一个接口类型

type error interface {
Error() string
}

最常用的是errors.New() 函数,代码示例如下

func sqrt(f float64) (float64,error) {
if f <0 {
return 0,errors.New("param error")
}
}

通常更规范的使用方法如下

err:= errors.New("error example")
fmt.Printf("error is %s\n",err)

Go语言的多返回值使得返回错误异常简单,一般的错误使用error,对于真正的异常,Go提供panic-recover机制

go知识点专项 02_缓存



十 反射

   反射是指程序在运行时可以访问 ,检测和修改它本身状态或行为的一种能力,在运行的时候能够观察并且纠正自己的行为;

   Go语言提供了一种机制在运行时更新变量和检查它们的值,调用它们的方法,但是在编译时并不知道这些变量的具体类型,称为反射机制

 使用场景 

 (1) 不能明确接口调用哪个函数,需要根据传入的参数在运行时决定

 (2) 不能明确传入函数的参数类型,需要在运行时处理任意对象

反射缺点 :

  (1) 可读性差 :反射相关的代码,难以阅读

  (2) Go作为一门静态语言,编译器能提前发现一些类型错误,但是对于反射代码是无能为力的。所以包含反射相关的代码,很可能运行很久才出错,会直接panic

 (3) 反射性能差 : 比正常代码运行速度慢一到两个数量级

 反射通过接口的类型信息实现,建立在类型的基础上;Go语言在reflect包里定义了各种类型,实现了反射的各种函数,通过它们可以在运行时检测类型的信息,改变类型的值。

iface描述的是非空接口,eface描述的是空接口,不包含任何方法。Go语言的所有类型都“实现了“空接口

type iface struct {
tab *itab
data unsafe.Pointer
}

type itab struct {
inter *interfacetype
_type *_type
link *itab
hash unit32
fun [1] unitptr
}
type eface struct {
_type *_type
data unsafe.Pointer
}

eface只维护了一个_type字段,表示空接口所承载的具体的实体类型,data指向了具体的值

reflect包里定义了一个接口和一个结构体,即reflect.Type和reflect.Value

前者主要提供关于类型相关的信息,所以它和_type关联比较紧密,后者则结合_type和data两者,因此程序员可以获取甚至改变类型的值

TypeOf函数用来 提取一个接口中值的类型信息,由于它的输入参数是一个空的interface{} ,调用此函数时,实参会先被转化为interface{}类型。这样,实参的类型信息,方法集,值信息都存储到interface{}变量里了,看源码

func TypeOf(i interface{})Type {
eface := *(*emptyInterface)(unsafe.Pointer(&i))
return toType(eface.typ)
}

// 类型转换
func toType(t *rtype) Type {
if t == nil {
return nil
}
return t
}

Type定义非常多的方法,通过它们可以获取类型的一切信息;所有的类型都会包含rtype这个字段,表示各种类型的公共信息;另外,不同类型包含自己的一些独特的部分

Type接口实现了String()函数,满足fmt.Stringer接口,因此使用fmt.Println打印的时候,输出的是String()的结果

TypeOf函数举例  使用 reflect.TypeOf() 函数可以获得任意值的类型对象

/**
* 反射测试代码
*/

func main() {
var a int
var b string
typeOfA := reflect.TypeOf(a)
typeOfB := reflect.TypeOf(b)
fmt.Println(typeOfA.Name(), typeOfA.Kind())
fmt.Println(typeOfB.Name(), typeOfB.Kind())
}

go知识点专项 02_get方法_02

对于结构体类型,可以进一步获取结构体成员的类型信息

func main() {
// 声明一个空结构体
type cat struct {
Name string
// 带有结构体tag的字段
Type int `json:"type" id:"100"`
}
// 创建cat的实例
ins := cat{Name: "mimi", Type: 1}
// 获取结构体实例的反射类型对象
typeOfCat := reflect.TypeOf(ins)
// 遍历结构体所有成员
for i := 0; i < typeOfCat.NumField(); i++ {
// 获取每个成员的结构体字段类型
fieldType := typeOfCat.Field(i)
// 输出成员名和tag
fmt.Printf("name: %v tag: '%v'\n", fieldType.Name, fieldType.Tag)
}
// 通过字段名, 找到字段类型信息
if catType, ok := typeOfCat.FieldByName("Type"); ok {
// 从tag中取出需要的tag
fmt.Println(catType.Tag.Get("json"), catType.Tag.Get("id"))
}
}

go知识点专项 02_get方法_03

反射原理图示

go知识点专项 02_get方法_04

ValueOf函数,返回值reflect.Value表示interface{}里存储的实际变量,能提供实际变量的各种信息

总结 : TypeOf()函数返回一个接口,这个接口定义了一系列方法,利用这些方法可以获取关于类型的所有信息;ValueOf()函数返回一个结构体变量,包含类型信息以及实际值

go知识点专项 02_get方法_05

go知识点专项 02_get方法_06


go知识点专项 02_代码块_07

1、golang中反射最常见的使用场景是做对象的序列化(serialization,有时候也叫Marshal & Unmarshal) 例如,Go语言标准库的encoding/json、encoding/xml、encoding/gob、encoding/binary等包就大量依赖于反射功能来实现。
2、但是有时你希望在运行时使用变量的在编写程序时还不存在的信息。比如你正在尝试将文件或网络请求中的数据映射到变量中。或者你想构建一个适用于不同类型的工具。在这种情况下,你需要使用反射。反射使您能够在运行时检查类型。它还允许您在运行时检查,修改和创建变量,函数和结构体。

go知识点专项 02_代码块_08

十一 同步模式

Sync.WaitGroup 同步屏障

可以达到并发goroutine的执行屏障的效果,提供了用于创建等待多个并发执行的代码块在达到WaitGroup显式指定的同步条件后,才可继续执行wait调用后的后续代码能力

Add和Wait之间必须有严格的happens before关系

(1) 对于整个代码块中所创建的全部Add操作,可被严格分割为不交的两个组别A和B,若设置A happens before B

(2) 存在一个Wait操作 W,使得A happens before B

(3) 并且该 W happens before B

Sync.Pool 缓存池

大量重复地创建许多对象,GC工作量飙升,CPU频繁掉底,产生CPU毛刺。这时可以用sync.Pool来缓存对象,减轻对GC的消耗

调用Get方法时,如果池子Pool缓存了对象,就直接返回缓存的对象,如果没有,就New函数创建一个新的对象

Get方法取出来的对象和上次Put进去的对象实际上是同一个,Pool没有做任何“清空”的处理,所以需要在Put之前,把对象清空

gin框架,对context的取用,也使用了sync.Pool

go vet工具可以检测到用户代码是否复制了Pool,是go1.7引入的一个静态检查机制,不仅仅工作在运行时或者标准库,同时也对用户代码有效

PoolDeque 是一个队列的接口,poolDequeue是一个具体的实现者,实现为单生产者,多消费者的固定大小的无锁Ring 式队列,生产者可以从head插入,head删除,而消费者仅可以从tail删除


go知识点专项 02_缓存_09

关键函数

Pool.pin() 将当前goroutine和P绑定在一起,禁止抢占,并且返回对应的poolLocal以及p的id

popHead() 会从头删掉,并且返回queue的头结点,queue为空,返回false 

getSlow() 如果在shared里没有获取到缓存对象,则继续调用Pool.getSlow(),尝试从其他P的poolLocal偷取

popTail()从尾部移除一个元素

go知识点专项 02_代码块_10

victim

victim在poolCleanup的时候被赋值,而poolCLeanup在GC的时候被调用

先把victim和victimsize情空,再把local和localsize的数据全放过来 ;下一次GC来临之前,如果有Get调用则会从p.victim取,获取后也不放回victim,这样在一定程度上也减小了下一次GC的开销,原来1次GC的开销拉长到2次,减小了GC的抖动

Sync.Map

Go 1.9引入,线程安全的,读取,插入,删除也都保持着常数级别的时间复杂度,sync.Map的零值是有效的,并且零值是一个空的map

使用read和dirty两个map来进行读写分离,降低锁冲突来提高效率

go知识点专项 02_get方法_11

Store方法

go知识点专项 02_缓存_12

1.如果m.read存在这个key, 并且对应的entry没有标记为expunged(删除),就直接更新read中的entry;如果没有成功,代表read没有key或者标记为删除,就加锁,再进行后续操作

2.再次在read查找这个key,如果存在key但是标记为expunged(删除),此时更改expunged为nil (不删除),并且dirtyMap没有这个key,代表用到了这个key数据,这时候需要在dirty map插入key,然后直接更新对应的value

3.如果此时read还是没有这个key,就查看dirty是否有这个key,有就直接更新dirty中这个key的value

4.read和dirty都没有,就把read未删除的数据,增加到dirty中;同时把这个

key-value数据,写入到dirtyMap中

Load方法

go知识点专项 02_代码块_13

read主要用于读取,每次Load都先从read读取,当read中不存在且amended为true,就从dirty读取数据

无论dirty是否存在该key,都会执行missLocked函数,该函数将misses+1,当misses等于dirty的大小时,便会将dirty复制到read,此时再将dirty置为nil

Delete方法

1.先从read中查是否有这个key,有就执行entry.delete, 把p置为nil,这样dirty和read都能看到这个变化

2.如果read中没有这个key,并且dirty不为空,先上锁,double check,仍然在read没有这个key,先调用missLocked判断是否提升dirty到read。如果最终发现dirty有这个key,那就从dirty删除这个key对应的value,通过更改entry状态实现。

十二 调度机制

Go程序的执行 有两个层面 : Go Program和Runtime,即用户程序和运行时,通过函数来实现内存管理,channel通信,goroutine创建等功能。用户程序进行的系统调用都会被Runtime拦截,以此来帮助它进行调度以及垃圾回收相关的工作。

Runtime维护所有的goroutine,并通过scheduler进行调度,goroutine需要依赖thread才能执行

GMP 需要P的原因是 : 当一个线程阻塞的时候,将和它绑定的P上的goroutine转移到其他线程。当线程进行阻塞系统调用的时候,这时候无法执行其他代码,因此可以将与其相关联的P上的goroutine分配给其他线程运行。

Go scheduler 会启动一个后台线程sysmon,用来检测长时间(10ms) 运行的goroutine,将其停靠到global runqueues, 这是一个全局的runqueue,优先级较低,以示惩罚。

Go scheduler在支持抢占式调度的同时,还支持协作式调度,意味着抢占这一过程会通常被动地进行。在go 表现为 : 编译器会在每个函数的执行代码前插入一小段检测代码,一旦判断成立,则调度器会转去执行其他的goroutine,而原goroutine将被放回调度队列,等待后续的调度

Go scheduler默认状态下启用了抢占式调度,同时给予了用户禁用抢占式调度的权利。显然,抢占式调度的支持需要确保代码执行到了可被安全抢占的位置,因此抢占式调度比协作式调度实现上更加复杂。

Go scheduler每一轮调度要做的工作,就是找到处于runnable的goroutine,并执行它,顺序如下

(1) 从本地可运行队列里找

(2) 从全局可运行队列里找

(3) 从netpoll里找

(4) 从其他P偷取goroutine

goroutine 

(1) 尝试将g放到本地可执行队列

(2) 本地可执行队列runqueue满了,runnext将g放到全局队列里,如果添加失败,说明本地队列发生了变化,又有位置了,就重试retry放到本地队列中

Go sysmon后台监控线程

不依赖P直接执行,通过newm函数创建一个工作线程

sysmon 会进行netpool(获取fd事件)、retake抢占,forcegc(按时间强制执行GC),scavenge heap(释放闲置内存减少内存占用) 等处理

1.抢占处于系统调用的P,让其他M接管它,以运行其他的goroutine

2. 对于长时间运行的P,或者说绑定在P上的长时间运行的goroutine,sysmon会检测这种情况,然后设置一些标志,表明goroutine自己cpu的执行权,给其他goroutine一些执行机会

​https://zhuanlan.zhihu.com/p/471490292​