3.5 Functions

Functions are first-class citizens in Go. They can be passed as arguments, returned from other functions, and assigned to variables. This section covers all aspects of Go functions.

Function Declaration

Functions in Go are declared using the func keyword, followed by the function name, parameters, return types, and the function body. The syntax is straightforward and consistent, making Go code predictable and easy to read.

The basic structure consists of four parts: the func keyword, the function name, the parameter list (which can be empty), and the return type list (which can also be empty). Go requires explicit type declarations for parameters and return values, which contributes to type safety and code clarity.

Basic function

A simple function takes parameters and returns a value. The return type comes after the parameter list, separated by parentheses if there are multiple return values.

func greet(name string) string {
    return "Hello, " + name
}

This function takes a single string parameter and returns a string. The function body uses the return statement to provide the result. When you call this function, you must provide a string argument, and you'll receive a string result.

Function with multiple parameters

Functions can accept multiple parameters. Each parameter must have its type specified. When consecutive parameters share the same type, Go allows you to specify the type once for all of them, which reduces repetition.

func add(a int, b int) int {
    return a + b
}

// Shorthand when types are the same
func multiply(x, y int) int {
    return x * y
}

In the add function, both parameters are explicitly typed as int. The multiply function demonstrates the shorthand: x, y int means both x and y are of type int. This shorthand only works when parameters are consecutive and share the same type.

Function with no parameters

Functions don't require parameters. When a function needs no input, you simply use empty parentheses. This is common for functions that retrieve information or perform actions that don't depend on external input.

func getCurrentTime() time.Time {
    return time.Now()
}

The empty parameter list () indicates this function takes no arguments. It still returns a value, in this case a time.Time representing the current time.

Function with no return value

Functions can also have no return value, which is useful for functions that perform actions rather than calculations. When a function returns nothing, you omit the return type entirely.

func printMessage(msg string) {
    fmt.Println(msg)
}

This function performs a side effect (printing) but doesn't return a value. Functions like this are often used for their side effects rather than their return values. In Go, this is perfectly valid and common for operations like logging, printing, or modifying state.

Parameters and Return Values

Go's function parameter and return value system is designed for clarity and flexibility, providing multiple ways to pass data into and out of functions.

Single return value

The simplest case is a function that takes parameters and returns a single value. The return type is specified after the parameter list.

func square(x int) int {
    return x * x
}

This function takes one int parameter and returns one int value. The return type appears after the closing parenthesis of the parameter list. When you call this function, you receive a single value that you can use directly or assign to a variable.

Multiple return values

One of Go's distinctive features is its support for multiple return values. This is particularly powerful for error handling, as functions can return both a result and an error indicator. Multiple return values are specified in parentheses, with each value having its own type.

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

This function returns two values: a float64 result and an error. The return types are enclosed in parentheses: (float64, error). When the operation succeeds, it returns the result and nil for the error. When it fails (division by zero), it returns 0 and a descriptive error. This pattern is idiomatic in Go and makes error handling explicit and visible.

Using multiple return values

When calling a function that returns multiple values, you must handle all of them. Go requires you to explicitly acknowledge each return value, either by using it or explicitly ignoring it with the blank identifier _.

result, err := divide(10, 2)
if err != nil {
    fmt.Println("Error:", err)
    return
}
fmt.Println("Result:", result)

The call divide(10, 2) returns two values, which are captured using the short variable declaration syntax. The error is checked immediately—this is the standard Go pattern. If there's an error, you handle it and return early. Only if there's no error do you proceed to use the result. This pattern ensures errors are never ignored accidentally.

Ignoring return values

Sometimes you don't need all return values. Go allows you to explicitly ignore values using the blank identifier _. This is useful when you're certain an error won't occur, or when you only care about one of the return values.

result, _ := divide(10, 2)  // Ignore error (use with caution)
_, err := divide(10, 0)     // Ignore result, check error

The first line ignores the error, which should only be done when you're absolutely certain the operation will succeed. The second line ignores the result but checks the error, which is useful when you only care about whether an operation succeeded, not its result. Be careful when ignoring errors—Go's philosophy is that errors should be handled explicitly.

Named Return Values

Go allows you to name return values in the function signature. This feature provides several benefits, though it should be used judiciously to maintain code clarity.

When you name return values, you're essentially declaring variables at the function scope. These variables are automatically initialized to their zero values, and you can assign to them throughout the function. When you use a "naked return" (just return without specifying values), Go automatically returns the current values of these named variables.

func calculate(x, y int) (sum int, product int) {
    sum = x + y
    product = x * y
    return  // Naked return (returns named values)
}

In this function, sum and product are named return values. They're declared in the function signature and can be used like regular variables within the function. The naked return statement automatically returns the current values of sum and product. This can make code more concise, but it can also reduce clarity if overused.

