1. Anonymous function: “a function without a name”
Sometimes you want to pass not only data into a function, but also an instruction: “what to do with it.”
A very programmer-ish real-life example: you give a friend an apple (data) and say one of three things: “wash it,” “slice it,” “eat it and don’t ask questions” (behavior). If every time you write a giant if/else of 50 lines listing all possible actions, the code quickly turns into a massive sheet big enough to cover a small dev team.
Functions as values let you write it like this: there is a common framework, and the specific action is passed as a function parameter. This is especially convenient when the framework repeats but the action changes.
An anonymous function in Go looks like a regular one, just without a name. Officially it’s a function literal, but in plain English—it’s an “anonymous one.”
func(x int) int {
return x * 2
}
Key idea: when you write func(...) { ... }, you create a value. This is not execution yet. Execution starts only when you add call parentheses.
Immediate invocation (IIFE, in plain terms)
Sometimes it’s convenient to run a small block of code right here and right now.
func() {
fmt.Println("=== MiniCalc ===")
fmt.Println("Enter: a op b rule")
}() // parentheses at the end mean a call
It’s useful to notice two pairs of parentheses: the first () is the function’s parameter list (empty here), the second () is the call.
You can do the same with parameters and a result.
res := func(x int) int {
return x * x
}(5)
fmt.Println(res) // 25
The syntax looks a bit weird only the first two times in your life. Then your brain accepts it (and may even start enjoying it, but that’s a diagnosis).
An anonymous function as “compute and assign”
Sometimes a computation needs an if, but you don’t want to clutter main with temporary variables.
x := -10
sign := func(n int) string {
if n < 0 {
return "negative"
}
if n == 0 {
return "zero"
}
return "positive"
}(x)
fmt.Println(sign) // negative
Practical rule: anonymous functions are good as long as they are short and located near where they are used. If the “anonymous one” grows to 30 lines, it stops being cute and starts looking suspicious—like “a simple fix on Friday evening.”
2. Function as a value: put it into a variable and call it
Yes, a variable can store a function.
f := func(x int) int {
return x + 1
}
fmt.Println(f(10)) // 11
This is not magic; it’s types. A function has a type, and that type describes its signature.
How to read a function type
A function type in Go is read left to right: “a function takes … and returns …”. This is one reason why Go declarations are often easier to read than in C-like languages, where “function pointers” can accidentally trigger an existential crisis.
Here’s a small cheat sheet:
| Type | How it reads | What it usually means |
|---|---|---|
|
takes nothing, returns nothing | an action: print, log, “do something” |
|
takes , returns |
a number transformation rule |
|
two → |
a “calculator operation” that can fail |
nil function: an important nuance
A function variable declared via var is nil by default.
var rule func(int) int
fmt.Println(rule == nil) // true
If you try to call rule(10), the program will crash at runtime with a panic, because you’re trying to call “nothing.” This is the situation where the compiler won’t save you: the types are correct, but there is no value.
So if the function comes from “outside” (you choose it from input, get it from another function, store it in a struct field), you need to keep the thought in mind: “is it definitely not nil?”
So signatures don’t look like spells: naming the function type
We’re already familiar with custom types (for example, type Status int), and here we can do the same thing: give a name to a function type. This greatly improves readability.
type Rule func(int) int
Now instead of func(int) int you can write Rule, and the code starts looking friendlier.
3. Higher-order functions: passing a function as a parameter
A higher-order function is a function that takes another function and uses it inside. Sometimes it also returns a function, but today we’ll approach that carefully, without drama.
apply: apply a rule to a number
type Rule func(int) int
func apply(x int, r Rule) int {
return r(x) // call the function passed in as r
}
Usage with a named function:
func double(x int) int { return x * 2 } // returns the doubled parameter
fmt.Println(apply(10, double)) // 20
And with an anonymous one:
fmt.Println(apply(10, func(x int) int { return x * x })) // 100
apply knows nothing about “double” or “square.” It knows only one thing: “I was given x and a rule r, I will return r(x).” That is the core power: separating the framework from the specific behavior.
repeat: perform an action N times
A very simple but useful function. It takes the number of repetitions and an action.
func repeat(n int, action func()) { // action is a variable of type func() with no params and no result
for i := 0; i < n; i++ {
action()
}
}
Usage:
repeat(3, func() { // pass a function with no params and no result
fmt.Println("hi")
})
Inside repeat there’s a regular for, just packaged so that main looks like a script rather than a bag of wires.
A small step for one person
What happens if you write code like this:
count := 0
repeat(5, func() {
count++
})
fmt.Println(count) // 5
The anonymous function “sees” the variable count from outside and modifies it. In Go this is allowed—this clever thing is called a closure.
4. Closures: a function that “remembers” context
A closure is a function that uses variables from an outer scope.
You can also put it this way: a function is not only code, but sometimes also state that it carries with it.
Classic example: a counter
Let’s write a counter factory: the function makeCounter returns a function that increments an internal number on every call.
func makeCounter() func() int {
n := 0
return func() int {
n++
return n
}
}
Usage:
c := makeCounter()
fmt.Println(c()) // 1
fmt.Println(c()) // 2
fmt.Println(c()) // 3
Here the variable n lives “longer” than the execution of makeCounter, because the returned function references it. That’s exactly “the function remembers the context.”
5. Example: MiniCalc v4
Now let’s put everything together into one mini program. We continue developing our learning calculator MiniCalc.
The idea of version v4 is this: the user enters an expression a op b, and then a rule code rule, which will be applied to the result.
Input format:
10 / 2 s
Where rule:
- "d" means double, double the result
- "s" means square, square the result
- "n" means none, do nothing
Function types for readability
type BinOp func(int, int) (int, error)
type Rule func(int) int
Now the signatures of chooseOp and chooseRule will look much nicer.
Parsing int from a string
func parseInt(s string) (int, error) {
n, err := strconv.Atoi(s)
if err != nil {
return 0, err
}
return n, nil
}
Calculator operations
Addition and the rest—no surprises.
func add(a, b int) (int, error) { return a + b, nil }
func sub(a, b int) (int, error) { return a - b, nil }
func mul(a, b int) (int, error) { return a * b, nil }
And we’ll make division safe.
func div(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
Choosing an operation as a function
This is where “function as a value” begins: we don’t compute immediately; we return a function that can compute.
func chooseOp(op string) (BinOp, error) {
switch op {
case "+":
return add, nil
case "-":
return sub, nil
case "*":
return mul, nil
case "/":
return div, nil
default:
return nil, errors.New("unknown operator")
}
}
If the operator is unknown, we return nil and an error. This matters: if you ignore the error and try to call a nil function, you’ll get a crash.
Choosing a rule as a function
Here we actively use anonymous functions: the rules are short, and they usually don’t need separate names.
func chooseRule(code string) (Rule, error) {
switch code {
case "d":
return func(x int) int { return x * 2 }, nil
case "s":
return func(x int) int { return x * x }, nil
case "n":
return func(x int) int { return x }, nil
default:
return nil, errors.New("unknown rule")
}
}
Applying the rule
func apply(x int, r Rule) int {
return r(x)
}
A nice header via repeat
func repeat(n int, action func()) {
for i := 0; i < n; i++ {
action()
}
}
Putting together run and main
Let’s make run() error so that main is very short and readable. This style conveniently keeps the “happy path” at the bottom, and errors bubble up. It pairs well with the idea that errors are values, and you can manage them with regular code.
func run() error {
repeat(20, func() { fmt.Print("=") })
fmt.Println()
var aS, opS, bS, ruleS string
_, err := fmt.Scan(&aS, &opS, &bS, &ruleS)
if err != nil {
return err
}
a, err := parseInt(aS)
if err != nil {
return fmt.Errorf("bad a: %v", err)
}
b, err := parseInt(bS)
if err != nil {
return fmt.Errorf("bad b: %v", err)
}
op, err := chooseOp(opS)
if err != nil {
return err
}
res, err := op(a, b)
if err != nil {
return err
}
rule, err := chooseRule(ruleS)
if err != nil {
return err
}
fmt.Println("result:", apply(res, rule))
return nil
}
main stays very small:
func main() {
if err := run(); err != nil {
fmt.Println("error:", err)
}
}
To see the whole control flow, you can imagine it like this:
flowchart TD
A["Read: a op b rule"] --> B["parseInt(a), parseInt(b)"]
B --> C["chooseOp(op) -> BinOp"]
C --> D["res := op(a,b)"]
D --> E["chooseRule(rule) -> Rule"]
E --> F["final := apply(res, Rule)"]
F --> G["print final"]
6. Typical mistakes
Mistake №1: you wrote an anonymous function and forgot to call it.
func() { ... } by itself executes nothing. It’s just a value. To execute it, you need call parentheses:
func() { ... }().
Mistake №2: confusing “the function” and “the result of calling it.”
double and double(10) are different things. The first can be passed into apply; the second is already a number. If you try to pass a number where a function is expected, the compiler will be right to complain.
Mistake №3: you forgot to check the error and called a nil function.
In chooseOp and chooseRule, bad input returns nil, error. If you ignore err and try to call the result, the program will crash at runtime. That’s why the “call → immediately check err” style is essential here.
Mistake №4: the closure “captured” a variable declared before the loop.
In Go 1.22+, the variable from the loop header (for i := 0; …) is separate for each iteration, and a closure called later sees “its own” value. But if you declare i before the loop (var i int, then for i = 0; …), there is a single variable for all iterations, and every closure will see the last value. Before Go 1.22 (and today too, if go.mod specifies a version below go 1.22), the variable from the loop header behaved the same way — hence the old advice to make a copy v := i. Declare the variable in the loop header.
Mistake №5: the anonymous function grew to the size of a mini novel.
A short anonymous function near its usage is great. An anonymous function of 30 lines with several if statements and lots of temporary variables is already a candidate to be extracted into a named function. A week later, you’ll thank yourself.
GO TO FULL VERSION