In the previous tutorial, you learned about variables, types, and constants. Now it is time to learn about functions. Functions are the building blocks of every Go program.

Go functions have a unique feature: they can return multiple values. This is the foundation of Go’s error handling pattern.

Basic Functions

A function in Go starts with the func keyword:

package main

import "fmt"

// A function that takes two ints and returns their sum
func add(a int, b int) int {
    return a + b
}

// When parameters have the same type, you can shorten it
func multiply(a, b int) int {
    return a * b
}

// A function with no return value
func greet(name string) {
    fmt.Printf("Hello, %s!\n", name)
}

func main() {
    result := add(3, 4)
    fmt.Println("3 + 4 =", result)

    product := multiply(5, 6)
    fmt.Println("5 * 6 =", product)

    greet("Alex")
}

Output:

3 + 4 = 7
5 * 6 = 30
Hello, Alex!

The function signature follows this pattern:

func functionName(param1 type1, param2 type2) returnType {
    // body
}

Multiple Return Values

This is one of Go’s most important features. A function can return more than one value:

package main

import "fmt"

// Returns both the quotient and remainder
func divide(a, b int) (int, int) {
    return a / b, a % b
}

// Returns min and max of two numbers
func minMax(a, b int) (int, int) {
    if a < b {
        return a, b
    }
    return b, a
}

func main() {
    quotient, remainder := divide(17, 5)
    fmt.Printf("17 / 5 = %d remainder %d\n", quotient, remainder)

    min, max := minMax(42, 7)
    fmt.Printf("Min: %d, Max: %d\n", min, max)
}

Output:

17 / 5 = 3 remainder 2
Min: 7, Max: 42

If you don’t need one of the return values, use the blank identifier _:

quotient, _ := divide(17, 5) // Ignore the remainder
_, max := minMax(42, 7)      // Ignore the min

The Error Type

Go does not have exceptions like Java or Python. Instead, functions return an error as the last return value. This is Go’s most important pattern.

The error type is a built-in interface. If the function succeeds, the error is nil. If it fails, the error contains a message.

package main

import (
    "errors"
    "fmt"
)

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

func main() {
    // Success case
    result, err := divide(10, 3)
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    fmt.Printf("10 / 3 = %.2f\n", result)

    // Error case
    result, err = divide(10, 0)
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    fmt.Printf("10 / 0 = %.2f\n", result)
}

Output:

10 / 3 = 3.33
Error: cannot divide by zero

The if err != nil pattern is the most common code pattern in Go. You will see it hundreds of times in every Go project. The idea is simple:

  1. Call a function that might fail
  2. Check if the error is not nil
  3. If there is an error, handle it (log it, return it, etc.)
  4. If there is no error, use the result

Creating Errors

There are two main ways to create errors:

package main

import (
    "errors"
    "fmt"
)

func validateAge(age int) error {
    if age < 0 {
        // Method 1: errors.New — simple error message
        return errors.New("age cannot be negative")
    }
    if age > 150 {
        // Method 2: fmt.Errorf — formatted error message
        return fmt.Errorf("age %d is not realistic", age)
    }
    return nil // No error
}

func main() {
    ages := []int{25, -5, 200}

    for _, age := range ages {
        err := validateAge(age)
        if err != nil {
            fmt.Printf("Age %d: Error — %s\n", age, err)
        } else {
            fmt.Printf("Age %d: Valid\n", age)
        }
    }
}

Output:

Age 25: Valid
Age -5: Error — age cannot be negative
Age 200: Error — age 200 is not realistic

Use errors.New for simple error messages. Use fmt.Errorf when you need to include variable values in the message.

Named Return Values

Go lets you name your return values. This can make your code more readable:

package main

import (
    "fmt"
    "math"
)

// Named return values: area and perimeter
func circleStats(radius float64) (area float64, perimeter float64) {
    area = math.Pi * radius * radius
    perimeter = 2 * math.Pi * radius
    return // "Naked return" — returns the named values
}

