Skip to main content

Someone wanted to learn this too, so Grasp built them a personal learning path.

You can keep exploring this one or create your own

Functions with Multiple Returns, Named Types, and Variadic Parameters

Hello! In the previous lesson, you worked with slices and maps, including the two-value form of a map lookup. That second value answered a question the first value could not: was the key present? Go functions can return multiple values for the same reason. A result can carry the data you want and information needed to use it correctly.

In this lesson, you’ll write functions with multiple results, give a function signature its own type, and accept a variable number of arguments. You’ll then combine all three in a small program in your existing module. Allow about 40 minutes, including time to run and inspect the code.


Return the information a caller needs

A Go function lists multiple result types in parentheses and returns a value for each position:

func firstAndLast(values []string) (string, string, bool) {
	if len(values) == 0 {
		return "", "", false
	}
	return values[0], values[len(values)-1], true
}

The bool distinguishes an empty input from a valid result that happens to contain empty strings. A caller receives the results in the same order:

first, last, ok := firstAndLast([]string{"new", "active", "done"})
fmt.Println(first, last, ok) // new done true

Watch LucidProgramming’s Multiple Return Values - Go Lang Programming Tutorial: Part 13 for the declaration, call, and assignment syntax. The video also shows _, which discards a result when you genuinely do not need it.

Multiple Return Values - Go Lang Programming Tutorial: Part 13

Watch this short demonstration to see multiple results written in a function signature and received by a caller.

Watch the declaration for the result types and return statement, then the call for assignment order. In the discard example, notice that _ drops only the result in its position.

You might use first, _, ok := firstAndLast(values) if the last element is irrelevant. Do not discard ok when it tells you whether the other result is valid.

The official guide Effective Go explains why returning several values is more than a convenience: an operation can report a useful result alongside an error, rather than hiding failure inside an ordinary result value.

Effective Go

Read the “Multiple return values” and “Named result parameters” subsections of “Functions” to connect the syntax with function design.

In “Functions” → “Multiple return values,” begin at the motivation. Continue through the nextInt example immediately below it, stopping before “Named result parameters.” Notice how its second result tells the caller where to continue reading. Then, in “Named result parameters,” read from the explanation and look at the ReadFull example that follows. Focus on what the result names clarify, not on the details of reading from a file.

A frequent Go signature returns a value and an error. On success, the error is nil; on failure, the caller checks the error before relying on the value. You will study error design in depth later. For now, the important function-writing pattern is:

func requireLabel(s string) (string, error) {
	if s == "" {
		return "", fmt.Errorf("empty label")
	}
	return s, nil
}

Result names are optional. They are especially useful when several results have the same type:

func firstAndLast(values []string) (first, last string, ok bool) {
	if len(values) == 0 {
		return "", "", false
	}
	return values[0], values[len(values)-1], true
}

Named results are variables initialized to their types’ zero values when the function begins. They permit a bare return, which returns their current values. Naming results does not require a bare return, though: the explicit returns above make each outcome easy to see.


Name a function signature

Go functions are values: you can store one in a variable and pass it to another function. A named function type gives a particular signature a meaningful name:

type Labeler func(string) (string, error)

A Labeler accepts a string and returns a string and an error. This is a type definition, not a new function and not a type alias. A function with a matching signature can be used where a Labeler is expected:

func requireLabel(s string) (string, error) {
	if s == "" {
		return "", fmt.Errorf("empty label")
	}
	return s, nil
}

var label Labeler = requireLabel
value, err := label("guest1")

The call label("guest1") uses the stored function. The type name becomes useful when another function accepts interchangeable behavior:

func makeLabel(input string, label Labeler) (string, error) {
	return label(input)
}

Here makeLabel specifies what kind of operation it needs without fixing which function performs it. A function variable’s zero value is nil, so a caller must supply a function before calling it.

Read the “Type definitions” subsection of kinbiko.com’s Function Types in Go for the distinction between defining a function type and merely writing a function. Its subsequent HTTP example is an application you do not need yet.

Function Types in Go · kinbiko.com

Read the “Type definitions” subsection to see how a function signature can become a distinct, named type.

