Skip to content
beginner Phase 3 · Functions, Defers, and Error Handling

Function Declarations and Multiple Returns

Declare functions with named parameters, multiple results, and variadic arguments.

40m
0 problems
Topic Progress 0%

One Function, Three Answers

You are writing a log parser. Each line contains a host and a port, and the caller needs both pieces. In C you would add an out-parameter and return a status code. In Python you would return a tuple and hope the caller unpacks it in the right order. Go makes the multi-value return part of the language: the result list is parenthesized, and the caller assigns every value.

func minMax(nums []int) (int, int) {
	if len(nums) == 0 {
		return 0, 0
	}
	lo, hi := nums[0], nums[0]
	for _, n := range nums[1:] {
		if n < lo {
			lo = n
		}
		if n > hi {
			hi = n
		}
	}
	return lo, hi
}

lo, hi := minMax([]int{4, 1, 7, 3})
// lo == 1, hi == 7

Three rules govern this syntax. The result list is parenthesized only when it holds more than one value. The caller must assign every result; discarding one requires an explicit underscore: lo, _ := minMax(nums). And the compiler checks the count at compile time, so a call site that assigns the wrong number of values fails to build rather than failing at runtime.

Multiple returns beat a struct when the values are few and the pairing is obvious from the signature. A struct earns its keep when the group is passed around as a unit, carries methods, or has a lifecycle of its own. For a two-value answer to a single question, the parenthesized list is the honest shape.

One idiom appears in almost every Go function you will ever read: the last return value is an error, and nil means success. You will master that convention in The error Interface and Error Messages. For now, notice that multiple returns are what make the idiom readable — the useful value and the failure travel together, and the caller cannot accidentally take one without the other.

Named Results: Documentation You Can Return

Look at the signature func bounds(nums []int) (int, int). Two ints go out. Which is which? The caller has to guess from the call site or read the body. Named results fix that by turning the result list into documentation:

func bounds(nums []int) (min, max int) {
	min, max = nums[0], nums[0]
	for _, n := range nums[1:] {
		if n < min {
			min = n
		}
		if n > max {
			max = n
		}
	}
	return
}

The names are initialized to their zero values when the function starts, so min and max begin at 0. The bare return at the end returns whatever those names currently hold. In a short function like this, the bare return is a wash — return min, max says the same thing. The payoff shows up in longer functions with multiple exit points, where a bare return at the top of an error branch is easier to scan than a three-value tuple.

The danger is shadowing. Named results are function-scoped variables, and a local declaration with the same name creates a different variable that hides them:

func bounds(nums []int) (min, max int) {
	min, max = nums[0], nums[0]
	if min := someOtherValue(); min < 0 { // shadows the named result
		// ...
	}
	return // returns the original min, not the shadowed one
}

The rule the Go community settled on: use named results when the signature needs documentation or the function is long enough that a bare return reads better. Avoid them in short functions where the names add nothing, and never let a local variable share a name with a result. go vet and most linters flag the shadowing pattern, which is exactly why it is worth internalizing now rather than debugging at midnight.

Functions Are Values

In Go a function is a value like any other. You can assign it to a variable, pass it as an argument, and return it from another function. This is not a curiosity — it is the mechanism behind callbacks, middleware, and closures.

func apply(nums []int, f func(int) int) []int {
	out := make([]int, len(nums))
	for i, n := range nums {
		out[i] = f(n)
	}
return out
}

double := func(n int) int { return n * 2 }
fmt.Println(apply([]int{1, 2, 3}, double)) // [2 4 6]

apply knows nothing about doubling. It knows only that it will call f once per element. The caller decides what f does. This is the same decoupling you get from interfaces, but at the level of a single behavior instead of a type — and it is the direct prerequisite for Anonymous Functions and Closures, where functions capture the variables around them.

Functions can also be returned, which is how you build factories:

func multiplier(factor int) func(int) int {
	return func(n int) int { return n * factor }
}

triple := multiplier(3)
fmt.Println(triple(5)) // 15

multiplier takes a value and hands back a function that remembers it. The returned function is a closure: it carries factor with it for as long as anyone holds a reference to the function. Hold that thought — the closures topic builds the full mental model of what "remembers" means there, including the loop-variable trap that catches every Go programmer at least once.

Returning a function is also how you defer a decision. The caller below receives a task it can run now, run later, or hand to a goroutine, without runTask knowing anything about what happens next:

func buildTask(url string) func() error {
	return func() error {
		resp, err := http.Get(url)
		if err != nil {
			return err
		}
		defer resp.Body.Close()
		return checkStatus(resp)
	}
}

task := buildTask("https://api.example.com/health")
if err := task(); err != nil {
	return fmt.Errorf("health check: %w", err)
}

The function value carries its configuration (the URL) and its behavior (the request) as one value. That shape — configure once, invoke anywhere — is what makes function values worth reaching for even before you know the word closure.

Variadic Parameters: The Three Dots

Some functions take a number of arguments you cannot know in advance. A sum of any length. A format call with a list of values. Go handles this with a variadic parameter, declared by putting three dots before the type:

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

