每一个gotoutine在runtime.g结构体中都包含了*defer指针字段,指向defer链表的头,而defer链表是从头部插入(先进后出)
1deferproc在编译的时候注册defer函数,
2函数返回之前,通过returndefer执行注册的defer函数,再return(先注册–返回前执行)
defer结构体
1栈帧先是函数A的两个局部变量a,b
2然后是A1的参数a=1,deferproc注册的fn地址以及参数siz
3 deferproc在堆上注册结构体,参数a=1(若函数有返回值的话还有返回值),link和panic为nil,调用者fn=addr2,返回地址addr,调用者栈指针sp of A, started为false,参数占8字节,这个结构体会添加到defer链表头,表头的defer先执行
4 执行defer,将defer结构体的参数和返回值拷贝到栈上,打印参数a=1
(编译阶段,deferproc将defer参数a=1拷贝到堆上,执行时拷贝到栈上defer的参数空间,注意区别defer的参数空间与函数A的局部变量)
先进后出
func Defer(){
for i:=0;i<5;i++{
fmt.Println(i)
}
}
defer : 4
defer : 3
defer : 2
defer : 1
defer : 0
先进后出
遇到panic ,先defer后panic
func defer_call() {
defer func() { fmt.Println("打印前") }()
defer func() { fmt.Println("打印中") }()
defer func() { fmt.Println("打印后") }()
panic("触发异常")
}
打印后
打印中
打印前
panic: 触发异常
defer语句包含函数的 先执行函数 再defer
defer捕获外部变量形成闭包,因为参数被修改,参数a在编译阶段就改为堆分配,栈上存地址,因为要表现闭包内部和外部操作同一参数:
1 参数a除了初始化还被修改过,所以逃逸到堆上,栈上只存地址,funcval也只存地址
2 第一步输出3,2,第二部闭包相加相加的时候,addr2指向堆上的地址=3,3+2=5,更新为5,闭包输出栈上局部变量a指向的地址,此时存储的是5
eg:deferproc(没有匿名函数func() ,不涉及闭包)
1deferproc注册A时a=1,A先要拿到B 的返回值,B将a的值+1返回给A,A再+1,等于3
go build -gcflags ‘-m -l’ main.go 分析上面代码,出了io,变量a确实没有逃逸
1,defer捕获的变量a=1
2,函数B加1后返回给A
1 defer链表存储A2-A1,执行链表时会判断defer是不是自己创建的,判断依据为defer结构体的sp of字段
2 A2执行,创建两个defer,添加到链表头部,
3 执行b2,执行后删除链表
4执行完b1,A2函数检测到下一个链表的defersp不等于自己的sp,A2退出,再次回到A的执行流程
func main() {
fmt.Println("user1",DeferFunc1(1))
fmt.Println("user2",DeferFunc2(1))
fmt.Println("user3",DeferFunc3(1))
}
func DeferFunc1(i int) (t int) {
t = i
defer func() {
fmt.Println("d1",t)
t += 3
}()
fmt.Println("r1",t)
return t
}
func DeferFunc2(i int) int {
t := i
defer func() {
fmt.Println("d2",t)
t += 3
}()
fmt.Println("r2",t)
return t
}
func DeferFunc3(i int) (t int) {
defer func() {
fmt.Println("d3",t)
t += i
}()
fmt.Println("r3",t)
return 2
}
执行顺序
r1 1
d1 1
user1 4
r2 1
d2 1
user2 1
r3 0
d3 2
user3 3
panic和defer一样,在每一个goroutine中包含panic字段,panic是一个链表,从头部插入
函数运行时先将A1,A2压入栈,执行到panic后面的代码就不会再执行,协程进入执行defer
panic触发defer执行时,先将defer结构体的started设置成true,panic设置成当前触发的panic,即panicA
按链表顺序先执行A2,修改struct的值,执行完A2将其从defer链表移除,继续顺着链表执行A1
1 执行A1,先将struct中started字段设置为true,panic字段设置为自己(panicA),执行函数A1遇到panicA1,后面的的代码不在执行
2将panicA1插入panic链表头,panicA1再去执行defer链表,发现A1已开始执行,并且 panic字段并不是自己,而是panicA(标defer已经被panicA执行过了,要将其移除)
3 panicA1根据记录的指针找到panicA,把他aborted标记为true,deferA1被释放
4,defer列表为空,执行panic输出,从后向前执行
panic结构体:
recover字段只把panic结构体的recover字段设置为true
1 发生panic后执行A2,发现recover,把panicA置为已恢复,recover的任务就完成了,程序继续执行
2 A2执行完,检查panic以恢复,从链表移除,deferA2移除,执行A1,程序结束
接着上面的例子,来看什么情况下panic不会被释放
1,当defer A2打印后又发生panic,先将panicA2插入panic列表头部,
2,再执行deferA2,发现A2已执行,将A2从列表移除,执行deferA1
3 A1执行完清除defer列表,打印panic信息
4 先打印painc A,打印时要打印recover信息,再打印panicA2,程序退出
1 函数A先注册一个panicA ,注册两个defer A1和A2,发生panic后调用gopanic ,将panicA加入panic链表,执行A2
2 函数A2注册defer B1插入链表头,注册panicA2插入链表头,开始执行deferB1
3 函数B1恢复了panic链表头部的panicA2,B1正常结束
恢复过程:
1 B1的recover恢复了panicA2,他要将panic2的recover字段置为true,然后跳出出并移除当前panic
1闭包函数内部可以操作外部定义的局部变量
2闭包离开上下文环境也可以执行
闭 包 捕 获 了 局 部 变 量 与 闭 包 函 数 生 产 了 f u n c c a l 结 构 体 , 存 储 在 栈 上 \color{#A0A}{闭包捕获了局部变量与闭包函数生产了funccal结构体,存储在栈上} 闭包捕获了局部变量与闭包函数生产了funccal结构体,存储在栈上
3闭包函数代码在编译阶段生成在堆上代码区,因为要捕获变量,闭包结构体在函数执行时生成
4捕获变量的值未被修改,捕获值拷贝,若被修改,则捕获变量的地址
go语言函数可以作为参数,返回值,也可以绑定到变量
这样的函数称为function value 它是一个结构体,里面存储的也是函数入口地址
函数运行是在堆上分配一个地址存储fn,指向函数入口
闭包捕获对外部变量是通过引用的方式实现的;会随着外部变量的改变而修改。为避免此问题可:
通过参数方式传入外部变量;
定义局部变量的方式;
func delayPrint() {
// 通过参数方式保证每个变量值是不同的;
for i := 0; i < 3; i++ {
go func(i int) {
time.Sleep(time.Second * 1)
fmt.Println("By param: ", i)
}(i)
}
time.Sleep(time.Second * 4)
// 直接引用外部变量,会发现所有调用最终都捕获了同一个变量值
for i := 0; i < 3; i++ {
go func() {
time.Sleep(time.Second * 1)
fmt.Println("By clouser: ", i)
}()
}
time.Sleep(time.Second * 4)
// 通过引入局部变量方式,保证捕获的变量是不同的
for i := 0; i < 3; i++ {
tmp := i
go func() {
time.Sleep(time.Second * 1)
fmt.Println("By tmp: ", tmp)
}()
}
time.Sleep(time.Second * 4)
}
// By param: 2
// By param: 0
// By param: 1
// By clouser: 3
// By clouser: 3
// By clouser: 3
// By tmp: 0
// By tmp: 2
// By tmp: 1
————————————————
捕获变量c对应两个地址
闭包函数对应一个地址
1
,
m
a
i
n
函
数
有
两
个
局
部
变
量
f
1
,
f
2
有
。
一
个
返
回
值
\color{#A0A}{1,main函数有两个局部变量f1,f2 有。一个返回值}
1,main函数有两个局部变量f1,f2有。一个返回值
2
,
c
r
e
a
t
e
函
数
有
一
个
局
部
变
量
c
=
2
\color{#A0A}{2,create函数有一个局部变量c=2}
2,create函数有一个局部变量c=2
3
,
函
数
运
行
时
在
堆
上
创
建
f
u
n
c
t
i
o
n
v
a
l
u
e
结
构
体
,
包
含
闭
包
入
口
地
址
和
闭
包
捕
获
变
量
c
\color{#A0A}{3,函数运行时在堆上创建function value结构体,包含闭包入口地址和闭包捕获变量c}
3,函数运行时在堆上创建functionvalue结构体,包含闭包入口地址和闭包捕获变量c
main的局部变量fs是长度为2的数组
有两个返回值
c
r
e
a
t
e
函
数
的
局
部
变
量
因
为
被
捕
获
所
以
在
堆
上
分
配
\color{#A0A}{create函数的局部变量因为被捕获所以在堆上分配}
create函数的局部变量因为被捕获所以在堆上分配
1两次循环闭包的捕获变量存储的都是地址,for执行完i的值是2,然后这个地址写入了返回值
2返回值拷贝给fs的时候,这个捕获变量i地址指向的值就是2
3调用fs[i]的时候,addr0,addr1一次写入寄存器,两个闭包捕获变量地址指向同一个位置,值是2
1main调用x时 x有一个返回值,返回值是funcval
2 func x返回值捕获了y,所以y是有捕获列表的funcval
3 因为捕获的变量y一开始定义了func,return时又做了修改,所以捕获了地址
4 当main调用x()返回y时,y保存在堆上,返回值 这里就变成了y的地址&y,这与func x()的定义不符,
5 这样编译器会在堆上保存y的副本y’,同时为x生成一个局部变量py‘记录y’的地址
6 这样只需要在return时将y’的值拷贝到返回值空间,y’和y都指向的是堆上的funcval fn
调用y指向的就是y’,捕获的变量也是y‘’,输出z
recover捕捉panic信息,panic之前压入栈的defer会执行,没有入栈的不执行
defer按先入后出执行 recover捕捉第一个panic
Context 的结构非常简单,它是一个接口。
timerctx,封装的timer和deadline的cancelctx,定时或在deadline时取消ctx
当然会涉及到节点管理
Context 提供跨越API的截止时间获取,取消信号,以及请求范围值的功能。//
它的这些方案在多个 goroutine 中使用是安全的
type Context interface {
// 如果设置了截止时间,这个方法ok会是true,并返回设置的截止时间
Deadline() (deadline time.Time, ok bool)
// 如果 Context 超时或者主动取消返回一个关闭的channel,如果返回的是nil,表示这个
// context 永远不会关闭,比如:Background()
Done() <-chan struct{}
// 返回发生的错误
Err() error
// 它的作用就是传值
Value(key interface{}) interface{}
}
写到这里,我们打住想一想,如果你来实现这样一个能力的 package,你抽象的接口是否也是具备这样四个能力?
获取截止时间
获取信号
获取信号产生的对应错误信息
传值专用