Go Quiz: precautions for function naming and return value from the go interview question (more than 80% of people answered wrong)

subject

Chief engineer of Redhat and Maintainer of Prometheus open source project Bartłomiej Płotka A Go programming question was asked on Twitter, and more than 80% of the people answered it wrong.

The title is as follows. 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: Compilation error
  • D: Recursive stack overflow

You can think about the output of this code first.

analysis

Execute the return statement at the end of the function bbb, and the return value variable done will be assigned,

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

Note: closure func() {print ("BBB: surprise!"); Done ()} is not replaced by done, err: = the value of done in AAA ().

Therefore, after the function bbb is executed, one of the return values, done, actually becomes a recursive function. First, print "bbb: surprise!", Then call yourself again, and you will fall into infinite recursion until the stack overflows. So the answer to this question is D.

Then why is the closure func() {print ("bbb: surprise!") of the last return of the function bbb; Done ()} will not be replaced by done, err: = what about the value of done in AAA ()? If it is replaced, the answer to this question is B.

At this time, an old saying will be put forward:

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()
    // The following function calls will enter an endless loop and print test continuously
    done()
}

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

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

But in fact, the Go design allows the above code to execute normally, so the value of done in the closure of the last return of the function test will not be parsed in advance. After the test function is executed, it actually produces the following effect. What is returned is a recursive function, which is the same as the topic at the beginning of this article.

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

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

summary

This topic is actually tricky. In actual programming, it is very easy to make mistakes to avoid using this writing method for the named return value.

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

In addition, the author also gives the explanation shown below. The original address can be referred to explicate:

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 because of 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 is not a new variable, but the same variable as the return variable done of function bbb.

Here is an episode: I fed back this flaw to the original author, who agreed with me and deleted this explanation.


The latest version of the English version is explained as follows, and the original address can be referred to Revised interpretation.

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 the named return value. You can see what the output result of this problem is. You can send a message 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 source in GitHub: Go language elementary, intermediate and advanced tutorials.

The official account: coding is advanced. Pay attention to the official account and get the latest Go interview questions and technology stack.

Personal website: Jincheng's Blog.

Know: Wuji.

References

Keywords: Go Programming Back-end

Added by wmac on Sun, 20 Feb 2022 08:21:42 +0200