Under “Type definitions,” start at the function-type discussion. Read the following bold sentence about methods, then stop before “The elegance of http.HandlerFunc.” Keep the distinction between a function’s name and a name for its type in mind.


Accept zero or more arguments

A variadic parameter accepts any number of arguments of one element type. It must be the function’s final parameter:

func joinLabels(prefix string, names ...string) string {
	if len(names) == 0 {
		return prefix
	}
	return prefix + strings.Join(names, ", ")
}

You can call this function with no names, one name, or several:

joinLabels("Guests: ")
joinLabels("Guests: ", "guest1")
joinLabels("Guests: ", "guest1", "guest2")

Inside joinLabels, names behaves as a []string. That is why len(names) and strings.Join(names, ", ") work. The diagram separates the parameter name, the ... marker, and the element type.

The declaration `names ...string` accepts a variable number of string arguments; inside the function, `names` is a slice of strings.

If you already have a slice, put ... after it at the call site to supply its elements as the variadic arguments:

guests := []string{"guest1", "guest2"}
message := joinLabels("Guests: ", guests...)

Without ..., guests is one value of type []string, not a string argument, so that call would not compile. This is the same call-site syntax you can use with the built-in append from the previous lesson.

Read the printing discussion in Effective Go for a real variadic forwarding example. The guide shows both ... in a declaration and ... when passing a slice along to another variadic function.

Effective Go

Read the variadic printing example to distinguish accepting arguments from forwarding an existing slice of them.

In the printing discussion, just after the MyString example, begin at the forwarding example. Stop there, then read the short Min(a ...int) example below the paragraph beginning “By the way” and stop before “Append.” Focus on where ... appears in each function declaration and call; you do not need to adopt the Min function’s behavior for empty input.


Put the three features together

Add a file named functions_demo.go beside main.go in your existing module. This example accepts a variable number of labels, applies a caller-supplied function to each one, and returns either the completed slice or an error:

package main

import (
	"fmt"
	"strings"
)

type Labeler func(string) (string, error)

func normalizeLabel(s string) (string, error) {
	label := strings.ToLower(strings.TrimSpace(s))
	if label == "" {
		return "", fmt.Errorf("empty label")
	}
	return label, nil
}

func collectLabels(label Labeler, names ...string) ([]string, error) {
	result := make([]string, 0, len(names))

	for _, name := range names {
		value, err := label(name)
		if err != nil {
			return nil, err
		}
		result = append(result, value)
	}
	return result, nil
}

func showFunctions() {
	labels, err := collectLabels(normalizeLabel, " Guest1 ", "GUEST2")
	fmt.Println("labels:", labels, "error:", err)

	more := []string{"GUEST3", " Guest4 "}
	labels, err = collectLabels(normalizeLabel, more...)
	fmt.Println("from slice:", labels, "error:", err)

	labels, err = collectLabels(normalizeLabel)
	fmt.Println("no names:", labels, "error:", err)

	labels, err = collectLabels(normalizeLabel, "guest5", " ")
	fmt.Println("invalid input:", labels, "error:", err)
}

Call showFunctions() near the end of your existing main, then run:

go fmt ./...
go test ./...
go run .

Read the example from the caller’s point of view. collectLabels promises two results, so each call receives both. normalizeLabel matches the Labeler signature, so it can be passed as the first argument. The remaining arguments are variadic: the first call supplies individual strings, the second expands a slice, and the third supplies none. With no names, collectLabels returns an empty slice and a nil error.

The last call demonstrates a design choice. After encountering an invalid label, collectLabels returns nil and an error rather than a partially completed result. That keeps its contract straightforward: when the error is nil, the returned slice contains a label for every input.


Takeaways

Multiple return values let a function report both data and the information needed to interpret it. A named function type describes a callable signature you can pass around; it does not itself perform the work. A final ...T parameter accepts zero or more values of type T, appears as a slice inside the function, and can receive an existing slice using slice... at the call site.

Next, you’ll examine pointer and value semantics. That will sharpen your understanding of what happens when functions receive arrays, slices, and other values—and when changes made inside a function remain visible to its caller.

Can't find a good explanation? Sign up and we'll make it for you

Sign up