go 函数和闭包

Created

2024-10-28 13:22:13

Updated

2024-10-28 13:22:35

1 函数名到底是个啥

main.go
package main

import (
    "fmt"
    "unsafe"
)

func add(a, b int) int {
    sum := 0
    sum = a + b
    return sum
}

func main() {
    println(add)
    addFuncVar := add
    println(addFuncVar)
    // 这里由于println() 和 fmt.Printf("%p") 打印 函数名, 处理上不同, 为了看函数名所代表直接内容
    addFuncVarVal := *(*uint64)(unsafe.Pointer(&addFuncVar))
    // 我们得到addFunc变量的值 与上面 println(add) 相同
    fmt.Printf("函数名所表示的值: 0x%x\n", addFuncVarVal)
    // 我们将这个值 作为内存地址, 将它转换为指针,找到它指向的内容
    addFuncCodeRealAddr := *(*uint64)(unsafe.Pointer(uintptr(addFuncVarVal)))
    fmt.Printf("把函数名表示的值作为地址,它指向的那块内存的内容 (即代码地址): 0x%x\n", addFuncCodeRealAddr)
    // fmt.Printf 打印 函数名 地址 结果是 真正代码的地址
    fmt.Printf("fmt.Printf(\"%%p\")打印的值: %p\n", add)
    add(1, 2)
    println(9999)
}
编译
go build -o main -gcflags "-N -l" main.go
./main  执行结果
0x10b1790
0x10b1790
函数名所表示的值: 0x10b1790
把函数名表示的值作为地址,它指向的那块内存的内容 (即代码地址): 0x108f600
fmt.Printf("%p")打印的值: 0x108f600
9999
dlv调试查看 add代码实际地址
# 断点打到 add(1,2) 那里
(dlv) s #进入函数add ,我们可以看到 PC的值, 这里是add代码指令的内存地址
> main.add() ./main.go:8 (PC: 0x108f600)
     3: import (
     4:         "fmt"
     5:         "unsafe"
     6: )
     7:
=>   8: func add(a, b int) int {
直接查看汇编代码来看add代码内存地址
go tool objdump -s "main.add" main
TEXT main.add(SB) main.go
  main.go:8             0x108f600               4883ec18                SUBQ $0x18, SP
  main.go:8             0x108f604               48896c2410              MOVQ BP, 0x10(SP)
  main.go:8             0x108f609               488d6c2410              LEAQ 0x10(SP), BP
  main.go:8             0x108f60e               4889442420              MOVQ AX, 0x20(SP)
  #....
你可以继续用代码获取 add 编译后的指令,与上面的 4883ec18 是否一致 (注意左边是低地址的内容)
    addFuncCode := *(*uint32)(unsafe.Pointer(uintptr(addFuncCodeRealAddr)))
    fmt.Printf("0x%x\n", addFuncCode)

上面的操作可以说用代码来实现获取”代码内容”

结论
  • 函数名是个二级指针!
  • 带着为什么函数名是个二级指针的疑问,我们继续看闭包

2 闭包

Caution

闭包(closure)是一个函数以及其捆绑的周边环境状态(lexical environment,词法环境)的引用的组合.

换而言之,闭包让开发者可以从内部函数访问外部函数的作用域

请看高亮的注释的思考
package main

// 外部函数
func outerFunc() func() int {
    var x int=1
    // 内部函数
    innerFunc := func() int {
        // x 属于 outerFunc的
        // 如果x在栈上,执行完 outerFunc ,x 那么作为它的局部变量会被释放,这肯定不行
        // 因为 我们这个 return的 innerFunc 还会操作x,所以逃逸到堆上
        x = x + 1
        return x
    }
    // 既然 innerFunc 会操作到x
    // 那么 innerFunc 这个东西所在的内存除了它自己函数的信息(函数体)外
    // 还必定含有x 这个数据的相关信息
    /*
    问: 为什么不直接将x 这个外部变量的信息编译到函数体内呢?
    答: 如果直接在函数体内, 那么堆上申请内存,初始化x 的操作都在函数内,
        然而这样你调用完函数后,没有任何东西再引用这个x了,x必定释放了,这与闭包本身的目的不一致了.
        而且你重新调用一次函数, 重新初始化了,不行
    */
    // 把x作为innerFunc的参数? 这不行,首先这不跟传参的一样吗, 还是值传递不成.
    // 值传递? 搞成传递指针 好像行得通? (这里只是一种思考)
    //   闭包的调用者堆上申请x的内存,(注:不同的闭包对外部变量的使用情况可能会有不同)
    //   然后在自己的栈帧上存地址,将地址传递给闭包函数

    return innerFunc
}

func main() {
    // 创建一个闭包函数
    closure := outerFunc()

    // 调用闭包函数
    closure()
}

通过汇编来看看实际的情况

go build -o main -gcflags "-N -l -S" main.go

会看到当前这个闭包的结构

runtime.newobject
LEAQ    type:noalg.struct { F uintptr; main.x *int }(SB), AX
代码验证结构
package main

import (
    "unsafe"
)

func outerFunc() func() int {
    var x int = 1
    innerFunc := func() int {
        x = x + 1
        return x
    }
    return innerFunc
}

//go:linkname inheap runtime.inheap
func inheap(arg uintptr) bool

func inHeapOrStack(b uintptr) bool
func main() {
    type closureStruct struct {
        F uintptr // 闭包的函数的地址
        x *int    // 闭包的外部变量 这里是指针
    }
    // 创建一个闭包函数
    closure := outerFunc()
    closureVal := *(*uint64)(unsafe.Pointer(&closure))
    println(inheap(uintptr(closureVal))) //堆上
    println(closure, closureVal)         // 0xc000090000
    closureCodeRealAddrAndVar := *(*closureStruct)(unsafe.Pointer(uintptr(closureVal)))
    println(*closureCodeRealAddrAndVar.x) // 1
    println(closureCodeRealAddrAndVar.x)  // 0xc00008e000  堆上
    println(closureCodeRealAddrAndVar.F)  // 闭包函数的地址 0x1057820  只读
    // 调用闭包函数
    println(closure()) // 2
    println(closure()) // 3

    closure2 := outerFunc()
    closureVal2 := *(*uint64)(unsafe.Pointer(&closure2))
    closureCodeRealAddrAndVar2 := *(*closureStruct)(unsafe.Pointer(uintptr(closureVal2)))
    // 函数地址一样
    println(closureCodeRealAddrAndVar2.F == closureCodeRealAddrAndVar.F) // true

}
Diagram

我们看看下面这个例子的结构

验证一下闭包结构
package main

import (
    "fmt"
    "unsafe"
)

func outerFunc(x int) func() int {
    innerFunc := func() int {
        return x
    }
    return innerFunc
}

func main() {
    // 创建一个闭包函数
    closure := outerFunc(10)
    // 这个是针对 上面的闭包结构
    type closureStruct struct {
        F uintptr
        x int //直接存就行了. 因为没做修改等操作,就打印的需求
    }
    closureVal := *(*uint64)(unsafe.Pointer(&closure))
    println(closureVal)
    closureCodeRealAddrAndVar := *(*closureStruct)(unsafe.Pointer(uintptr(closureVal)))
    println(closureCodeRealAddrAndVar.x) //10
    println(closureCodeRealAddrAndVar.F) //闭包函数的地址
    // 调用闭包函数
    closure()

}

3 println & fmt.Print

Tip

todo

Back to top