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,和是否读取了数据                  
}
相关推荐
Shingmc31 小时前
【Linux】线程互斥与同步
linux
翊谦8 小时前
Java Agent开发 Milvus 向量数据库安装
java·数据库·milvus
晓晓hh8 小时前
JavaSE学习——迭代器
java·开发语言·学习
iFlyCai8 小时前
C语言中的指针
c语言·数据结构·算法
Laurence8 小时前
C++ 引入第三方库(一):直接引入源文件
开发语言·c++·第三方库·添加·添加库·添加包·源文件
Vect__8 小时前
深刻理解进程、线程、程序
linux
查古穆8 小时前
栈-有效的括号
java·数据结构·算法
kyriewen118 小时前
你点的“刷新”是假刷新?前端路由的瞒天过海术
开发语言·前端·javascript·ecmascript·html5
Java面试题总结8 小时前
Spring - Bean 生命周期
java·spring·rpc
硅基诗人8 小时前
每日一道面试题 10:synchronized 与 ReentrantLock 的核心区别及生产环境如何选型?
java