JavaRush /Courses /Go SELF /Multiple function results

Multiple function results

Go SELF
Level 9, Lesson 2
Available

1. Multiple results in Go

If you came to Go from languages where error information is passed via exceptions, multiple results may initially look like a weird tradition: “why can’t you just return a single value and not distract me with your error?”. But Go is designed so that errors are ordinary values, not some magical “gift from the sky”.

Because of this, we get a simple and honest contract: a function either returns a useful result, or reports that it didn’t work out via error. This idea is very old and is deeply built into the standard library: error is an interface with the method Error() string, meaning any error must be able to explain itself as text.

Imagine that a function is a courier. Sometimes it delivers a package (a result), and sometimes it calls and says: “Sorry, the address wasn’t found” (an error). In Go, the courier brings both the package and the note at the same time—you decide what to do with the note.

Multiple results: syntax without mysticism

When we say “multiple result,” it literally means: a function can return several values with a single return. In the signature, such results are written in parentheses.

First, let’s look at an example that isn’t about errors at all—just to get used to the mechanics:

package main

import "fmt"

func swap(a, b string) (string, string) {
	return b, a
}

func main() {
	left, right := swap("L", "R")
	fmt.Println(left, right) // R L
}

It’s important to internalize two rules here.

First, order matters: whatever is written in the signature (string, string) is what we return in the same order. Second, on the left side of the assignment we also write two identifiers: left, right := swap(...).

How to receive multiple results: a, b := f() and _

In real code, you’ll most often see this form:

a, b := f()

And this is not a “special thing for errors,” it’s just normal language syntax.

Sometimes we don’t need the second result, but the compiler won’t let us “just forget” about it. Then we use the special identifier _ (underscore)—“discard the value intentionally.”

A small example—purely to get acquainted with _:

package main

import "fmt"

func pair() (int, int) {
	return 10, 20
}

func main() {
	x, _ := pair()
	fmt.Println(x) // 10
}

An important note: discarding an error via _ is almost always a bad idea. You can do it, but you’re digging a hole for yourself. Today, we’re learning to do the opposite: take errors carefully and check them.

2. The (value, error) contract in Go

Now we move on to the star of the day: functions that may fail to complete the task.

In Go, there’s a standard form for this: a function returns (T, error), where T is the useful result, and error is either nil (success) or non-nil (failure). The error type itself is an interface that requires the method Error() string so that an error can be printed as text.

The most minimal way to create an error is errors.New("message"). The standard library explicitly demonstrates this approach in examples.

Let’s make a “safe division” function that can say “you can’t divide by zero.”

package main

import (
	"errors"
	"fmt"
)

func safeDiv(a, b int) (int, error) {
	if b == 0 {
		return 0, errors.New("division by zero")
	}
	return a / b, nil
}

func main() {
	q, err := safeDiv(10, 0)
	if err != nil {
		fmt.Println("error:", err) // error: division by zero
		return
	}
	fmt.Println("q =", q)
}

Here a discipline appears: immediately after the call, we check err, and only then use the result.

This isn’t “style for style’s sake.” It’s literally an agreement with your future self: a week from now, you’ll open the code and won’t have to guess where exactly the error happened—everything is nearby and linear.

3. Why, on error, people return the zero value

In the (T, error) contract, there’s a very important tradition: if the error is not nil, then the T value is usually returned as the zero value (the default value).

Why? Because it’s predictable and safe: the caller shouldn’t accidentally use a “junk” value that looks plausible. Zero is immediately suspicious; an empty string is, too; false is, too. Even if you forget (don’t, but still) to check err, the chance of noticing the problem is higher.

Here’s a mini table so you don’t have to guess what a zero value is:

Type Zero value Comment
int
0
Default zero
float64
0.0
Also zero, just “with a dot”
string
""
Empty string
bool
false
False

That’s why in safeDiv, on error we returned 0, errors.New(...), and on success—a/b, nil.

4. Where you already know (value, error) from: an example with strconv.Atoi

Most likely, you’ve already used or seen strconv.Atoi. It converts a string to a number and returns (int, error).

