Functions & Multiple Returns

Declaring functions, multiple return values, named returns, and variadic parameters.

Declaring functions

Go
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:

Go
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:

Go
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

Go
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

Go
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:

Go
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:

Go
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:

Go
func readFile() {
    fmt.Println("opening file")
    defer fmt.Println("closing file") // runs last, even if a panic happens above
    fmt.Println("reading file")
}
Plaintext
opening file
reading file
closing file

Common mistakes

  • Ignoring the returned error value — it's just a normal value, so nothing forces you to check it (unlike Java's checked exceptions). Idiomatic Go always checks if err != nil immediately after a call.
  • Forgetting that defer arguments 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.