Functions
Functions in Go — multiple and named returns, variadic parameters, first-class function values, and recursion.
5 questions
JuniorTheoryVery commonHow do multiple and named return values work in Go?
How do multiple and named return values work in Go?
A Go function can return several values at once, idiomatically a (T, error) pair returned as return val, err. You can also name the results: named results are pre-declared variables that start at their zero value and stay in scope for the whole body, enabling a naked return that sends back their current values. The caller must read every returned value or discard one with _.
Common mistakes
- ✗Thinking a naked
returnignores the named results instead of sending their current values - ✗Forgetting that a named result starts at its zero value, not unset or
nil-only - ✗Leaving a returned value unbound without
_, which fails to compile
Follow-up questions
- →When does a naked
returnhurt readability despite being legal? - →How does a deferred closure interact with a named return value?
JuniorTheoryCommonFunctions in Go are first-class values. What does that mean in practice?
Functions in Go are first-class values. What does that mean in practice?
First-class means a function is an ordinary value: you can assign it to a variable, pass it as an argument, and return it from another function. Each function has a function type like func(int) int describing its parameters and results, so the compiler type-checks these uses. You can also write an anonymous function inline, with no name.
Common mistakes
- ✗Thinking only named top-level functions count, not anonymous inline ones
- ✗Forgetting that a function value has a type like
func(int) int - ✗Believing you cannot return a function from another function
Follow-up questions
- →How does an anonymous function become a closure over surrounding variables?
- →What is the zero value of a function-typed variable, and what happens if you call it?
JuniorTheoryCommonWhat is a variadic parameter in Go and how is it passed?
What is a variadic parameter in Go and how is it passed?
Writing ...T on the last parameter makes a function variadic: it collects any number of trailing arguments into a []T slice, so inside the body the parameter is an ordinary slice you can range over. You call it with separate args like f(a, b, c), or spread an existing slice with f(s...). Standard functions such as append and fmt.Println are variadic this way.
Common mistakes
- ✗Forgetting the variadic parameter must be the last one in the signature
- ✗Passing a slice directly instead of spreading it with the
s...form - ✗Assuming the collected arguments are an array rather than a
[]Tslice
Follow-up questions
- →What does passing zero arguments to a variadic parameter give you inside the body?
- →Why can a variadic call sometimes allocate a backing array on the heap?
JuniorTheoryOccasionalHow does recursion work in Go, and is there tail-call optimization?
How does recursion work in Go, and is there tail-call optimization?
Recursion is a function that calls itself, and it needs a base case that returns without recursing so the chain terminates. Go does NOT guarantee tail-call optimization, so even a tail-recursive call adds a new frame: each level consumes more of the goroutine stack, which grows but can eventually overflow. Because of this, an explicit loop is preferred for hot or very deep paths.
Common mistakes
- ✗Assuming Go does tail-call optimization and recursion is free on stack
- ✗Forgetting a base case, so the recursion never terminates and overflows
- ✗Reaching for recursion on a hot deep path where a loop is cheaper
Follow-up questions
- →What error or panic do you get when a Go recursion exhausts the stack?
- →How does the goroutine stack growing on demand differ from a fixed C stack?
MiddleTheoryOccasionalHow can a deferred function modify a named return value?
How can a deferred function modify a named return value?
Named results are real variables that stay in scope until the function actually returns, even after a return statement has set them. A deferred closure — itself a first-class function value — runs in that window, so it can read and reassign them: defer func(){ err = wrap(err) }() overwrites what the caller receives. This is impossible with unnamed returns, since no variable is left for the deferred call to touch.
Common mistakes
- ✗Thinking the defer gets a copy of the result rather than the live variable
- ✗Expecting the same trick to work with unnamed returns, where no variable exists
- ✗Believing the defer runs before
returnsets the result, not after
Follow-up questions
- →In what order do multiple deferred edits to the same named result apply?
- →Why is this pattern common for wrapping or annotating an error before it leaves a function?