This is classic Go: “it seems like a simple operation, but it might not work.” The string might be "42", or it might be "forty two", and Go doesn’t have to guess.

We can wrap strconv.Atoi in our own function to practice the style:

package main

import (
	"fmt"
	"strconv"
)

func parseInt(s string) (int, error) {
	n, err := strconv.Atoi(s)
	if err != nil {
		return 0, err
	}
	return n, nil
}

func main() {
	n, err := parseInt("12a")
	if err != nil {
		fmt.Println("parse error:", err) // parse error: strconv.Atoi: parsing "12a": invalid syntax
		return
	}
	fmt.Println("n =", n)
}

Notice: we don’t try to “get clever” inside parseInt. We simply say honestly: either it worked, or we return the error upward.

And this is very much in the spirit of Go: errors are values; you can return them, store them, print them, and pass them further along. Go even has a well-known phrase: “Errors are values”—meaning errors are not special magic, but data that we work with using normal code.

5. Early return: making code linear

Now we’ll get to a technique you’ll write constantly: early return on error.

The idea is simple. Instead of: “if there is no error—do the thing; otherwise—in deep nesting handle it”

we write: “if there is an error—exit immediately; what remains is the ‘happy path’ without extra indentation”

Let’s see how this looks with a function that computes the average of two numbers, but receives them as strings:

package main

import (
	"fmt"
	"strconv"
)

func averageFromStrings(a, b string) (float64, error) {
	x, err := strconv.Atoi(a)
	if err != nil {
		return 0, err
	}

	y, err := strconv.Atoi(b)
	if err != nil {
		return 0, err
	}

	return (float64(x) + float64(y)) / 2, nil
}

func main() {
	avg, err := averageFromStrings("10", "20")
	if err != nil {
		fmt.Println("error:", err)
		return
	}
	fmt.Printf("avg = %.1f\n", avg) // avg = 15.0
}

Two important conventions are clearly visible here:

  • We always return (value, nil) on success.
  • We always return (zeroValue, err) on error.

And notice: even if the computation returns float64, on error we return 0—it is automatically treated as 0.0 of the needed type. This is normal and readable.

6. A mental model: “call → check err → use the result”

To make this a reflex, it helps to keep a short execution scenario in mind.

flowchart TD
    A["Called function f()"] --> B{err != nil?}
    B -- yes --> C[Handled the error / return]
    B -- no --> D[Use result value]
    D --> E[Continue working]

In Go, this isn’t just “a nice style.” It helps you avoid hidden bugs where you keep computing with incorrect data.

7. Embedding (value, error) into a console app

Now we’ll carefully “wire” everything into a small application that gradually evolves throughout the course. Let it be a simple console utility CalcBox that reads two numbers and prints their average and the result of division. For now, no complex menus or collections—we haven’t covered those topics yet, and we don’t need them.

We’ll make small functions with clear contracts.

Reading two strings: (a, b, error)

Yes, a function can return even three values: two “value” and one “error”. This is also normal practice if it’s clearer that way.

package main

import (
	"fmt"
)

func readTwoStrings() (string, string, error) {
	var a, b string
	_, err := fmt.Scan(&a, &b)
	if err != nil {
		return "", "", err
	}
	return a, b, nil
}

Here we return two strings and an error. On error—two zero values for strings (empty strings) and the err itself.

Parsing a string into int: (int, error)

Let’s extract parsing separately—this will make the code reusable and readable:

package main

import (
	"strconv"
)

func parseInt(s string) (int, error) {
	n, err := strconv.Atoi(s)
	if err != nil {
		return 0, err
	}
	return n, nil
}

Computing the average: (float64, error)

The average itself is safe, but we need to parse the input:

package main

func averageFromStrings(a, b string) (float64, error) {
	x, err := parseInt(a)
	if err != nil {
		return 0, err
	}

	y, err := parseInt(b)
	if err != nil {
		return 0, err
	}

	return (float64(x) + float64(y)) / 2, nil
}

Safe division: (int, error)

