Functions & Multiple Returns
Declaring functions, multiple return values, named returns, and variadic parameters.
Declaring functions
package main
import "fmt"
func add(a int, b int) int {
return a + b
}
func main() {
fmt.Println(add(2, 3)) // 5
}
When consecutive parameters share a type, you can shorten the signature:
func add(a, b int) int {
return a + b
}
Multiple return values
This is one of Go's most distinctive features — functions routinely return a result and an error together, instead of throwing exceptions:
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, fmt.Errorf("cannot divide %v by zero", a)
}
return a / b, nil
}
func main() {
result, err := divide(10, 2)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("Result:", result) // Result: 5
}
Named return values
func split(total int) (half int, remainder int) {
half = total / 2
remainder = total % 2
return // "naked" return — sends back the named values as-is
}
Named returns are useful for documenting intent, but most idiomatic Go still prefers explicit return half, remainder for clarity in anything non-trivial.
Variadic parameters
func sum(nums ...int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}
func main() {
fmt.Println(sum(1, 2, 3)) // 6
fmt.Println(sum(1, 2, 3, 4, 5)) // 15
values := []int{10, 20, 30}
fmt.Println(sum(values...)) // spread a slice into variadic args — 60
}
Functions as values
Go treats functions as first-class values — they can be assigned to variables, passed as arguments, and returned from other functions:
func applyTwice(f func(int) int, x int) int {
return f(f(x))
}
func main() {
double := func(n int) int { return n * 2 }
fmt.Println(applyTwice(double, 3)) // 12 — double(double(3))
}
Closures
An anonymous function can capture variables from its surrounding scope:
func makeCounter() func() int {
count := 0
return func() int {
count++
return count
}
}
func main() {
counter := makeCounter()
fmt.Println(counter()) // 1
fmt.Println(counter()) // 2
fmt.Println(counter()) // 3
}
Defer
defer schedules a function call to run right before the enclosing function returns — perfect for guaranteed cleanup, regardless of how the function exits:
func readFile() {
fmt.Println("opening file")
defer fmt.Println("closing file") // runs last, even if a panic happens above
fmt.Println("reading file")
}
opening file
reading file
closing file
Common mistakes
- Ignoring the returned
errorvalue — it's just a normal value, so nothing forces you to check it (unlike Java's checked exceptions). Idiomatic Go always checksif err != nilimmediately after a call. - Forgetting that
deferarguments are evaluated immediately, even though the call itself happens later. - Overusing named returns for complex functions, which can make control flow harder to follow.
Interview questions
Q: Why does Go use multiple return values (result, error) instead of exceptions?
It makes error handling explicit and visible at every call site — the compiler doesn't force it, but Go's culture and tooling (go vet, linters) strongly encourage always checking the error immediately, keeping failure paths as visible as the happy path.
Q: When would you use defer?
For guaranteed cleanup that must run regardless of how a function exits — closing files, unlocking a mutex, closing a database connection or HTTP response body — since it runs even if a panic unwinds the stack.