Functions

Multiple returns, named returns, variadic functions, closures, and defer.

Beginner 35 min read 🐹 Go

Function Basics

Functions in Go are defined with the func keyword. Unlike most languages, Go puts the type after the parameter name. Functions can return multiple values — this is fundamental to Go's error handling pattern.

package main

import "fmt"

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

// Multiple parameters of same type
func add(a, b int) int {
    return a + b
}

// Multiple return values
func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, fmt.Errorf("cannot divide by zero")
    }
    return a / b, nil
}

func main() {
    fmt.Println(greet("Gopher"))
    fmt.Println(add(3, 4))

    result, err := divide(10, 3)
    if err != nil {
        fmt.Println("Error:", err)
    } else {
        fmt.Printf("10 / 3 = %.2f\n", result)
    }
}
Output
Hello, Gopher!
7
10 / 3 = 3.33
Key Takeaway: Multiple return values are how Go handles errors. The pattern result, err := doSomething() followed by if err != nil is the most common code pattern in Go.

Named Return Values

You can name return values in the function signature. They act as pre-declared variables and a bare return returns their current values:

func rectangleProps(length, width float64) (area, perimeter float64) {
    area = length * width
    perimeter = 2 * (length + width)
    return // returns area and perimeter
}

area, perim := rectangleProps(5, 3)
fmt.Printf("Area: %.0f, Perimeter: %.0f\n", area, perim)
Output
Area: 15, Perimeter: 16

Variadic Functions

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

fmt.Println(sum(1, 2, 3))       // 6
fmt.Println(sum(10, 20, 30, 40)) // 100

// Spread a slice with ...
numbers := []int{5, 10, 15}
fmt.Println(sum(numbers...))     // 30

Closures

Functions in Go can capture variables from their enclosing scope, creating closures. This is commonly used for counters, iterators, and middleware:

func makeCounter() func() int {
    count := 0
    return func() int {
        count++
        return count
    }
}

counter := makeCounter()
fmt.Println(counter()) // 1
fmt.Println(counter()) // 2
fmt.Println(counter()) // 3

defer, panic, recover

defer schedules a function call to run when the enclosing function returns. It's used for cleanup — closing files, releasing locks, logging. Deferred calls execute in LIFO (last-in, first-out) order.

func readFile(path string) error {
    f, err := os.Open(path)
    if err != nil {
        return err
    }
    defer f.Close() // Always runs, even if we return early with an error

    // ... read file ...
    return nil
}

panic and recover

panic causes a runtime crash (like throwing an unrecoverable exception). recover catches a panic in a deferred function. In practice, avoid panic — return errors instead. Use panic only for truly unrecoverable situations (corrupted state, missing critical config).

⚠️ Common Mistake: Defer in a loop

Deferred calls don't run until the function returns. In a loop processing 1000 files, defer f.Close() keeps all 1000 file handles open until the loop ends. Instead, wrap the body in a function or close explicitly.

Practice Exercises

Easy Hello World Variant

Modify the example to accept user input and print a personalized greeting.

Easy Code Reading

Read through the code examples above and predict the output before running them.

Medium Extend the Example

Take one code example and add error handling, input validation, or a new feature.