头图

topic

Redhat's chief engineer, Prometheus open source project Maintainer Bartłomiej Płotka a Go programming question on Twitter, and more than 80% of the people answered it wrong.

The questions are as follows, and answer the output of the following program.

// named_return.go
package main

import "fmt"

func aaa() (done func(), err error) {
    return func() { print("aaa: done") }, nil
}

func bbb() (done func(), _ error) {
    done, err := aaa()
    return func() { print("bbb: surprise!"); done() }, err
}

func main() {
    done, _ := bbb()
    done()
}
  • A: bbb: surprise!
  • B: bbb: surprise!aaa: done
  • C: compile error
  • D: recursion stack overflow

You can first think about what the output of this code is.

Parse

When the return statement is executed at the end of the function bbb , the return value variable done will be assigned,

done := func() { print("bbb: surprise!"); done() }

Note : func() { print("bbb: surprise!"); done() } in the closure done is not replaced with the value of done, err := aaa() in done .

Therefore, after the function bbb executed, one of the return values, done , actually becomes a recursive function. It prints "bbb: surprise!" first, and then calls itself, which will fall into infinite recursion until the stack overflows. So the answer to this question is D .

Then why is the value of 0621717e315b61 in the closure func() { print("bbb: surprise!"); done() } by the done bbb replaced with the value of done, err := aaa() in done ? If replaced, the answer to this question is B .

At this time, it is time to move out an old saying:

This is a feature, not a bug

We can look at the following simpler example to help us understand:

// named_return1.go
package main

import "fmt"

func test() (done func()) {
    return func() { fmt.Println("test"); done() }
}

func main() {
    done := test()
    // 下面的函数调用会进入死循环,不断打印test
    done()
}

As the comments in the code above indicate, this program will also enter infinite recursion until the stack overflows.

If the function test closure last return of func() { fmt.Println("test"); done() } in the done be resolved in advance, because done is a function type, done zero value is nil , then closed the bag done value would be nil , execution nil function is will cause panic.

But in fact, the Go design allows the above code to be executed normally, so the value of done in the closure of the final return of the function test will not be parsed in advance. After the execution of the test function, the following effect is actually produced, and the return is A recursive function, the same as the title at the beginning of this article.

done := func() { fmt.Println("test"); done() }

Therefore, it will also enter infinite recursion until the stack overflows.

Summarize

This topic is actually very tricky. In actual programming, it is very error-prone to avoid using this way of writing named return values.

If you want to know more about the discussion of this topic by foreign Go developers, please refer to Go Named Return Parameters Discussion .

In addition, the author of the title also gave the following explanation. The original address can refer to detailed explanation of :

package main

func aaa() (done func(), err error) {
    return func() { print("aaa: done") }, nil
}

func bbb() (done func(), _ error) {
    // NOTE(bwplotka): Here is the problem. We already defined special "return argument" variable called "done".
    // By using `:=` and not `=` we define a totally new variable with the same name in
    // new, local function scope.
    done, err := aaa()

    // NOTE(bwplotka): In this closure (anonymous function), we might think we use `done` from the local scope,
    // but we don't! This is because Go "return" as a side effect ASSIGNS returned values to
    // our special "return arguments". If they are named, this means that after return we can refer
    // to those values with those names during any execution after the main body of function finishes
    // (e.g in defer or closures we created).
    //
    // What is happening here is that no matter what we do in the local "done" variable, the special "return named"
    // variable `done` will get assigned with whatever was returned. Which in bbb case is this closure with
    // "bbb:surprise" print. This means that anyone who runs this closure AFTER `return` did the assignment
    // will start infinite recursive execution.
    //
    // Note that it's a feature, not a bug. We use this often to capture
    // errors (e.g https://github.com/efficientgo/tools/blob/main/core/pkg/errcapture/doc.go)
    //
    // Go compiler actually detects that `done` variable defined above is NOT USED. But we also have `err`
    // variable which is actually used. This makes compiler to satisfy that unused variable check,
    // which is wrong in this context..
    return func() { print("bbb: surprise!"); done() }, err
}

func main() {
    done, _ := bbb()
    done()
}

However, this explanation is flawed, mainly this description:

By using := and not = we define a totally new variable with the same name in
new, local function scope.

For done, err := aaa() , the return variable done not a new variable, but the same variable as the return variable done of the function bbb .

Here is a small episode: I reported this flaw to the original author, and the original author agreed with my opinion and deleted this explanation .


The English explanation of the latest version is as follows, the original address can refer to the revised version of to explain .

package main

func aaa() (done func()) {
    return func() { print("aaa: done") }
}

func bbb() (done func()) {
    done = aaa()

    // NOTE(bwplotka): In this closure (anonymous function), we might think we use `done` value assigned to aaa(),
    // but we don't! This is because Go "return" as a side effect ASSIGNS returned values to
    // our special "return arguments". If they are named, this means that after return we can refer
    // to those values with those names during any execution after the main body of function finishes
    // (e.g in defer or closures we created).
    //
    // What is happening here is that no matter what we do with our "done" variable, the special "return named"
    // variable `done` will get assigned with whatever was returned when the function ends.
    // Which in bbb case is this closure with "bbb:surprise" print. This means that anyone who runs
    // this closure AFTER `return` did the assignment, will start infinite recursive execution.
    //
    // Note that it's a feature, not a bug. We use this often to capture
    // errors (e.g https://github.com/efficientgo/tools/blob/main/core/pkg/errcapture/doc.go)
    return func() { print("bbb: surprise!"); done() }
}

func main() {
    done := bbb()
    done()
}

thinking questions

The following code also uses named return values. You can see what the output of this question is. You can send a message to the WeChat public nrv to get the answer.

package main

func bar() (r int) {
    defer func() {
        r += 4
        if recover() != nil {
            r += 8
        }
    }()
    
    var f func()
    defer f()
    f = func() {
        r += 2
    }

    return 1
}

func main() {
    println(bar())
}

open source address

The article and sample code are open sourced on GitHub: Go language beginner, intermediate and advanced tutorial .

Official account: coding advanced. Follow the official account to get the latest Go interview questions and technology stacks.

Personal website: Jincheng's Blog .

Know: Wuji .

References


coding进阶
116 声望18 粉丝