func main() {
    area, perimeter := circleStats(5)
    fmt.Printf("Circle with radius 5:\n")
    fmt.Printf("  Area: %.2f\n", area)
    fmt.Printf("  Perimeter: %.2f\n", perimeter)
}

Output:

Circle with radius 5:
  Area: 78.54
  Perimeter: 31.42

Named returns are useful for documentation. They tell the reader what each return value means. But don’t overuse naked returns. In long functions, explicit return area, perimeter is clearer.

Variadic Functions

A variadic function accepts a variable number of arguments. Use ... before the type:

package main

import "fmt"

// sum accepts any number of integers
func sum(numbers ...int) int {
    total := 0
    for _, n := range numbers {
        total += n
    }
    return total
}

// You can have regular parameters before the variadic parameter
func printInfo(prefix string, values ...int) {
    fmt.Printf("%s: %v\n", prefix, values)
}

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

    printInfo("Scores", 95, 87, 92)
    printInfo("Ages", 25, 30)

    // You can also pass a slice using ...
    numbers := []int{5, 10, 15}
    fmt.Println(sum(numbers...)) // 30
}

Output:

6
100
0
Scores: [95 87 92]
Ages: [25 30]
5 10 15
30

Inside the function, numbers is a slice ([]int). The ... syntax is just a convenient way to pass multiple values without creating a slice manually.

fmt.Println is itself a variadic function. That is why you can pass any number of arguments to it.

The defer Keyword

defer schedules a function call to run when the surrounding function returns. It is used for cleanup actions.

package main

import "fmt"

func main() {
    fmt.Println("Start")
    defer fmt.Println("This runs last")
    fmt.Println("Middle")
    fmt.Println("End")
}

Output:

Start
Middle
End
This runs last

The deferred call runs after all other code in the function, right before the function returns.

Multiple Defers

If you have multiple defer statements, they run in reverse order (last in, first out):

package main

import "fmt"

func main() {
    defer fmt.Println("First defer")
    defer fmt.Println("Second defer")
    defer fmt.Println("Third defer")
    fmt.Println("Main function")
}

Output:

Main function
Third defer
Second defer
First defer

defer for Cleanup

The most common use of defer is closing resources like files:

package main

import (
    "fmt"
    "os"
)

func readFile(filename string) error {
    file, err := os.Open(filename)
    if err != nil {
        return fmt.Errorf("failed to open file: %w", err)
    }
    defer file.Close() // This runs when readFile returns

    // Read and process the file here...
    fmt.Println("File opened successfully:", filename)

    return nil
}

func main() {
    err := readFile("test.txt")
    if err != nil {
        fmt.Println("Error:", err)
    }
}

By putting defer file.Close() right after opening the file, you guarantee the file will be closed. Even if an error happens later in the function, the deferred close will still run.

Functions as Values

In Go, functions are first-class values. You can assign them to variables and pass them to other functions:

package main

import "fmt"

func main() {
    // Assign a function to a variable
    double := func(n int) int {
        return n * 2
    }

    fmt.Println(double(5))  // 10
    fmt.Println(double(12)) // 24

    // Pass a function as a parameter
    result := apply(10, double)
    fmt.Println(result) // 20

    // Use an anonymous function inline
    result = apply(10, func(n int) int {
        return n * n
    })
    fmt.Println(result) // 100
}

func apply(value int, fn func(int) int) int {
    return fn(value)
}

Output:

10
24
20
100

Functions as values are useful for callbacks, strategies, and higher-order programming patterns.

Closures

A closure is a function that captures variables from its surrounding scope:

package main

import "fmt"

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

    // A new counter is independent
    otherCounter := makeCounter()
    fmt.Println(otherCounter()) // 1
}

Output:

1
2
3
1

The inner function “closes over” the count variable. Each call to makeCounter creates a new, independent counter.

