Functions & Multiple Returns

Go · Lesson 12 of 32 7 min read Updated

A function is a named block of code that does one job. You have already used many: fmt.Println, strconv.Atoi, and your own main. Go functions have one feature that most languages lack: they can return several values at once, which is how Go reports errors without exceptions.

In this lesson you will write functions with parameters and results, return multiple values, accept any number of arguments, treat functions as values, build closures, and clean up with defer.

Defining and calling functions

A function starts with func, then its name, its parameters in brackets (each with a type), and the type of the result. When two parameters in a row share a type, you can write the type once.

Go
package main

import "fmt"

func splitBill(total, people int) (int, int) {
	return total / people, total % people
}

func main() {
	each, extra := splitBill(1150, 4)
	fmt.Println("Each pays:", each)
	fmt.Println("Left over:", extra)
}
Output
Each pays: 287
Left over: 2

splitBill returns two values, listed in brackets after the parameters: (int, int). The caller receives both with each, extra := .... Four friends in Pune each pay ₹287, and ₹2 is left for the tip jar.

If you do not need one of the results, ignore it with the blank identifier: each, _ := splitBill(1150, 4). Go does not allow you to simply drop it, which makes ignored results visible in code reviews.

The (value, error) pattern

The most important use of multiple returns is error handling. A function that can fail returns its normal result plus an error. If everything worked, the error is nil:

Go
package main

import "fmt"

func ticketPrice(class string) (int, error) {
	switch class {
	case "sleeper":
		return 450, nil
	case "ac3":
		return 1200, nil
	}
	return 0, fmt.Errorf("unknown class %q", class)
}

func main() {
	for _, c := range []string{"ac3", "ac5"} {
		price, err := ticketPrice(c)
		if err != nil {
			fmt.Println("Error:", err)
			continue
		}
		fmt.Println(c, "costs", price)
	}
}
Output
ac3 costs 1200
Error: unknown class "ac5"

fmt.Errorf builds an error with a formatted message. The caller checks err != nil straight away and handles the problem before using price. You will see this shape in nearly every Go program; Errors: Wrapping, Is & As covers it in depth.

Named results

You can give the results names. They start at their zero values, act as normal variables inside the function, and a bare return sends back their current values:

Go
package main

import "fmt"

func marksRange(marks []int) (lowest, highest int) {
	lowest, highest = marks[0], marks[0]
	for _, m := range marks {
		lowest = min(lowest, m)
		highest = max(highest, m)
	}
	return
}

func main() {
	lo, hi := marksRange([]int{67, 91, 45, 88})
	fmt.Println("Lowest:", lo, "Highest:", hi)
}
Output
Lowest: 45 Highest: 91

min and max are built-in functions (added in Go 1.21). Named results are good documentation: the signature now says which number is which. Use the bare return only in short functions; in long ones, write return lowest, highest so readers do not have to search for the values.

Variadic functions

A variadic function accepts any number of arguments of one type. Put ... before the type of the last parameter. Inside, the parameter is a slice. To pass an existing slice, add ... after it in the call:

Go
package main

import "fmt"

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

func main() {
	fmt.Println(total())
	fmt.Println(total(40, 25, 60))

	cart := []int{199, 349}
	fmt.Println(total(cart...))
}
Output
0
125
548

fmt.Println itself is variadic, which is why it accepts any number of values.

Functions as values and closures

In Go, functions are values. You can store them in variables, pass them to other functions and return them. A function written without a name is an anonymous function:

Go
package main

import "fmt"

func applyOffer(price int, offer func(int) int) int {
	return offer(price)
}

func tokenMaker(prefix string) func() string {
	n := 0
	return func() string {
		n++
		return fmt.Sprintf("%s%d", prefix, n)
	}
}

func main() {
	festive := func(p int) int { return p * 90 / 100 }
	flatOff := func(p int) int { return p - 50 }
	fmt.Println(applyOffer(1000, festive), applyOffer(1000, flatOff))

	nextToken := tokenMaker("A")
	fmt.Println(nextToken(), nextToken(), nextToken())
}
Output
900 950
A1 A2 A3

The type func(int) int means "a function that takes an int and returns an int". The function returned by tokenMaker is a closure: it remembers the variable n from the function that created it, and each call increases the same n. A clinic in Lucknow could use one maker per counter to hand out tokens A1, A2 and so on.

defer: run something when the function ends

defer schedules a function call to run when the surrounding function returns, however it returns. It is Go's tool for cleanup: closing files, unlocking locks, stopping timers. Deferred calls run in reverse order (last in, first out), and their arguments are evaluated at the moment you write defer.

Go
package main

import "fmt"

func serve(customer string) {
	fmt.Println("Open counter for", customer)
	defer fmt.Println("Close counter for", customer)
	defer fmt.Println("Print receipt for", customer)
	fmt.Println("Serving", customer)
}

func main() {
	serve("Isha")

	x := 1
	defer fmt.Println("deferred x =", x)
	x = 2
	fmt.Println("x =", x)
}
Output
Open counter for Isha
Serving Isha
Print receipt for Isha
Close counter for Isha
x = 2
deferred x = 1

The receipt prints before the counter closes because deferred calls run in reverse. The last line shows 1, not 2, because the argument x was copied when defer ran. A typical real use is f, err := os.Open(name) followed by defer f.Close() right after the error check.

Common mistakes

  • Ignoring the error result. price, _ := ticketPrice(c) hides failures; handle err unless you are sure it cannot happen.
  • Expecting default or keyword arguments. Go has neither, and no function overloading either. Use different function names or an options struct.
  • Deferring inside a long loop. Deferred calls run only when the function returns, so a loop that opens 1,000 files with defer f.Close() keeps them all open until the end. Move the loop body into its own function.
  • Forgetting ... when passing a slice to a variadic function. total(cart) does not compile; write total(cart...).

Key points

  • func name(a, b int) (int, error) declares parameters and results; results can be several values.
  • Return (value, error) and check if err != nil immediately after the call.
  • Named results document what is returned; keep bare return for short functions.
  • ...T makes a variadic parameter; pass a slice with slice....
  • Functions are values; closures capture variables from the enclosing function.
  • defer runs calls in reverse order when the function returns, with arguments fixed at the defer line.

Exercise

Write a function cartSummary(prices ...int) (count, total int, err error) for a grocery app in Rohtak. It returns the number of items and the total, but returns an error if any price is negative. Call it once with 120, 45, 300 and once with 80, -10, printing either the summary or the error.

Show solution
Go
package main

import "fmt"

func cartSummary(prices ...int) (count, total int, err error) {
	for _, p := range prices {
		if p < 0 {
			return 0, 0, fmt.Errorf("invalid price %d", p)
		}
		total += p
	}
	return len(prices), total, nil
}

func main() {
	if n, t, err := cartSummary(120, 45, 300); err != nil {
		fmt.Println("Error:", err)
	} else {
		fmt.Println("Items:", n, "Total:", t)
	}

	if n, t, err := cartSummary(80, -10); err != nil {
		fmt.Println("Error:", err)
	} else {
		fmt.Println("Items:", n, "Total:", t)
	}
}
Output
Items: 3 Total: 465
Error: invalid price -10

The function returns three values: two named counts and an error. The if short statement keeps n, t and err local to each check.

Quick Quiz

1. How does a Go function usually report that something went wrong?

2. What does defer do?

3. How do you pass the slice nums to func sum(n ...int) int?

4. What is a closure?

Learn Go with a mentor

Free lessons get you started. Our Programming & Software Development training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call