如何在 Go 中访问调用堆栈中的变量

How to access a variable up the callstack in Go

前言:我知道这是一个有问题的想法,但想象一下我控制调用堆栈的开始和结束但不控制一些中间函数的情况。

我正在寻找 "recover" 一个 context.Context ,它存在于调用堆栈的更上层,但在向下到达当前运行时位置的过程中被忽略了。有什么方法可以做到这一点?

我走了多远? 我可以使用以下代码在调用堆栈中获取任意帧:

pc := make([]uintptr, 15)
n := runtime.Callers(2, pc) // skip the Callers frame, and this current frame
frames := runtime.CallersFrames(pc[:n])
frame, _ := frames.Next()

这让我可以访问运行时 Frame (source), but nothing else. Access to its internal _func 可能有用,但这就是我被困的地方

警告!

这是可能的,稍后我会向您展示,但在您继续并获取所提供的解决方案之前,您应该大声疾呼并竭尽全力反对它。重构!永远不要在生产代码中这样做,它很脆弱并且不受 Go's compatibility promise!

的保护

现在,出于教育目的,让我们看看如何完成这样的事情。

你想要的基本上是访问一个 变量 ,它的声明既不是全局的(或者更准确地说是 Go 中的包级别)也不是函数的局部,但它在调用的某个地方更高堆。这称为动态作用域。下面的解决方案基于 Dave Cheney 在他的 Dynamically scoped variables 博客 post.

中提出的解决方案

为了演示,我们假设有一个 f1() 函数接收 ctx context.Context,它调用 f2() 而不传递上下文。我们想访问 f1f2() 中的上下文。像这样:

func f1(ctx context.Context) {
    // Does something with ctx
    f2()
}

func f2() {
    ctx := getCtx() // We desperately need context here!!!
    // Do something with ctx
}

最大的问题是:getCtx()如何实施?

首先让我们试试下面的程序:

func main() {
    f1(context.Background())
}

func f1(ctx context.Context) {
    fmt.Println(ctx)
    f2()
}

func f2() {
    panic("")
}

这将输出(在 Go Playground 试试):

context.Background
panic: 

goroutine 1 [running]:
main.f2(...)
    /tmp/sandbox870956446/prog.go:18
main.f1(0x4e7000, 0xc00002c008)
    /tmp/sandbox870956446/prog.go:14 +0x9b
main.main()
    /tmp/sandbox870956446/prog.go:9 +0x3

如果您查看堆栈跟踪,尤其是这一行:

main.f1(0x4e7000, 0xc00002c008)

它包含对 main.f1() 的函数调用及其参数! f1 有一个 context.Context 类型的参数,这是什么?

在 Go 内部接口由 2 个指针表示,一个指向其中存储的动态值的类型描述符,另一个指向其中存储的动态值。这些是您看到的 2 个指针。

想法是获取这 2 个指针,并从中构造一个 context.Context 接口值。怎么做?

只能通过导入包unsafe来完成(是的,使用它是不安全的!)。我们可以构造一个 [2]uintptr 类型的数组,将这 2 个值加载到其中,并将其转换为 context.Context ,如下所示:

idata := [2]uintptr{p1, p2}
*(*context.Context)(unsafe.Pointer(&idata))

事不宜迟,下面是 getCtx() 实施:

func getCtx() context.Context {
    var buf [8192]byte
    n := runtime.Stack(buf[:], false)
    sc := bufio.NewScanner(bytes.NewReader(buf[:n]))
    for sc.Scan() {
        var p1, p2 uintptr
        n, _ := fmt.Sscanf(sc.Text(), "main.f1(%v, %v", &p1, &p2)
        if n != 2 {
            continue
        }

        idata := [2]uintptr{p1, p2}
        return *(*context.Context)(unsafe.Pointer(&idata))
    }
    return nil
}

我们来构建一个快应用来测试它:

func main() {
    testContexts := []context.Context{
        context.Background(),
        context.TODO(),
    }
    for i, ctx := range testContexts {
        fmt.Printf("[%d] In main(): %p\n", i, ctx)
        f1(ctx)
    }
}

func f1(ctx context.Context) {
    fmt.Printf("In f1(): %p\n", ctx)
    f2()
}

func f2() {
    ctx := getCtx()
    fmt.Printf("In f2(): %p\n", ctx)
}

运行 它将输出(在 Go Playground 上尝试):

[0] In main(): 0xc00002c008
In f1(): 0xc00002c008
In f2(): 0xc00002c008
[1] In main(): 0xc00002c020
In f1(): 0xc00002c020
In f2(): 0xc00002c020

如您所见,我们可以获得在 f2() 中传递给 f1 的相同上下文,而无需将上下文传递给它。