A Complete Example

Here is a program that combines everything from this tutorial:

package main

import (
    "errors"
    "fmt"
    "strings"
)

// parseFullName splits a name and validates it
func parseFullName(fullName string) (string, string, error) {
    fullName = strings.TrimSpace(fullName)
    if fullName == "" {
        return "", "", errors.New("name cannot be empty")
    }

    parts := strings.Fields(fullName)
    if len(parts) < 2 {
        return "", "", fmt.Errorf("expected first and last name, got: %q", fullName)
    }

    return parts[0], parts[len(parts)-1], nil
}

// formatNames formats a list of names with a title
func formatNames(title string, names ...string) string {
    var builder strings.Builder
    builder.WriteString(title + ":\n")
    for i, name := range names {
        builder.WriteString(fmt.Sprintf("  %d. %s\n", i+1, name))
    }
    return builder.String()
}

// processName wraps parseFullName with logging
func processName(fullName string) {
    defer fmt.Println("---")

    first, last, err := parseFullName(fullName)
    if err != nil {
        fmt.Printf("Error processing %q: %s\n", fullName, err)
        return
    }

    fmt.Printf("First: %s, Last: %s\n", first, last)
}

func main() {
    fmt.Println("=== GO-4: Functions and Error Handling ===")
    fmt.Println()

    // Process names — some valid, some invalid
    processName("Alex Smith")
    processName("Jordan Lee")
    processName("")
    processName("Sam")

    fmt.Println()

    // Variadic function
    output := formatNames("Team Members", "Alex", "Sam", "Jordan", "Taylor")
    fmt.Print(output)

    // Closure: create a greeting function
    makeGreeter := func(greeting string) func(string) string {
        return func(name string) string {
            return fmt.Sprintf("%s, %s!", greeting, name)
        }
    }

    hello := makeGreeter("Hello")
    hi := makeGreeter("Hi")

    fmt.Println(hello("Alex"))
    fmt.Println(hi("Sam"))
}

Output:

=== GO-4: Functions and Error Handling ===

First: Alex, Last: Smith
---
First: Jordan, Last: Lee
---
Error processing "": name cannot be empty
---
Error processing "Sam": expected first and last name, got: "Sam"
---

Team Members:
  1. Alex
  2. Sam
  3. Jordan
  4. Taylor
Hello, Alex!
Hi, Sam!

Common Mistakes

1. Ignoring errors.

// Bad — ignoring the error
result, _ := strconv.Atoi("not a number")
fmt.Println(result) // 0 — no idea something went wrong

// Good — always check errors
result, err := strconv.Atoi("not a number")
if err != nil {
    fmt.Println("Error:", err)
    return
}
fmt.Println(result)

Never ignore errors in Go. If a function returns an error, check it. This is the most important rule in Go programming.

2. Using defer in a loop.

// Be careful — all defers run when the function exits, not when the loop ends
for _, filename := range files {
    f, err := os.Open(filename)
    if err != nil {
        continue
    }
    defer f.Close() // All files stay open until main() returns!
}

If you need to close resources inside a loop, use a separate function or close them manually.

3. Forgetting that deferred functions see the final values.

func main() {
    x := 0
    defer fmt.Println("x =", x) // Captures x = 0 at defer time
    x = 42
}
// Output: x = 0 (not 42!)

The arguments to a deferred function are evaluated when the defer statement runs, not when the function executes.

Source Code

You can find the complete source code for this tutorial on GitHub:

GO-4 Source Code on GitHub

What’s Next?

In the next tutorial, Go Tutorial #5: Control Flow — if, switch, for, you will learn:

  • if/else with short statements
  • switch — Go’s clean alternative to long if-else chains
  • for — the only loop keyword in Go
  • for range for iterating over collections
  • break, continue, and labels

This is part 4 of the Go Tutorial series. Follow along to learn Go from scratch.