Go interview topic: talk about closure understanding in go language

Keywords: Go Interview

Hello, I'm brother Xiao Dao.

Today's interview topic is: closure.

definition

Closure is defined in computer science as a function that references internal variables of a function.

After reading the definition, I fell into a deep thought... Indeed, if I hadn't touched closures before or didn't understand closures, this definition is really a little annoying.

Let's look at a few examples first. After understanding the practical application of closures, we can understand this definition, so it won't be so obscure.

Example

Go language implements closures through anonymous functions.

func increase() func(int) int {
	sum := 0
	return func(i int) int {
		sum += i
		return sum
	}
}

func main() {
	incr := increase()
	fmt.Println(incr(1))
	fmt.Println(incr(2))
}

Output results:

$go run main.go
1
3

It is not difficult to see that the sum variable is not destroyed after the increase function is executed, but is always kept in memory. Next, we can check whether the variable escapes through the relevant commands in go.

$go build -gcflags=-m main.go
# command-line-arguments
./main.go:112:9: can inline increase.func1
./main.go:106:13: inlining call to fmt.Println
./main.go:107:13: inlining call to fmt.Println
./main.go:111:2: moved to heap: sum
./main.go:112:9: func literal escapes to heap
./main.go:106:18: incr(1) escapes to heap
./main.go:106:13: []interface {}{...} does not escape
./main.go:107:18: incr(2) escapes to heap
./main.go:107:13: []interface {}{...} does not escape
<autogenerated>:1: .this does not escape

It can be found that the sum variable has escaped from memory and escaped from the internal variable of the increase() function to the heap, ensuring that it will always remain in memory after leaving the scope of the increase() function.

Look at the definition

Let's combine the definition below: for functions that reference internal variables of functions, disassemble the above example:

//Function internals refer to: inside the increase() function
//Function internal variable: sum variable
//Functions are:
//func(i int)int{ 
//    sum += i
//    return sum
//}
The language of the example is:
stay increase()A reference is defined inside the function increase()Function inside sum Anonymous function of variable func(i int)int{}[It's a bit of a doll. Taoist friends read it several times and try to figure it out. Its meaning is self-evident]

From the above example, we can see the two core functions of closures:

  • It is possible to access function internal variables outside the function
  • Function internal variables are always kept in memory without being destroyed after they leave their scope

Closure application in other scenarios

After clarifying the definition, in order to deepen our understanding of closures, let's look at two more examples of closures

defer deferred calls and closures

Defer calls will be executed before the end of the current function execution. These calls are called deferred calls. The anonymous function used in defer is also a closure.

func func1() {
	a := 1
	defer func(r int) {
		fmt.Println(r)
	}(a)
	a = a + 100
	fmt.Println(a)
}

func func2() {
	a := 1
	defer func() {
		fmt.Println(a)
	}()
	a = a + 100
	fmt.Println(a)
}

func1 output result:

$go run main.go
101
1

func2 output results

$go run main.go
101
101

The difference between the two functions is that when defer in func1 is defined, a=1 is assigned to defer. When executing the defer function, a is the copy of a at the time of definition, not the a value in the current environment variable, that is, defer executes:

func (r int) {
  fmt.Println(r)
}(1)

In func2, the defer definition does not complete any assignment actions. It only registers the functions that are called after execution, and the a variables used are variables in the current environment.

goroutine and closure

func func2() {
	for i := 0; i < 5; i++ {
		go func() {
			fmt.Println(i)
		}()
	}
}

For the above function output, many people will mistakenly think it is 0, 1, 2, 3, 4, 5. But in fact, the results of multiple runs are inconsistent:

$go run main.go
5 3 5 5 5
//or
5 5 5 5 5
//or
3 3 5 5 5

This is because the timing of go scheduling is uncertain. It may occur at any time point when i is equal to 0-5. The output result depends on the value of external i when this goroutine is executed.

What happens if we add sleep to goroutine?

func func2() {
	for i := 0; i < 5; i++ {
		go func() {
			time.Sleep(time.Second)
			fmt.Println(i)
		}()
	}
}

Output results:

$go run main.go
5 5 5 5 5

This is because after adding sleep, we ensure that the execution of the external loop is completed. At this time, i=5. Then, when goroutine is executed, the output result is also 5.

Summary of interview points

  • Talk about your understanding of closures. PS: in the investigation of closures, you will often give several examples similar to those mentioned in this article, and then let you say the output results and reasons.

Posted by tdors on Sat, 06 Nov 2021 03:14:54 -0700