Benefits of named returns

Named return values serve several purposes. They make function signatures self-documenting—you can see what the function returns just by reading the signature. They enable "naked returns" which can reduce verbosity in simple functions. Most importantly, they're useful with deferred operations, as deferred functions can access and modify named return values.

Example with deferred operations

Named returns are particularly powerful when combined with defer, because deferred functions can access and modify the named return values. This is useful for cleanup operations, logging, or error handling that needs to run regardless of how the function exits. (The defer statement will be covered in detail in section 4.7.)

func divideWithLog(a, b float64) (result float64, err error) {
    defer func() {
        if err != nil {
            log.Printf("Division failed: %v", err)
        }
    }()

    if b == 0 {
        err = fmt.Errorf("division by zero")
        return  // Returns zero value for result, and the error
    }

    result = a / b
    return  // Returns calculated result and nil error
}

Here, the deferred function can access err even though it executes after the return statement. If an error occurred, the deferred function logs it. This pattern is common in Go for ensuring cleanup or logging happens regardless of the function's exit path. The named return values are initialized to their zero values (0.0 for float64, nil for error), so if you don't explicitly set them, they return as zero values.

When to use named returns

Named returns are most appropriate when:

However, for longer functions or when the names don't add clarity, explicit returns with values are often clearer and make the code easier to follow.

Variadic Functions

Variadic functions accept a variable number of arguments of the same type. This feature provides flexibility when you don't know in advance how many arguments a function will receive, eliminating the need for function overloading (which Go doesn't support) or always passing slices.

Without variadic functions, you'd need to either create multiple function versions for different argument counts or always pass a slice. Variadic functions provide a clean, natural syntax for functions that conceptually work with multiple values, like fmt.Printf which can format any number of arguments, or a sum function that can add any number of integers.

When you declare a parameter with ... before the type (e.g., numbers ...int), Go treats it as a variadic parameter. Inside the function, this parameter is actually a slice of that type. When you call the function, Go automatically collects all the arguments after the regular parameters into a slice. If no arguments are provided for the variadic parameter, it becomes an empty slice (not nil), which means you can safely range over it without checking for nil.

func sum(numbers ...int) int {
    // numbers is actually []int inside the function
    total := 0
    for _, n := range numbers {
        total += n
    }
    return total
}

// Usage
result := sum(1, 2, 3, 4, 5)  // numbers = []int{1, 2, 3, 4, 5}
result = sum()                // numbers = []int{} (empty slice, not nil)

When you call sum(1, 2, 3), Go creates a temporary slice []int{1, 2, 3} and passes it to the function. The variadic parameter must be the last parameter in the function signature because Go needs to know where the variadic arguments end—it can't distinguish between arguments meant for the variadic parameter and arguments meant for subsequent parameters if the variadic parameter isn't last.

Variadic with other parameters

func printNumbers(prefix string, numbers ...int) {
    // prefix is a regular parameter
    // numbers is variadic (must be last)
    fmt.Print(prefix, ": ")
    for _, n := range numbers {
        fmt.Print(n, " ")
    }
    fmt.Println()
}

printNumbers("Numbers", 1, 2, 3)  // prefix="Numbers", numbers=[]int{1,2,3}

Passing slices to variadic functions

numbers := []int{1, 2, 3, 4, 5}
result := sum(numbers...)  // Use ... to unpack slice into individual arguments

The ... operator after a slice "unpacks" it into individual arguments. So sum(numbers...) is equivalent to sum(numbers[0], numbers[1], numbers[2], ...). This allows you to pass a slice to a variadic function when you already have the values in a slice, providing flexibility in how you call variadic functions.

Complete example

package main

import "fmt"

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

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

// Named return values
func calculate(x, y int) (sum int, product int) {
    sum = x + y
    product = x * y
    return
}

// Variadic function
func sum(numbers ...int) int {
    total := 0
    for _, n := range numbers {
        total += n
    }
    return total
}

func main() {
    fmt.Println(greet("Go"))

    result, err := divide(10, 2)
    if err != nil {
        fmt.Println("Error:", err)
    } else {
        fmt.Println("Result:", result)
    }

    s, p := calculate(5, 3)
    fmt.Printf("Sum: %d, Product: %d\n", s, p)

    fmt.Println("Sum:", sum(1, 2, 3, 4, 5))
}

Function Types

Functions in Go are first-class values, meaning they can be assigned to variables, passed as arguments, returned from other functions, and stored in data structures. This enables powerful programming patterns like higher-order functions, callbacks, and function factories.

Defining function types

You can define custom types for functions, which makes function signatures more readable and allows you to create reusable function type definitions. Function types are declared using the func keyword followed by the parameter and return types.

// Function type
type Operation func(int, int) int

