Functions in Go: init, Anonymous Functions, Higher-Order Functions, Variadic and defer
Every shape a Go function can take, plus callbacks, variadic parameters and the three rules that explain every defer puzzle.
On this page
In Go, functions are first-class values: you can store them in variables, pass them to other functions and return them. That one idea unlocks callbacks, middleware, decorators and closures. This article covers every shape a Go function can take, plus two features you will use daily: variadic parameters and defer.
Named functions
The standard form you already know:
func add(a, b int) int {
return a + b
}Parameters vs arguments: a and b are parameters (the names in the definition). The values you pass when calling, like add(2, 3), are arguments.
The special init function
A package can declare init() functions. Go calls them automatically, once, after package-level variables are initialised and before main runs. You cannot call init yourself, and it takes no parameters and returns nothing.
package main
import "fmt"
var num = 10
func init() {
fmt.Println("init:", num) // init: 10
num = 100
}
func main() {
fmt.Println("main:", num) // main: 100
}Use init sparingly, for things like registering drivers or validating configuration. Hidden work at startup makes programs harder to test and reason about.
Anonymous functions and IIFE
A function without a name is an anonymous function. You can call it immediately, which is called an IIFE (Immediately Invoked Function Expression):
func(a, b int) {
fmt.Println("sum:", a+b)
}(2, 3) // runs right away: sum: 5This is handy for running a small block with its own scope, and it is the usual way to start a goroutine: go func() { ... }().
Function values (function expressions)
Assign a function to a variable and call it through the variable:
doSum := func(a, b int) int {
return a + b
}
fmt.Println(doSum(2, 3)) // 5Unlike a named function, a function value only exists from the line where it is assigned. Calling doSum before that line is a compile error.
The type of doSum is func(int, int) int. You can name function types to make signatures readable:
type Operation func(a, b int) intHigher-order functions and callbacks
A higher-order function takes a function as a parameter, returns one, or both. The function you pass in is often called a callback.
package main
import "fmt"
type Operation func(a, b int) int
func add(a, b int) int { return a + b }
func multiply(a, b int) int { return a * b }
// Takes a function as a parameter (callback)
func process(a, b int, op Operation) int {
return op(a, b)
}
// Returns a function
func pick(name string) Operation {
if name == "mul" {
return multiply
}
return add
}
func main() {
fmt.Println(process(2, 3, add)) // 5
fmt.Println(process(2, 3, multiply)) // 6
fmt.Println(pick("mul")(4, 5)) // 20
}This pattern is all over real Go code. For example, sort.Slice takes a "less" function, and HTTP middleware is a function that takes a handler and returns a new one (we build one in the web chapter).
10, true or "hello" are. Higher-order functions come from functional programming (languages like Haskell), which in turn comes from higher-order logic in mathematics.Variadic functions
A variadic function accepts any number of arguments of one type. Inside the function the parameter is just a slice.
func sum(nums ...int) int { // nums is []int
total := 0
for _, n := range nums {
total += n
}
return total
}
sum() // 0
sum(1, 2, 3) // 6
numbers := []int{1, 2, 3, 4, 5}
sum(numbers...) // 15, "spread" an existing slice with ...The variadic parameter must be the last one. fmt.Println(a ...any) is the most famous example: any (an alias for interface{}) is why it accepts values of different types.
sum(numbers...) passes the same underlying array, not a copy. If the function modifies nums[0], the caller's slice changes too.defer: run something when the function returns
defer schedules a call to run when the surrounding function returns, whether it returns normally or because of a panic. It is how Go guarantees cleanup.
f, err := os.Open("data.txt")
if err != nil {
return err
}
defer f.Close() // always runs, no matter which return path we takeThree rules explain every defer puzzle:
defer line, not when the call finally runs.package main
import "fmt"
func add(a, b int) {
fmt.Println("Result:", a+b)
}
func counter() (n int) {
defer func() { n++ }() // modifies the named result after return 41
return 41
}
func main() {
x := 1
defer fmt.Println("deferred x =", x) // x is captured as 1 right now
x = 2
fmt.Println("Start...")
defer fmt.Println("End")
defer add(1, 3)
defer add(2, 3)
fmt.Println("counter:", counter())
}Start...
counter: 42
Result: 5
Result: 4
End
deferred x = 1Read the output from the bottom of the defer stack: add(2, 3) was deferred last, so it runs first. And even though x became 2, the deferred print shows 1 because its argument was evaluated at the defer line.
defer right after acquiring a resource (Open, Lock, Begin). Then the cleanup sits next to the setup and you cannot forget it on an early return. Avoid defer inside long loops: deferred calls only run when the function returns, so they pile up.Key takeaways
init runs automatically before main; keep it small.values....defer is LIFO, evaluates arguments immediately, and can change named results.Last updated on