defer
defer用于延迟函数的调用,每次defer都会把一个函数压入栈内,函数返回前再把延迟的函数取出并执行。
defer规则
defer的行为规则只有3条:
- 延迟函数的参数在defer语句出现时就已经确定。
如:
go
func a(){
i:=0
defer fmt.Println(i)
i++
return
}
最终打印0。
- 延迟函数执行按后进先出的顺序执行,即先出现的defer最后执行。
- 延迟函数可能操作主函数的具名返回值。
return不是一个原子的过程,函数返回过程:将值存入栈中作为返回值->执行defer的函数->执行跳转。
如:
go
//defer无影响,返回1
func f()int{
i:=0
defer func(){
i++
}()
return 1
}
//defer无影响,返回0
func f()int{
i:=0
defer func(){
i++
}()
return i //i的值已经拷贝给匿名返回值,defer再修改i不会影响匿名返回值
}
//defer修改了返回值,返回1
func f() (result int){
i:=0
defer func(){
result++
}()
result=i
return result
}
defer实现原理
<font style="color:rgb(0, 0, 0);background-color:rgba(0, 0, 0, 0.06);">src/runtime/runtime2.go</font>中定义了defer的数据结构:
go
type _defer struct{
sp uintptr //函数栈指针
pc uintptr //程序计数器
fn *funcval //函数地址
link *_defer //指向自身结构的指针,用于链接多个defer
}
每个goroutine数据结构中也有一个defer指针,该指针指向一个defer的单链表,每次声明一个defer就将defer插入到单链表表头,每次执行defer时就从单链表表头取出一个defer执行。

defer创建和执行
<font style="color:rgb(0, 0, 0);background-color:rgba(0, 0, 0, 0.06);">src/runtime/panic.go</font>定义了两个方法用于创建defer和执行defer
<font style="color:rgb(0, 0, 0);background-color:rgba(0, 0, 0, 0.06);">deferproc()</font>:声明defer处调用,将defer函数存入goroutine的链表中。<font style="color:rgb(0, 0, 0);background-color:rgba(0, 0, 0, 0.06);">deferreturn()</font>:在ret指令前调用,将defer从goroutine链表中取出并执行。
range
实现原理
在<font style="color:rgb(0, 0, 0);background-color:rgba(0, 0, 0, 0.06);">gofrontend/go/statements.cc/For_range_statement::do_lower()</font>方法的注释中,可以知道range实际上是一个c风格的循环结构。其支持数组、切片、map和channel类型,不同的类型有些细节上的差异。
range for slice
go
// The loop we generate:
for_temp:=range
len_temp:=len(for_temp)
for index_temp=0 ; index_temp<len_temp ; index_temp++ {
value_temp=for_temp[index_temp]
index=index_temp
value=value_temp
original body
}
遍历slice会先以slice的长度len_temp作为循环次数,循环体中,每次循环会先获取元素值,如果for-range中接收index和value,会对index和value进行一次赋值。
由于循环次数在循环之前就已经确定了,故在循环中向slice添加元素也不会影响循环次数。
数组的遍历过程和切片基本一致。
range for map
go
// The loop we generate:
var hiter map_iteration_struct
for mapiterinit(type,range,&hiter); hiter.key!= nil; mapiternext(&hiter) {
index_temp=*hiter.key
value_temp=*hiter.val
index=index_temp
value=value_temp
original body
}
func mapiterinit(t *maptype, h *hmap, it *hiter) {
...
// 对it进行了初始化
it.t = t
it.h = h
it.B = h.B
it.buckets = h.buckets
if t.bucket.kind&kindNoPointers != 0 {
h.createOverflow()
it.overflow = h.extra.overflow
it.oldoverflow = h.extra.oldoverflow
}
//生成了随机数
r := uintptr(fastrand())
if h.B > 31-bucketCntBits {
r += uintptr(fastrand()) << 31
}
// 用随机数r决定了从哪个桶开始遍历
it.startBucket = r & bucketMask(h.B)
it.offset = uint8(r >> h.B & (bucketCnt - 1))
it.bucket = it.startBucket
...
mapiternext(it)
}
遍历map没有指定循环次数,循环体和遍历slice差不多。
由于map底层使用hash表,在循环过程中插入的数据不一定能遍历到。
range for channel
go
// The loop we generate:
for {
value_temp,ok_temp=<-range
if !ok_temp{
break
}
value=value_temp
original body
}
channel遍历是依次从channel中读取数据,读取前不知道里面多少数据。如果channel没有数据,则阻塞等待,若channel已经关闭,则会退出循环。
select
Go在语言层面提供的IO多路复用的机制,检测多个channel是否可读或可写。
实现原理
case数据结构
<font style="color:rgb(0, 0, 0);background-color:rgba(0, 0, 0, 0.06);">src/runtime/select.go</font>中定义了表示case语句的数据结构:
go
type scase struct{
c *hchan //当前case语句操作的channel指针
kind uint16 //表示当前case的类型:读chan、写chan或default
elem unsafe.Pointer //缓冲区地址,如果是读chan,该字段表示读出chan的数据存放的地址
//如果是写chan,该字段表示写入chann的数据存放的地址
}
select实现逻辑
select选择case函数伪代码:
参数:
<font style="color:rgb(0, 0, 0);background-color:rgba(0, 0, 0, 0.06);">cas0</font>:scase数组,selectgo()从这些scase中找出一个返回<font style="color:rgb(0, 0, 0);background-color:rgba(0, 0, 0, 0.06);">order0</font>:两倍cas0数组长度的buffer,保存scase的随机序列pollorder和scase中channel地址序列lockorder。
pollorder:每次selectgo都会把scase序列打乱,以随机检测case
lockorder:所有case中的channel序列,以去重,防止对channel加锁时重复加锁
<font style="color:rgb(0, 0, 0);background-color:rgba(0, 0, 0, 0.06);">ncases</font>:scase数组的长度
返回值:
<font style="color:rgb(0, 0, 0);background-color:rgba(0, 0, 0, 0.06);">int</font>:选中的case的下标<font style="color:rgb(0, 0, 0);background-color:rgba(0, 0, 0, 0.06);">bool</font>:是否成功从channel读取到了数据
go
func selectgo(cas0 *scase,order0 *uint16,ncases int)(int ,bool){
1、锁定scase中所有channel
2、按照随机顺序检测scase中channel是否ready
有可读的就读取,解锁所有channel,返回(case index,true)
有可写的写入,解锁所有channel,返回(case index,false)
都没ready,解锁所有channel,返回(default index,false)
3、都没ready,也没default。将当前协程加入到所有channel的等待队列,阻塞当前协程等待被唤醒
4、唤醒后,解锁所有channel,返回channel对象的case index,和是否读取了数据
}