On this page

Functions and Multiple Returns

14 min read TextCh. 2 — Functions and Structures

Functions in Go: Flexible and Explicit

Functions are the fundamental building blocks of every Go program. Unlike Python or JavaScript, Go functions are more predictable: the static type system guarantees that you always know exactly what each function receives and returns.

The most important and distinguishing feature of Go functions is the ability to return multiple values. This feature is at the heart of the language's design and is fundamental to understanding idiomatic error handling.

Basic Function Declaration

// Simple function
func greet(name string) string {
    return "Hello, " + name + "!"
}

// No parameters or return
func cleanup() {
    fmt.Println("Cleaning up...")
}

// Same-type parameters — shorthand form
func add(a, b int) int {
    return a + b
}

// Multiple parameter types
func format(name string, age int, active bool) string {
    return fmt.Sprintf("%s (%d years, active: %v)", name, age, active)
}

Multiple Return Values

This is the feature that makes Go so expressive for error handling. A function can return any number of values:

// Returns two values: result and error
func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("cannot divide by zero")
    }
    return a / b, nil
}

// Returns three values
func minMax(nums []int) (int, int, error) {
    if len(nums) == 0 {
        return 0, 0, errors.New("empty slice")
    }
    min, max := nums[0], nums[0]
    for _, n := range nums[1:] {
        if n < min {
            min = n
        }
        if n > max {
            max = n
        }
    }
    return min, max, nil
}

func main() {
    // You must handle all return values
    result, err := divide(10, 3)
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    fmt.Printf("Result: %.2f\n", result)

    // Ignore a return with _ (generally not recommended)
    min, max, _ := minMax([]int{5, 2, 8, 1, 9, 3})
    fmt.Println("Min:", min, "Max:", max)
}

Named Returns

Named returns let you assign names to return values. This has two benefits: it documents the meaning of each value, and it enables the "naked return":

// The names min, max, average are documentation AND variables
func analyze(data []float64) (min, max, average float64, err error) {
    if len(data) == 0 {
        err = errors.New("data required")
        return  // naked return: returns current state of min, max, average, err
    }

    min, max = data[0], data[0]
    sum := 0.0

    for _, d := range data {
        sum += d
        if d < min {
            min = d
        }
        if d > max {
            max = d
        }
    }

    average = sum / float64(len(data))
    return  // naked return: returns current values
}

Named returns are useful for functions that compute multiple related results, but avoid overusing naked returns in long functions — they can reduce readability.

Variadic Functions

Variadic functions accept a variable number of arguments of the same type using ...:

func sum(nums ...int) int {
    total := 0
    for _, n := range nums {
        total += n
    }
    return total
}

func log(level string, parts ...any) {
    fmt.Printf("[%s] ", level)
    fmt.Println(parts...)
}

func main() {
    fmt.Println(sum())           // 0
    fmt.Println(sum(1))          // 1
    fmt.Println(sum(1, 2, 3))    // 6
    fmt.Println(sum(1, 2, 3, 4, 5, 6, 7, 8, 9, 10))  // 55

    // Pass a slice to a variadic function with ...
    numbers := []int{10, 20, 30, 40}
    fmt.Println(sum(numbers...))  // 100

    log("INFO", "Server", "started", "on port", 8080)
}

Inside the function, the variadic parameter behaves as a slice. The ... operator when calling the function expands a slice into individual arguments.

Functions as First-Class Values

In Go, functions are values. You can assign them to variables, pass them as arguments, and return them:

// Function type
type Transformer func(int) int

// Higher-order function
func applyTwice(f Transformer, x int) int {
    return f(f(x))
}

func double(n int) int {
    return n * 2
}

// Anonymous function assigned to a variable
triple := func(n int) int {
    return n * 3
}

func main() {
    fmt.Println(applyTwice(double, 3))   // 12
    fmt.Println(applyTwice(triple, 2))   // 18

    // Immediately invoked function expression (IIFE)
    result := func(a, b int) int {
        return a + b
    }(5, 3)
    fmt.Println(result)  // 8
}

Closures: Functions with State

A closure is a function that captures variables from its outer scope. The function "closes over" those variables, keeping them alive even after the outer scope has returned:

// Counter factory — each call creates an independent counter
func newCounter() func() int {
    count := 0  // this variable lives as long as the closure exists
    return func() int {
        count++
        return count
    }
}

// Closure for memoization
func memoize(f func(int) int) func(int) int {
    cache := make(map[int]int)
    return func(n int) int {
        if result, ok := cache[n]; ok {
            return result
        }
        result := f(n)
        cache[n] = result
        return result
    }
}

func fibonacci(n int) int {
    if n <= 1 {
        return n
    }
    return fibonacci(n-1) + fibonacci(n-2)
}

func main() {
    c1 := newCounter()
    c2 := newCounter()  // independent counter

    fmt.Println(c1(), c1(), c1())  // 1 2 3
    fmt.Println(c2(), c2())        // 1 2 (independent of c1)

    fibMemo := memoize(fibonacci)
    fmt.Println(fibMemo(40))  // fast thanks to the cache
}

`defer`: Guaranteed Resource Cleanup

defer postpones the execution of a function until the surrounding function returns. It is the idiomatic way to guarantee that resources are released correctly, regardless of how the function exits (normal return, error, or panic):

import "os"

func readFile(name string) (string, error) {
    f, err := os.Open(name)
    if err != nil {
        return "", err
    }
    defer f.Close()  // guaranteed: runs when the function exits, no matter what

    buf := make([]byte, 1024)
    n, err := f.Read(buf)
    if err != nil {
        return "", err  // f.Close() will still run
    }

    return string(buf[:n]), nil
}

LIFO Order of defer

func demoDefer() {
    defer fmt.Println("third")   // runs first (LIFO)
    defer fmt.Println("second")  // runs second
    defer fmt.Println("first")   // runs last declared, runs first
    fmt.Println("function running")
}
// Output:
// function running
// first
// second
// third

Recursion

Go supports recursion with the same behavior as any language:

func factorial(n int) int {
    if n <= 1 {
        return 1
    }
    return n * factorial(n-1)
}

func fibonacci(n int) int {
    if n <= 1 {
        return n
    }
    return fibonacci(n-1) + fibonacci(n-2)
}

For deep recursion, consider using iteration or accumulator techniques, since Go does not optimize tail calls automatically.

With a solid mastery of Go functions, in the next lesson we will learn structs — Go's way of organizing data and behavior to model the real world.

The (value, error) pattern is Go's signature
Returning (result, error) as a pair is the most idiomatic Go pattern. The caller must always check the error before using the result. This pattern makes error handling explicit and visible in the code, unlike exceptions that can be silently ignored.
defer executes in LIFO order
Multiple defer statements execute in Last-In First-Out order. If you have defer A(), defer B(), defer C(), they run in order C, B, A when the function exits. Use this to guarantee cleanup of resources such as closing files or database connections.