Go Closures and Higher-Order Functions Work Just Like JavaScript
Frontend to Go Full-Stack (Part 5): Finally Met an Old Friend — Go's Closures and Higher-Order Functions Are So Similar to JavaScript
This is the fifth article in the "Frontend to Go Full-Stack" series.
The previous article covered named return values, naked
return, anddefer, and verified through code experiments the execution order of multipledefers, the timing of parameter evaluation, and the impact ofdeferon named return values.This article continues learning about Go functions, entering a domain very familiar to frontend developers: anonymous functions, closures, and higher-order functions.
Foreword: After Learning Go for So Long, Finally Encountered Something Familiar
After learning the previous few articles, my biggest feeling about Go is:
There are many familiar concepts, but the syntax and way of thinking are often different from JavaScript.
For example, variables:
name := "Starry"
For example, error handling:
result, err := doSomething()
if err != nil {
return err
}
And the defer just learned in the last article:
defer file.Close()
Although I can gradually understand these things, for a frontend developer who has been using JavaScript and TypeScript for a long time, it still feels a bit like "entering someone else's home turf."
Until this stage, when I started learning anonymous functions and closures.
Seeing code like this:
add := func(a, b int) int {
return a + b
}
My first reaction was finally not:
What kind of Go-specific syntax is this again?
But rather:
I know this well.
If converted to JavaScript:
const add = (a, b) => a + b;
Apart from the syntax looking different, the underlying idea is almost exactly the same.
Continuing to learn about closures, returning functions, and functions as arguments, I even started to have an illusion:
Did I temporarily switch from a Go tutorial back to frontend?
This article records this rare "familiar" learning process.
Functions Can Also Be Saved to Variables
Previously, when learning functions, they were always defined like this:
func add(a, b int) int {
return a + b
}
And then called:
result := add(10, 20)
This is certainly fine.
But Go functions can also have no name.
I wrote code like this for the first time:
package main
import "fmt"
func main() {
add := func(a, b int) int {
return a + b
}
result := add(10, 20)
fmt.Println("Calculation result:", result)
}
Run:
go run .
Output:
Calculation result: 30
The key point is here:
add := func(a, b int) int {
return a + b
}
The right side creates a function without a name:
func(a, b int) int {
return a + b
}
Then this function value is saved to the variable:
add
After that, you can call it through the variable just like calling a regular function:
add(10, 20)
What is created here is an anonymous function.
Isn't This Just a JavaScript Function Expression?
If converted to JavaScript, it can be written as a function expression:
const add = function (a, b) {
return a + b;
};
Or using the arrow function more common in modern frontend development:
const add = (a, b) => {
return a + b;
};
Even shorter:
const add = (a, b) => a + b;
Putting Go and JavaScript side by side for comparison:
javascript
const add = (a, b) => a + b;
add := func(a, b int) int {
return a + b
}
Although Go doesn't have shorthand like arrow functions, the core idea is the same:
A function is also a value
→ Can be created
→ Can be saved to a variable
→ Can be called through the variable
For frontend developers, this has almost no learning curve.
It's important to note:
An anonymous function is just a "function without a name," it doesn't mean all anonymous functions are closures.
Only when a function references variables from its outer scope does it begin to exhibit closure characteristics.
Anonymous Functions Can Also Be Executed Immediately
Since functions can be created directly, can they be executed immediately after creation?
Yes.
package main
import "fmt"
func main() {
func() {
fmt.Println("This anonymous function executed immediately")
}()
}
The key is at the end:
}()
The preceding part:
func() {
fmt.Println("This anonymous function executed immediately")
}
Just defines an anonymous function.
The final:
()
Is what actually calls it.
Execution result:
This anonymous function executed immediately
Seeing this, frontend developers should feel very familiar again.
In JavaScript, you often see IIFE, Immediately Invoked Function Expression:
(function () {
console.log("Executed immediately");
})();
Using arrow functions can also be written as:
(() => {
console.log("Executed immediately");
})();
Go just changes it to:
func() {
fmt.Println("Executed immediately")
}()
The overall idea is not much different:
Define function
→ Don't save to variable
→ Call immediately
Immediately Executed Functions Can Also Receive Arguments
I also tried passing arguments to an immediately executed anonymous function:
package main
import "fmt"
func main() {
func(name string) {
fmt.Println("Hello:", name)
}("Starry")
}
Output:
Hello: Starry
The function definition here is:
func(name string) {
fmt.Println("Hello:", name)
}
Passed in when calling:
("Starry")
If using JavaScript, it can be written as:
javascript
((name) => {
console.log("Hello:", name);
})("Starry");
The idea is basically the same.
This stage started to make me clearly feel:
Although Go and JavaScript are two completely different languages, many programming concepts are actually common.
What really needs to be relearned is often just how the language expresses these concepts.
Closures: Functions Can Access and Modify Outer Variables
Next, start learning closures.
First piece of code:
package main
import "fmt"
func main() {
count := 0
increase := func() {
count++
fmt.Println("Inside function count:", count)
}
increase()
increase()
increase()
fmt.Println("Final count:", count)
}
Execution result:
Inside function count: 1
Inside function count: 2
Inside function count: 3
Final count: 3
There is a key point here:
count := 0
Is declared outside the anonymous function.
But the anonymous function below can not only access count, but also directly modify it:
increase := func() {
count++
}
After calling three times:
0
→ 1
→ 2
→ 3
The anonymous function here references count from the outer scope, thus forming a closure.
For Frontend Developers, Closures Are Too Familiar
Converted to JavaScript:
let count = 0;
const increase = () => {
count++;
console.log(count);
};
increase();
increase();
increase();
Will also get:
1
2
3
So this part can almost directly reuse the previous understanding of JavaScript closures:
A function can access variables in the lexical scope where it was defined.
Go:
count := 0
increase := func() {
count++
}
JavaScript:
let count = 0;
const increase = () => {
count++;
};
This sense of familiarity is a completely different experience from learning defer earlier.
However, two concepts need to be distinguished here.
Anonymous Function
add := func(a, b int) int {
return a + b
}
This function has no name, so it is an anonymous function.
But it does not access any external variables and does not need to depend on outer state.
Closure
count := 0
increase := func() {
count++
}
This function references and modifies the outer count, thus forming a closure.
It can be simply understood as:
Anonymous functions focus on: whether the function has a name
Closures focus on: whether the function references variables in the outer scope
Anonymous functions are often used to create closures, but the two are not exactly the same concept.
This Still Doesn't Fully Reflect the Value of Closures
The previous count existed directly in main:
count := 0
If it's just like this, it seems hard to see what's special about closures.
So, I continued to write a counter:
package main
import "fmt"
func createCounter() func() int {
count := 0
return func() int {
count++
return count
}
}
func main() {
counter := createCounter()
fmt.Println(counter())
fmt.Println(counter())
fmt.Println(counter())
}
Execution result:
1
2
3
This code starts to get interesting.
How Exactly to Read func createCounter() func() int?
Seeing the following function declaration for the first time:
func createCounter() func() int
It's indeed a bit like a nesting doll.
It can be understood by breaking it down.
The front part:
func createCounter()
Means:
Define a function named
createCounterthat takes no arguments.
The back part:
func() int
Is the return value type of createCounter.
It means:
Returns a function that takes no arguments and has a return type of
int.
So the complete function declaration:
func createCounter() func() int
Can be read as:
createCounteris a function that returns another "function that takes no arguments and returns anint".
If converted to TypeScript, it's roughly:
function createCounter(): () => number
With this comparison, it immediately looks smooth.
Go:
func createCounter() func() int
TypeScript:
function createCounter(): () => number
Both essentially describe:
A function returns another function.
createCounter Has Finished, Why Is count Still Alive?
What really made me find closures interesting is here.
createCounter declares a local variable internally:
count := 0
According to the previous understanding of local variables, after the function execution ends:
createCounter()
The local variables inside should also no longer be accessible.
But, createCounter returns the following anonymous function:
return func() int {
count++
return count
}
This anonymous function still references count.
Thus:
counter := createCounter()
What is obtained is not just a piece of function logic that "increments a number".
It also retains access to count.
First call:
counter()
Gets:
1
Second call gets:
2
Third call gets:
3
That is to say, this function "remembers" the previous state.
It can be temporarily imagined as:
counter
├── Execution logic: count++
└── Persistently referenced state: count
This is a very typical use of closures:
Binding a piece of function logic together with the state it needs to continuously access.
As for where count is ultimately stored and how the compiler specifically handles the lifecycle of variables, this stage won't go deep for now.
Now just need to know:
As long as the returned function still references
count, subsequent calls to this function can still read and modify this state.
The JavaScript Version Is Almost Identical
Go:
func createCounter() func() int {
count := 0
return func() int {
count++
return count
}
}
JavaScript:
function createCounter() {
let count = 0;
return function () {
count++;
return count;
};
}
Or using arrow functions:
const createCounter = () => {
let count = 0;
return () => {
count++;
return count;
};
};
Calling:
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
At this point, for frontend developers, Go closures are basically no longer unfamiliar.
Although the syntax is different, the execution logic is almost identical:
Call outer function
→ Create a piece of local state
→ Return a function referencing that state
→ Subsequent calls to the returned function
→ Continuously read and modify this state
Will Two Counters Share the Same count?
Next, I did another very important experiment:
package main
import "fmt"
func createCounter() func() int {
count := 0
return func() int {
count++
return count
}
}
func main() {
counterA := createCounter()
counterB := createCounter()
fmt.Println("A:", counterA())
fmt.Println("A:", counterA())
fmt.Println("B:", counterB())
fmt.Println("B:", counterB())
fmt.Println("A:", counterA())
}
Final execution result:
A: 1
A: 2
B: 1
B: 2
A: 3
This point is very important.
If counterA and counterB shared the same count, the result should be:
A: 1
A: 2
B: 3
B: 4
A: 5
But the actual result is not like this.
The reason is:
counterA := createCounter()
Executed createCounter() once, producing a new:
count := 0
Then:
counterB := createCounter()
Executed createCounter() again, producing another:
count := 0
So they can be understood as:
counterA
└── References its own copy of count
counterB
└── References its own copy of count
The two calls to createCounter created independent states, so they do not affect each other.
This Reminds Me of "State Factories" in Frontend
The same is true in JavaScript:
const counterA = createCounter();
const counterB = createCounter();
counterA(); // 1
counterA(); // 2
counterB(); // 1
counterB(); // 2
counterA(); // 3
If you have written Vue Composition API, this idea of "each function call creates an independent state" is also very familiar.
For example:
function useCounter() {
let count = 0;
return {
increase() {
count++;
},
getCount() {
return count;
},
};
}
Each call:
const a = useCounter();
const b = useCounter();
Can have its own copy of state.
Of course, Vue's reactivity system and Go closures are not the same thing.
This is just an analogy from the following perspective:
Call a function
→ Create a new internal state
→ Return functions that can operate on this state
→ States between different calls are independent of each other
This idea is very common in JavaScript and can also be implemented through closures in Go.
Functions Can Not Only Be Returned, But Also Passed as Arguments
Since functions are also values, the next question naturally arises:
Can functions be passed as arguments to another function?
Yes.
So I wrote the following code:
package main
import "fmt"
func calculate(a, b int, operation func(int, int) int) int {
return operation(a, b)
}
func add(a, b int) int {
return a + b
}
func multiply(a, b int) int {
return a * b
}
func main() {
addResult := calculate(10, 20, add)
multiplyResult := calculate(10, 20, multiply)
fmt.Println("Addition result:", addResult)
fmt.Println("Multiplication result:", multiplyResult)
}
Execution result:
Addition result: 30
Multiplication result: 200
Focus on this:
operation func(int, int) int
operation is a function parameter.
It requires the passed-in function to satisfy the following type:
func(int, int) int
That is:
Receives two ints
Returns one int
So:
func add(a, b int) int
Meets the requirement.
The following function:
func multiply(a, b int) int
Also meets the requirement.
Thus you can write:
calculate(10, 20, add)
Or:
calculate(10, 20, multiply)
calculate doesn't care whether the specific operation is addition or multiplication.
It is only responsible for:
Receiving two numbers
→ Receiving a piece of calculation logic that meets the requirements
→ Calling this logic
→ Returning the calculation result
The Signature of the Passed-in Function Must Match
calculate requires the type of operation to be:
func(int, int) int
This means the passed-in function must satisfy:
Same number of parameters
Same parameter types
Same number of return values
Same return value types
For example, the following function cannot be directly passed in:
func greet(name string) string {
return "Hello, " + name
}
Because its function type is:
func(string) string
But calculate needs:
func(int, int) int
These two are not the same function type.
So Go not only allows "passing functions in", but also checks at compile time:
Whether the passed-in function conforms to the function signature required by the parameter.
Frontend Developers Should Understand Callbacks
This is very close to callback functions in JavaScript:
function calculate(a, b, operation) {
return operation(a, b);
}
function add(a, b) {
return a + b;
}
function multiply(a, b) {
return a * b;
}
calculate(10, 20, add);
calculate(10, 20, multiply);
TypeScript's syntax is even more similar to Go:
function calculate(
a: number,
b: number,
operation: (a: number, b: number) => number
): number {
return operation(a, b);
}
Go:
func calculate(
a, b int,
operation func(int, int) int,
) int {
return operation(a, b)
}
Compare the types of the function parameters.
TypeScript:
operation: (a: number, b: number) => number
Go:
operation func(int, int) int
Both are describing:
operationmust be a function that receives two numbers and returns a number.
You Can Even Pass an Anonymous Function Directly
It's not necessary to define in advance:
func add(a, b int) int
You can also pass an anonymous function directly when calling calculate:
package main
import "fmt"
func calculate(a, b int, operation func(int, int) int) int {
return operation(a, b)
}
func main() {
result := calculate(10, 20, func(a, b int) int {
return a - b
})
fmt.Println("Calculation result:", result)
}
Execution result:
Calculation result: -10
If converted to frontend code:
calculate(10, 20, (a, b) => {
return a - b;
});
Go:
calculate(10, 20, func(a, b int) int {
return a - b
})
Writing this, it's already very similar to code commonly used in JavaScript:
array.map((item) => ...)
array.filter((item) => ...)
array.find((item) => ...)
button.addEventListener("click", () => ...)
The common idea behind them is:
Take a piece of function logic
→ As an argument
→ Hand it to another function
→ To be called by the other function at the appropriate time
Functions as Arguments Are Not Necessarily Closures
Here again, several concepts need to be distinguished.
The following add is a regular function:
func add(a, b int) int {
return a + b
}
Passing it to calculate:
calculate(10, 20, add)
Here, the ability of "function as argument" is used, but add does not reference outer variables, so it is not a closure.
And the following anonymous function:
offset := 10
operation := func(a, b int) int {
return a + b + offset
}
References the outer variable offset, so it forms a closure.
If it is passed in:
result := calculate(10, 20, operation)
Here, the following are used simultaneously:
Anonymous function
Closure
Function as argument
These concepts often appear together, but they focus on different questions:
Anonymous function
→ Does the function have a name?
Closure
→ Does the function reference variables in the outer scope?
Function parameter
→ Can a function be passed as a value to another function?
Higher-order function
→ Does a function receive a function or return a function?
I Even Wrote a Function Similar to filter Myself
Learning up to here, I conveniently tried to simulate the very familiar frontend filter.
JavaScript:
const numbers = [1, 2, 3, 4, 5, 6];
const evenNumbers = numbers.filter((number) => {
return number % 2 === 0;
});
console.log(evenNumbers);
Result:
[2, 4, 6]
Go version:
package main
import "fmt"
func filter(numbers []int, condition func(int) bool) []int {
result := []int{}
for _, number := range numbers {
if condition(number) {
result = append(result, number)
}
}
return result
}
func main() {
numbers := []int{1, 2, 3, 4, 5, 6}
evenNumbers := filter(numbers, func(number int) bool {
return number%2 == 0
})
fmt.Println(evenNumbers)
}
Final output:
[2 4 6]
After seeing the result, it already feels very much like JavaScript's filter.
However, this code contains several things not formally learned yet:
[]int
range
append
So this article won't expand on them for now.
At this stage, only focus on:
condition func(int) bool
This means:
conditionis a function that receives anintand returns abool.
What is passed in when calling:
func(number int) bool {
return number%2 == 0
}
Is equivalent to the frontend's:
(number) => number % 2 === 0
filter will pass each number to condition:
condition(1) → false
condition(2) → true
condition(3) → false
condition(4) → true
condition(5) → false
condition(6) → true
Only numbers returning true enter the final result.
As for:
What exactly is []int?
Why not [6]int?
What is range?
Why can append add elements?
These questions will be formally addressed in the next stage.
The filter here is only used to verify:
Go functions can also be passed into another function and participate in logical judgment, just like JavaScript callbacks.
What Are Higher-Order Functions?
Learning up to here, higher-order functions are already being used.
As long as a function satisfies any of the following conditions:
Receives a function as an argument
Or
Returns another function
It can usually be called a higher-order function.
For example:
func createCounter() func() int
It returns another function, so createCounter is a higher-order function.
Another example:
func calculate(
a, b int,
operation func(int, int) int,
) int
It receives a function as an argument, so calculate is also a higher-order function.
The filter just written is the same:
func filter(numbers []int, condition func(int) bool) []int
It receives the condition function, so it is also a higher-order function.
For frontend developers, this term should not be unfamiliar.
In JavaScript:
map
filter
reduce
sort
find
some
every
These APIs that heavily receive callback functions all use the idea of higher-order functions behind the scenes.
So although Go doesn't directly use:
numbers.filter(...)
This kind of syntax, the language itself fully supports treating functions as values for saving, passing, and returning.
You Can Give Names to Complex Function Types
When function types are relatively simple, writing directly:
operation func(int, int) int
Is still quite clear.
But if the same function type appears repeatedly, Go can also use type to give it a name:
type Operation func(int, int) int
Then change calculate to:
func calculate(a, b int, operation Operation) int {
return operation(a, b)
}
Complete code:
package main
import "fmt"
type Operation func(int, int) int
func calculate(a, b int, operation Operation) int {
return operation(a, b)
}
func add(a, b int) int {
return a + b
}
func main() {
result := calculate(10, 20, add)
fmt.Println("Calculation result:", result)
}
Here:
type Operation func(int, int) int
Means a function type named Operation is defined.
In the future, seeing:
operation Operation
You can know it requires:
Receives two ints
Returns one int
This article won't continue expanding on custom types for now, just treat it as supplementary knowledge.
Currently, using directly:
func(int, int) int
Is already sufficient for function parameter exercises.
Comparison of Function Capabilities Between Go and JavaScript
This stage can first be organized into the following table:
Table
| Go | JavaScript / TypeScript |
|---|---|
func add() {} |
function add() {} |
f := func() {} |
Function expression |
| No arrow function syntax | () => {} |
func() {}() |
IIFE |
| Closure accesses outer variables | JavaScript closure |
| Function returns function | Function returns function |
func(int) bool |
(value: number) => boolean |
| Function as argument | callback |
| Receives or returns function | Higher-order function |
type Operation func(...) |
TypeScript function type alias |
For me, this is the part where Go and frontend thinking are the closest so far.
The Most Easily Confused Concepts in This Stage
1. Is an anonymous function a closure?
Not necessarily.
The following is an anonymous function:
add := func(a, b int) int {
return a + b
}
It has no name, but also doesn't reference outer variables.
The following anonymous function references the outer count:
count := 0
increase := func() {
count++
}
It forms a closure.
So:
Anonymous functions are not necessarily closures
Closures are also often created through anonymous functions
2. Can functions be passed like variables?
Yes.
func add(a, b int) int {
return a + b
}
result := calculate(10, 20, add)
What is passed in here is the function value:
add
Not the call result:
add(10, 20)
Don't confuse the two:
add
→ The function itself
add(10, 20)
→ The result obtained after calling the function
3. Why not add parentheses when passing add?
Because calculate needs a function:
calculate(10, 20, add)
If written as:
calculate(10, 20, add(10, 20))
The third argument is no longer a function, but the integer obtained after add executes:
30
But calculate's third argument requires:
func(int, int) int
Not int.
4. What does returning a function mean?
The following function:
func createCounter() func() int
Does not directly return an integer.
It returns another function.
So:
counter := createCounter()
At this point, counter is a function.
It needs to be called further:
result := counter()
To get the integer result.
The calling relationship is:
createCounter()
→ Returns function
counter()
→ Calls the returned function
→ Gets int
5. Does each call to createCounter share state?
No.
counterA := createCounter()
counterB := createCounter()
createCounter() is executed twice, creating two independent copies of count.
Therefore:
counterA modifies its own count
counterB modifies its own count
The two do not affect each other.
6. What is a higher-order function?
If a function:
Receives another function as an argument
Or
Returns another function
It can be called a higher-order function.
For example:
func createCounter() func() int
And:
func calculate(a, b int, operation func(int, int) int) int
Are both higher-order functions.
My Biggest Feeling from This Stage
When learning Go earlier, it was often about adapting to differences:
JavaScript's number
→ Go splits into specific types like int, float64
JavaScript's common exception handling
→ Go commonly uses result + error
No syntax directly corresponding to defer in frontend
→ Go can execute deferred calls before function exit
When it came to closures and higher-order functions, the direction suddenly reversed.
It was no longer:
Why is Go designed this way?
But rather:
So this concept is something I've already been using daily in JavaScript.
This also made me realize that when switching languages, there's no need to treat yourself as starting completely from scratch.
Syntax indeed needs to be relearned.
The standard library, engineering system, and runtime mechanisms also need to be re-understood.
But many already mastered programming concepts can continue to be reused:
Scope
Closures
Functions as values
Callback functions
Higher-order functions
State encapsulation
The difference is just how they are expressed in another language.
Of course, this doesn't mean Go closures and JavaScript closures are exactly the same in all details.
Currently, it can only be said:
The core ideas are very close
→ Syntactic expression differs
→ Deeper runtime mechanisms still need further study
Summary of Knowledge Points in This Stage
The content actually learned this time can be organized into the following table:
Table
| Content | Example |
|---|---|
| Create anonymous function | func() {} |
| Save function to variable | add := func(a, b int) int {} |
| Call function via variable | add(10, 20) |
| Immediately execute anonymous function | func() {}() |
| Pass argument during immediate execution | `func(name string) {}( |
Top 1 from juejin.cn, machine-translated. The original thread is authoritative.
[Awesome][Awesome]