var add Operation = func(a, b int) int {
    return a + b
}

var multiply Operation = func(a, b int) int {
    return a * b
}

// Using function types
func calculate(a, b int, op Operation) int {
    return op(a, b)
}

result := calculate(5, 3, add)      // 8
result = calculate(5, 3, multiply)  // 15

Here, Operation is a type that represents any function taking two int parameters and returning an int. Both add and multiply are of type Operation, so they can be used interchangeably wherever an Operation is expected. The calculate function accepts an Operation as a parameter, making it a higher-order function—a function that operates on other functions.

Functions as parameters

Passing functions as parameters enables powerful abstractions. You can write generic functions that work with different behaviors by accepting a function parameter that defines the specific behavior.

func apply(numbers []int, fn func(int) int) []int {
    result := make([]int, len(numbers))
    for i, n := range numbers {
        result[i] = fn(n)
    }
    return result
}

// Usage
numbers := []int{1, 2, 3, 4, 5}
doubled := apply(numbers, func(x int) int {
    return x * 2
})

The apply function takes a slice and a function, applying the function to each element. This pattern is common in functional programming and allows you to write reusable code that works with different transformations. You can pass any function that matches the signature func(int) int, including anonymous functions defined inline.

Benefits of function types

Function types enable several powerful patterns:

This flexibility makes Go code more modular and testable, as you can easily substitute different implementations for testing or different runtime behaviors.

Closures

Closures are functions that capture and retain access to variables from their surrounding lexical scope. This powerful feature enables functions to "remember" their environment, creating stateful functions and enabling sophisticated programming patterns.

Closures allow functions to access variables from their enclosing scope even after that scope has finished executing. This capability enables several important patterns:

When a function references a variable from an outer scope, Go automatically captures that variable in the closure. The captured variable's lifetime extends to match the closure's lifetime—it continues to exist as long as the closure exists, even if the outer function has already returned. This is different from regular local variables, which are destroyed when their function returns.

The mechanism works by the compiler creating a structure that holds references to the captured variables. When you call the closure, it accesses these captured variables. Multiple closures can share the same captured variable (if they're created in the same scope and capture the same variable), or each closure can have its own independent copy (depending on when and how the variable is captured).

func counter() func() int {
    count := 0  // Local variable in counter()
    return func() int {
        count++  // This closure captures 'count' from outer scope
        return count
    }
}

// Usage
c := counter()  // Returns a closure that has captured 'count'
fmt.Println(c())  // 1 (count was 0, now incremented to 1)
fmt.Println(c())  // 2 (count persists between calls)
fmt.Println(c())  // 3 (count continues to increment)

When counter() is called, it creates a local variable count. The returned function (closure) captures a reference to this variable. Even after counter() returns, the count variable continues to exist because the closure holds a reference to it. Each call to c() increments the same count variable.

Practical closure example

func multiplier(factor int) func(int) int {
    // factor is captured by the returned closure
    return func(x int) int {
        return x * factor  // Uses captured 'factor'
    }
}

// Usage
double := multiplier(2)   // Creates closure with factor=2
triple := multiplier(3)   // Creates closure with factor=3

fmt.Println(double(5))   // 10 (5 * 2, using captured factor=2)
fmt.Println(triple(5))  // 15 (5 * 3, using captured factor=3)

Each call to multiplier() creates a new closure with its own captured factor. double and triple are independent closures, each with its own captured value. This demonstrates how closures can create specialized functions from a template—the same function can generate multiple specialized versions, each with different captured values.

Closure with loop variables (Go 1.23)

// In Go 1.23, this works correctly
var funcs []func()
for i := 0; i < 3; i++ {
    funcs = append(funcs, func() {
        fmt.Println(i)  // Each closure gets its own copy of i
    })
}

for _, f := range funcs {
    f()  // Prints: 0, 1, 2 (correct in Go 1.23)
}

Complete example

package main

import "fmt"

// Function type
type MathOp func(int, int) int

// Functions
var add MathOp = func(a, b int) int {
    return a + b
}

var multiply MathOp = func(a, b int) int {
    return a * b
}

// Function that takes function as parameter
func compute(a, b int, op MathOp) int {
    return op(a, b)
}

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

// Closure with parameter
func makeMultiplier(factor int) func(int) int {
    return func(x int) int {
        return x * factor
    }
}

func main() {
    // Using function types
    result1 := compute(5, 3, add)
    result2 := compute(5, 3, multiply)
    fmt.Printf("Add: %d, Multiply: %d\n", result1, result2)

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

    // Closure: multiplier
    double := makeMultiplier(2)
    triple := makeMultiplier(3)
    fmt.Printf("Double 5: %d, Triple 5: %d\n", double(5), triple(5))
}

Key points about functions