sum(1, 2, 3)    // 6
sum()           // 0 — the slice is simply empty

nums := []int{1, 2, 3}
sum(nums...)     // 6 — spread the slice into individual arguments

Two details matter. Inside the function, the variadic parameter is an ordinary slice — you can range over it, index it, pass it to another function. The three dots exist only at the boundary: they collect the arguments on the way in, and they spread a slice on the way out.

The variadic parameter must be the last parameter in the signature. This is why func join(sep string, parts ...string) compiles but func join(parts ...string, sep string) does not — the compiler would have no way to know where the variadic arguments end and the next parameter begins.

A common mistake is passing a slice without the spread:

sum(nums) // compile error: cannot use nums (type []int) as type int

The variadic parameter wants int values, not a slice of them. The ... at the call site is the signal that says "take this slice apart and hand the elements over one by one." When you see a function whose last parameter ends in ...T, read it as "zero or more T values, delivered to you as a slice."

The standard library leans on this constantly — fmt.Sprintf, append, errors.Join, and strings.Join's cousin bytes.Join all take variable arguments — so the signature is the library telling you "I compose zero or more things." One design note for your own APIs: a variadic parameter must sit last, which means a function like func tag(label string, names ...string) is legal while the reversed order is not. And if you need both a fixed set of options and a variable list, pass the options as a struct or a separate slice rather than fighting the ordering rule.

Quiz

1. A function is declared as `func parse(s string) (int, string)`. Which call site is valid?

Question 1 options

2. What does this function return? ```go func f() (a, b int) { a, b = 1, 2 return } ```

Question 2 options

3. Given `func sum(nums ...int) int` and `nums := []int{1, 2, 3}`, what does `sum(nums...)` do?

Question 3 options

4. What does this print? ```go apply := func(f func(int) int, n int) int { return f(n) } fmt.Println(apply(func(x int) int { return x * x }, 5)) ```

Question 4 options

Flashcards

Question

How do you declare a function that returns two ints in Go?

Answer

Use a parenthesized result list: `func f() (int, int)`. The caller must assign both values at the call site, discarding with an underscore if one is unwanted. The compiler enforces the count, so mismatches fail to build.

Question

What does a bare `return` do in a function with named results?

Answer

It returns the current values of the named results. Those names are initialized to zero values at function entry, so a bare return before any assignment returns zeros. Shadowing a named result with a same-named local variable is a classic bug — the bare return still returns the original.

Question

What does the `...` in `func sum(nums ...int)` do, and how do you pass a slice to it?

Answer

It collects any number of trailing int arguments into a slice called nums. Inside the function it is an ordinary slice. To pass an existing slice, spread it at the call site with `sum(nums...)`. The variadic parameter must be the last parameter in the signature.

Question

Can you pass a function as an argument in Go?

Answer

Yes. Functions are values: assign them to variables, pass them as arguments, and return them from other functions. This powers callbacks, middleware, and closures — a function returned from another function can capture and remember the variables of its enclosing scope.

Question

When should you prefer multiple returns over returning a struct?

Answer

When the values are few, the pairing is obvious from the signature, and the group has no shared lifecycle or behavior. Use a struct when the values travel together as a unit, need methods, or form a domain concept worth naming.

Revision Notes

Key Takeaways

  • 1. Multiple returns are first-class: parenthesized result list, caller assigns every value, compiler enforces the count
  • 2. Named results document the signature and enable the bare return; shadowing them with local variables is a classic bug
  • 3. Functions are values: assign them, pass them, return them — the basis for closures and middleware
  • 4. Variadic parameters collect trailing arguments into a slice; spread a slice into the call with `...`
  • 5. The error-as-last-return-value idiom depends on multiple returns and is the subject of the next topic

Interview Tips

  • • "How does Go handle functions that need to return more than one value?" — parenthesized result list, caller assigns all values, and the error-as-last-value idiom keeps the useful value and the failure together
  • • "When would you use named results?" — long functions where a bare return reads better than a tuple; immediately add the caveat that shadowing them with locals is a bug linters catch
  • • "What makes a function variadic and what's the constraint?" — the `...T` parameter must be last; inside the function it is a slice, and you spread slices into the call with `...`

Cheat Sheet

Function Declarations and Multiple Returns

Declaration forms:

Form Use when
func f(a int) int single result
func f() (int, string) multiple results, names add nothing
func f() (n int, err error) names document the signature
func f(nums ...int) variable argument count

Rules:

  • Result list is parenthesized only when it holds more than one value
  • Caller assigns every result; discard with _
  • Variadic parameter must be last; inside the function it is a slice
  • Spread a slice into a variadic call with slice...
  • Bare return returns the current values of named results

Code template:

func splitHostPort(hostport string) (string, int) {
	i := strings.LastIndex(hostport, ":")
	if i < 0 {
		return hostport, 80
	}
	port, err := strconv.Atoi(hostport[i+1:])
	if err != nil {
		return hostport, 80
	}
	return hostport[:i], port
}

Continue Learning

Before You Start

Related Reading