And again—a classic example, because division by zero happens even to good people:

package main

import "errors"

func safeDiv(a, b int) (int, error) {
	if b == 0 {
		return 0, errors.New("division by zero")
	}
	return a / b, nil
}

Putting it all together in main: “happy path” at the bottom

Now the most pleasant part: main becomes a linear story. It almost reads like text: “read → compute → print.”

package main

import (
	"fmt"
)

func main() {
	a, b, err := readTwoStrings()
	if err != nil {
		fmt.Println("input error:", err)
		return
	}

	avg, err := averageFromStrings(a, b)
	if err != nil {
		fmt.Println("avg error:", err)
		return
	}

	x, err := parseInt(a)
	if err != nil {
		fmt.Println("parse error:", err)
		return
	}
	y, err := parseInt(b)
	if err != nil {
		fmt.Println("parse error:", err)
		return
	}

	q, err := safeDiv(x, y)
	if err != nil {
		fmt.Println("div error:", err) // e.g., division by zero
		return
	}

	fmt.Printf("avg = %.2f\n", avg)
	fmt.Println("div =", q)
}

This code looks a bit longer, but it’s very honest. Each step either succeeds or cleanly terminates the program.

And most importantly: now you can extract one more step—for example, a function readTwoInts() (int, int, error) that internally uses readTwoStrings + parseInt. We’ll do this later when we talk about decomposition more deeply, but even now you can see that the architecture is ready for it.

8. Why error is usually second

Sometimes you want to ask: “why not (error, value)?”. Technically, you can do it that way, but by convention in Go the result comes first and the error comes second. This makes reading code more smooth: you see what is returned as the “main value,” and next to it—the “status.”

Plus, there’s a practical reason: sometimes you want to ignore the result (_, err := ...), and sometimes—on the contrary—you want to ignore the second result. With error as the second value, it more often reads clearer: _ is usually placed in the result position when it’s not needed, but you must not lose the error.

This convention is so entrenched that many standard library APIs are built around it. And in educational examples about error handling in Go, the check err != nil is constantly emphasized as the most normal and expected path.

9. Common mistakes when working with (value, error)

Mistake #1: using the result before checking err.
This is the most common “beginner” failure. The code compiles, sometimes even “works,” and then suddenly you get strange numbers, empty strings, or division by zero in an unexpected place. The habit must be mechanical: called a function—immediately check err != nil right next to it, and only after that work with the result.

Mistake #2: returning a “random” value on error.
Sometimes a beginner thinks: “let it be -1 on error, it’s more convenient.” This breaks predictability. In Go, it’s customary to return the result’s zero value on error: 0, "", false. Then the caller has a clear model: “if err is not nil, the value can’t be trusted.”

Mistake #3: printing the error but continuing execution as if nothing happened.
This often happens in main: you print fmt.Println("error:", err) and move on. This almost always means you’re about to work with incorrect data. If the error is fatal for the current scenario, it’s better to return and finish “cleanly.” That’s exactly why early return is so popular: it saves nerves and makes control flow obvious.

Mistake #4: ignoring err via _ because it “gets in the way.”
Yes, _ exists. But when you write n, _ := strconv.Atoi(s), you are literally saying: “I don’t care that the string might not be a number.” Usually that’s not true—you just want the compiler to stop complaining. At that moment, it’s better to stop and think: what should the program do on invalid input?

Mistake #5: mixing error handling and the “happy path” into one tangle.
When error checks are smeared all over the function, it becomes painful to read: your eyes jump between computations and error messages. Try to keep the style: “check the error immediately after the call,” “early return,” “then—clean computations.” This makes code look linear, not like a labyrinth guarded by a Minotaur made of ifs.

1
Task
Go SELF, level 9, lesson 2
Locked
Two digits
Two digits
1
Task
Go SELF, level 9, lesson 2
Locked
Measurement bounds
Measurement bounds
1
Task
Go SELF, level 9, lesson 2
Locked
Positive input
Positive input
1
Task
Go SELF, level 9, lesson 2
Locked
One operation
One operation
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION