Golang--多种控制结构详解

defer

defer用于延迟函数的调用,每次defer都会把一个函数压入栈内,函数返回前再把延迟的函数取出并执行。

defer规则

defer的行为规则只有3条:

  1. 延迟函数的参数在defer语句出现时就已经确定。

如:

go 复制代码
func a(){
    i:=0
    defer fmt.Println(i)
    i++
    return
}

最终打印0。

  1. 延迟函数执行按后进先出的顺序执行,即先出现的defer最后执行。
  2. 延迟函数可能操作主函数的具名返回值。

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,和是否读取了数据                  
}
相关推荐
indexsunny1 小时前
互联网大厂Java面试实战:微服务与Spring Boot在电商场景下的应用解析
java·spring boot·redis·docker·微服务·kubernetes·oauth2
薛定谔之死猫1 小时前
Ruby简单粗暴把图片合成PDF文档
java·pdf·ruby
moxiaoran57531 小时前
Spring Bean线程安全性分析
java·spring
小鸡脚来咯2 小时前
正则表达式考点
java·开发语言·前端
Cg136269159742 小时前
JS-对象-
开发语言·javascript·ecmascript
IT_陈寒2 小时前
SpringBoot开发效率提升50%的5个隐藏技巧,官方文档都没告诉你!
前端·人工智能·后端
芒果披萨2 小时前
Shell脚本基础编程
linux·运维·服务器
liuyao_xianhui2 小时前
递归_反转链表_C++
java·开发语言·数据结构·c++·算法·链表·动态规划