Golang Basics – Closure 

First of all, what is closure?

From the explanation on Wiki:

In programming languages, a closure, also lexical closure or function closure, is a technique for implementing lexically scoped name binding in a language with first-class functions. Operationally, a closure is a record storing a function[a] together with an environment.[1] The environment is a mapping associating each free variable of the function (variables that are used locally, but defined in an enclosing scope) with the value or reference to which the name was bound when the closure was created.[b] Unlike a plain function, a closure allows the function to access those captured variables through the closure’s copies of their values or references, even when the function is invoked outside their scope.

If you have no experience with closures, I believe this explanation is quite confusing. What does this “environment” mean, and what does it mean for a function and environment to be stored together?

Here we start with a practical demo code, and then give an example from a actual project to give you a glimpse.

Sample for explanation

We start by implementing a calculator that only does addition. Our requirement is: I need a calculation method that stores the existing sum, and each call will return the result of the existing sum plus the latest value we input. For example, if I use this calculator to add 10 for the first time, then I can get a return value of 10; if I add 20 for the second time, then I can get a return value of 30. Okay, the requirements are clear, let’s implement it.

package main

import "fmt"

// Create a function that returns another function

func adder() func(int) int {

    // This is what we call “environment” in the text, here it is a free variable, its lifecycle is not constrained by the function, that is, when the function returns, the sum is still in our stack.

    sum := 0

    // This is the closure we mention in the text

    return func(x int) int {

        // Here we use and modify the "environment" with a closure, and the environment can be unaffected by the function lifecycle, so we can use this feature to achieve the effect of accumulation

        sum += x

        return sum

    }

}

func main() {

    // Create an accumulator

    myAdder := adder()

    // Each call to myAdder adds the parameter to the total and returns the new total

    fmt.Println(myAdder(10))  // Output: 10

    fmt.Println(myAdder(20))  // Output: 30

    fmt.Println(myAdder(100)) // Output: 130

}

The core of this is actually whether you can understand why the “environment” sum always exists. In fact, when we define this accumulator myAdder, this sum variable is actually as long-lived as myAddr because myAddr holds this closure reference, that is, the return func(x int) int {…}, and this function holds the sum variable, so the lifecycle of the sum variable is tied to myAddr.

Conclusion

Once you understand this, the smart you must have thought, why go through all this trouble, can’t I just define a global variable?

Let’s use this example to answer that. Suppose we need to implement individual calculations for accumulation, subtraction, multiplication, division, squaring, etc., then how many global variables do we need to maintain? And for these global variables, they can actually be accessed globally, that is, if I want to, I can also access the global variables corresponding to accumulation, subtraction and division in the accumulator. I believe that by this point, you can already appreciate the benefits of closures, which is to package these variables, these contexts and specific function logic together, as its name suggests. This feature is what we often call good encapsulation, modular design concept, that is, the open-closed principle, single responsibility principle, high cohesion and low coupling.