暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

【Golang】图解Function value

幼麟实验室 2020-06-22
1318


 你知道把一个函数赋给一个变量,这个变量是什么结构吗?
 你知道闭包长什么样子吗?




01


函数




函数,在GO语言中属于头等对象,可以被当作参数传递、也可以作为函数返回值、绑定到变量。Go语言称这样的参数、返回值和变量为Function Value”
Function Value本质上是一个指针,却不直接指向函数指令入口,而是指向runtime.funcval结构体。
    type funcval struct {
    fn uintptr
    }
    这个结构体从定义上看只有一个地址,这个地址才是函数的指令入口。所以,一个Function Value是以下图所示形式存在的。


    图:一个Function Value的存在形式






    02


    闭包




    在计算机科学中,闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是在支持头等函数的编程语言中实现词法绑定的一种技术。闭包在实现上是一个结构体,它存储了一个函数(通常是其入口地址)和一个关联的环境(相当于一个符号查找表)。环境里是若干对符号和值的对应关系,它既要包括约束变量(该函数内部绑定的符号),也要包括自由变量(在函数外部定义但在函数内被引用),有些函数也可能没有自由变量。闭包跟函数最大的不同在于,当捕捉闭包的时候,它的自由变量会在捕捉时被确定,这样即便脱离了捕捉时的上下文,它也能照常运行。
    维基百科


    所以像下面这个例子:

      func create() func(){
      c := 2
      return func(){
      fmt.Println(c)
          }
      }
      func main(){
          f1 := create()
          f2 := create()
          f1()
          f2()
      }

      create函数的返回值是一个函数,并且引用了其外层函数定义的局部变量c;而且,即便create函数结束,依然可以通过f1和f2正常执行这个函数并使用定义在create内部的变量c。所以这个返回值符合闭包的定义,而这个自由变量c,通常被称为“捕获变量”。

      虽然create函数的返回值函数形成闭包,但是Go语言里并没有把闭包从Function Value中特别区分出来。在Go语言中闭包只是拥有一个或多个捕获变量的Function Value而已。这些捕获变量就是它的捕获列表,就放在对应的funcval结构体的后面。所以上例中,f1和f2的内存布局如下图所示。

      图:示例闭包对象的结构

      每个闭包对象都是一个Function Value,但是各自持有自己的捕获列表,这也是称闭包为有状态的函数”的原因。





      03


      调用




      通过FunctionValue调用函数时,会把对应的funcval结构体地址存入特定寄存器,例如amd64平台使用的是DX寄存器。
      继续使用闭包的示例,通过f1调用闭包函数时,会把f1存储的funcval结构体地址存入寄存器DX,这样在闭包函数的指令中就可以通过这个寄存器存储的地址加上8字节的偏移,就找到f1的捕获变量了。

      图:Function Value 的调用

      同样的,通过f2调用闭包函数时,会把f2存储的funcval结构体地址存入寄存器,闭包函数执行时找到的就是f2的捕获变量了。

      图:Function Value 的调用

      如果是没有捕获列表的Function Value,直接忽略这个寄存器即可。通过这样的方式,Go语言实现了对Function Value的统一调用。对Function Value有了大致了解,就可以关注一些细节了。




      04


      静态分配




      对于没有捕获列表的Function Value,如果多个变量关联到同一个函数,编译器会做出优化,让它们共用一个funcval结构体。
         func A(i int) {
            i++
            fmt.Println(i)
        }  
        func B(){
            f1 := A
            f1(1)
        }
        func C(){
            f2 := A
            f2(1)
        }
           
        像上面这种情况,编译阶段会创建一个funcval结构体放到只读数据段,而执行阶段,f1和f2都会使用它。

        图:funcval静态分配





        05


        捕获列表




        因为捕获列表需要由闭包对象各自持有,所以有捕获列表的Function Value要到执行阶段才会在堆上分配对应的funcval结构体以及捕获列表空间。但是,捕获列表里存什么?直接拷贝捕获变量值吗?才没有那么简单。

        闭包捕获的变量要在闭包函数和外层函数中表现一致,如果单纯值拷贝,就无法保证这一点。所以编译器针对捕获变量的不同情况分别做出了不同的处理。

        • 捕获变量除了初始化赋值外在任何地方都没有被修改过,那就可以直接拷贝值,因为它不会再变化。

        • 捕获变量除了初始化赋值外,还被修改过,就要再细分了。


        case [01]   捕获局部变量
            

          func create() (fs [2]func()){
              for i := 0; i < 2; i++ {
                  fs[i] = func(){
                      fmt.Println(i)
                  }    
              }
              return
          }
          func main() {
              fs := create()
              for i := 0; i < len(fs); i++ {
                  fs[i]()
              }
          }
           
          这个例子中,被捕获的是局部变量i,并且除了初始化赋值外还被修改过,所以局部变量i改为堆分配,栈上只存一个地址。我们把闭包函数指令入口地址记为addrf。


          main函数栈帧中,局部变量fs,以及create函数的返回值都是长度为2的Function Value型数组。在create函数栈帧中,局部变量i被闭包捕获,分配到了堆上,在栈上的局部变量空间只存储它在堆上的地址。在for循环执行以前,i等于0。

          第一次for循环


          在堆上创建一个funcval结构体,指向闭包函数入口,并且在捕获列表中存储捕获变量i的地址,这样就可以和外层函数使用同一个变量了。返回值数组第一个元素存储这一次创建的funcval结构体地址,第一次for循环结束后,i自增1。

          第二次for循环


          第二次for循环再次创建一个funcval结构体,存储捕获变量i的地址,而funcval结构体本身的地址作为返回值的第二个元素。第二次循环结束,i再次自增1。


          在这个示例中,为了让被捕获的局部变量在闭包函数和外层函数中保持一致,本该在栈上分配的局部变量被分配到堆上,这其实也是“变量逃逸”的一种场景。

          case [02]   捕获参数

          如果是参数被捕获,那么调用者依然从栈上传递参数,但是被调用函数会把它拷贝到堆上一份,然后和闭包函数都使用堆上分配的那一个。



          case [03]   捕获返回值

          如果是返回值被捕获,那么处理方式就又有些不同了。返回值空间依然由调用者在栈上分配,但是被调用函数(闭包的外层函数)会在堆上也分配一个,并且与闭包函数都使用堆上这一个。但是,在外层函数返回前要把堆上的返回值拷贝到栈上那一个。





          06


          关键点



          (1)Go语言里Function Value本质上是指向funcval结构体的指针;
          (2)Go语言里闭包只是拥有捕获列表的Function Value;
          (3)捕获变量在外层函数与闭包函数中要保持一致。





          文章转载自幼麟实验室,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

          评论