JavaRush /Courses /Go SELF /Named return values and return without values

Named return values and return without values

Go SELF
Level 9, Lesson 3
Available

1. Introduction

When you start writing functions, you usually think like this: “OK, I need to return an int—I’ll return an int.” But pretty quickly a more human need appears: you want the function signature to explain the meaning of the returned values by itself. This is especially noticeable in functions with multiple results, where without names it is easy to mix up what is what and end up with working but incorrect code (which is even worse than a compiler error).

In Go, you can not only list the result types, but also give them names right in the signature:

func minMax(a, b int) (min int, max int) {
	// ...
}

And this is not a “comment for aesthetics.” These are real variables that live inside the function.

Named results are variables

Now for an important point that people often skip, and then wonder why a function returns zeros. When you write named results, Go does something like this: upon entering the function, it creates result variables and initializes them to their zero value.

So in the function:

func example() (x int, ok bool) {
	// ...
}

the variables x and ok already exist from the very beginning: x == 0, ok == false. This is similar to how it would look if at the beginning of the function you wrote:

x := 0
ok := false

(only it is not exactly :=, but the meaning is close).

This leads to two consequences. First, you can assign values to these results in the function body like regular variables. Second, Go allows a special return form—return without values.

2. Naked return: return without values

return without values is allowed only when the results are named. And it literally means: “return whatever is currently stored in the result variables.”

It is convenient, but tricky. The convenience is that you can write less, especially if you return many values. The trickiness is that the code reader (including you a week later) may not understand exactly what will be returned on each branch.

Let’s look at an example that is easy to understand:

package main

import "fmt"

func minMax(a, b int) (min int, max int) {
	if a < b {
		min = a
		max = b
		return
	}
	min = b
	max = a
	return
}

func main() {
	lo, hi := minMax(10, 3)
	fmt.Println(lo, hi) // 3 10
}

Here return without values looks acceptable because the function is short, and before each return we explicitly assign min and max.

But let’s be honest: this works as long as the function is small and linear. As soon as it has more conditions, more temporary variables, and more logic, naked return starts turning into a small riddle.

4. Mini-app: a calculator built from functions

To avoid discussing named results in the abstract, we will stick to one simple app that can be extended: a console “two-number calculator.” It reads a, op, b, performs the operation, and prints the result. Today we are not building a perfect calculator for the ages; what matters more is learning to write neat functions.

Let the input format be like this:

10 / 2

First, we will make a function that tries to parse two numbers and an operation. For simplicity, we assume that fmt.Scan has already read three tokens as strings.

5. Names in (T, error): possible, but be careful

Now for a very typical temptation: since Go allows naming results, let’s name (result, error) and use return without values. People do write like that sometimes. But it is important to understand the cost.

Here is an example of a number-parsing function with a strictly useful signature:

package main

import (
	"strconv"
)

func parseInt(s string) (n int, err error) {
	n, err = strconv.Atoi(s)
	return
}

Yes, it works. If strconv.Atoi returned an error, it will end up in err, and n will remain zero (which is exactly consistent with the “zero value on error” convention).

But the question is not “does it work.” The question is is it easy to read. If you see return without values, you need to remember: OK, the results are named; OK, so the current state of n and err will be returned; OK, now I need to find where they were changed.

For a very short function this is fine. But in a longer one, it starts to annoy you like a mosquito at 3 a.m.: it is small, but why is it still here?

Often it is more readable to write a bit longer, but more transparently:

package main

import "strconv"

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

This version is a couple lines longer, but it is “not a riddle”: on error we return 0, on success we return n.

6. When result names genuinely help

There are situations where naming results is useful specifically as documentation in code. Especially when there are several results and you do not want to force the reader to guess what is what.

For example, a function “parse the expression a op b” can return three entities. Without names, the signature looks like this:

func parseExpr(aS, opS, bS string) (int, string, int, error)

It seems fine, but your eye catches: “so, the first int is a, the string is op, the second int is b... I think.”

With names it becomes clearer:

package main

import (
	"errors"
)

func parseExpr(aS, opS, bS string) (a int, op string, b int, err error) {
	if opS != "+" && opS != "-" && opS != "*" && opS != "/" {
		return 0, "", 0, errors.New("unknown operator")
	}
	// next we will parse the numbers (a bit later)
	return
}

But notice: I am already not using return without values as the main style. On error I explicitly returned (0, "", 0, err). This makes the function behavior obvious, even if the results are named.

7. Why naked return is better avoided

When a function gets longer, return without values hurts readability for a simple reason: it hides the main thing—what exactly is being returned. The reader has to “execute the code in their head” and track what values are currently stored in res, err, count, ok, and so on.

There is another unpleasant effect: naked return makes mistakes quieter. You might forget to assign a result on some branch, and the function will calmly return the zero value. And a zero value can look like a “normal result.” For example, 0 can be a valid value. And then you will spend an hour looking for why it is “sometimes zero.”

That is why in a teaching style (and in many production codebases) people often follow a rule: named results can be used for meaning, but return without values should be used only in very short functions where everything is visible at once.

8. Trap #1: shadowing next to named results

Now for the fun part. Named results are very easy to “break” without a single compiler error—simply because of :=.

The scenario is: you have a named result err of type error. Inside the function you write := and accidentally create a new variable err that lives only in the block, while the “real” result err remains nil. Then you do return without values—and return nil, even though there was an error inside the block.

This is not fantasy; it is a typical Go trap: := can create a new variable instead of assigning to an existing one, and if you “miss,” you get shadowing.

Bad example (pay attention!):

package main

import (
	"strconv"
)

func parseIntBad(s string) (n int, err error) {
	if s == "" {
		err := strconv.ErrSyntax // BUG: this is a new err, not the result!
		_ = err
		return // returns n=0, err=nil (which is terrible)
	}
	n, err = strconv.Atoi(s)
	return
}

Here err := ... created a local err, and it did not get into the named result. After return without values, err == nil will fly out.

Fix: use = (assignment), not := (declaration + assignment):

package main

import "strconv"

func parseIntOK(s string) (n int, err error) {
	if s == "" {
		err = strconv.ErrSyntax
		return
	}
	n, err = strconv.Atoi(s)
	return
}

And even better (if we want it fully transparent)—without naked return:

package main

import (
	"errors"
	"strconv"
)

func parseIntClear(s string) (int, error) {
	if s == "" {
		return 0, errors.New("empty number")
	}
	n, err := strconv.Atoi(s)
	if err != nil {
		return 0, err
	}
	return n, nil
}

9. Trap #2: forgot to assign the result

Another story that is especially tricky in math functions. You write:

func abs(x int) (res int) {
	if x < 0 {
		res = -x
		return
	}
	return
}

At a glance everything even looks “elegant.” But here, in the branch x >= 0 you return res, which stayed at zero. That means abs(10) will return 0. The error does not make noise, the compiler does not complain, there are no tests—life is great until the user notices that the calculator is “a bit sad.”

Correct is either to assign in every branch:

package main

func abs(x int) (res int) {
	if x < 0 {
		res = -x
		return
	}
	res = x
	return
}

Or do not use naked return at all:

package main

func abs(x int) int {
	if x < 0 {
		return -x
	}
	return x
}

The second version is easier to read. And it is a great example that “shorter” and “better” are sometimes different things.

10. A practical style for the calculator

Let’s assemble neat pieces into one line of logic.

Let’s make an operation-evaluation function where the result and the error are obvious. The (int, error) contract is asking for it.

package main

import "errors"

func calc(a int, op string, b int) (int, error) {
	if op == "/" && b == 0 {
		return 0, errors.New("division by zero")
	}
	if op == "+" {
		return a + b, nil
	}
	if op == "-" {
		return a - b, nil
	}
	if op == "*" {
		return a * b, nil
	}
	if op == "/" {
		return a / b, nil
	}
	return 0, errors.New("unknown operator")
}

Here we deliberately do not use named results: the function is already transparent, and explicit return statements look better.

Now let’s put together main, which reads three tokens, parses, and calculates:

package main

import (
	"fmt"
)

func main() {
	var aS, opS, bS string
	fmt.Scan(&aS, &opS, &bS)

	a, err := parseIntClear(aS)
	if err != nil {
		fmt.Println("error:", err)
		return
	}
	b, err := parseIntClear(bS)
	if err != nil {
		fmt.Println("error:", err)
		return
	}

	res, err := calc(a, opS, b)
	if err != nil {
		fmt.Println("error:", err)
		return
	}
	fmt.Println("result:", res) // result: 5 (for example)
}

This is not super-pretty yet (we repeated parsing twice), but for the course level it is a fine readable linear flow. The main thing: each function has a clear contract, and there are no “magic return values” anywhere.

A small table: three return styles and their cost

When you choose a style, it helps to compare options directly. The table below is not “the law of Go,” but a handy cheat sheet for your brain:

Style Signature example How we return When it is fine When it is bad
Without named results
func f() (int, error)
return 0, err
almost always almost never
Named results, but explicit return
func f() (n int, err error)
return 0, err
/
return n, nil
when names truly add meaning if there are many names and they do not clarify anything
Names + return without values
func f() (n int, err error)
return
in very short functions in long functions, with branching and shadowing risk

11. Typical mistakes

Mistake #1: using return without values in a long function “because you can.”
The problem does not show up immediately: the code compiles, seems to work, but is hard to read. After a while you start being afraid to change such a function, because it is unclear what values will really be returned on which branches. In real code this often ends with refactoring “backwards”: switching to explicit return x, y.

Mistake #2: forgetting to assign a value to a named result on one of the branches.
The most unpleasant part is that the compiler will not help you here: it assumes that since the result variable exists, you are consciously returning its current value. The outcome is “silent zeros” (or empty strings) that look like legal values and break logic without a crash.

Mistake #3: shadowing because of := next to named results.
This is a classic trap: you think you are assigning to the result err, but in fact you create a new err in the block. Then return returns the old one (usually nil). This type of bug is especially painful because visually the code looks “Go-like,” but breaks quietly. Such situations are generally typical for := and are especially dangerous when you need to update an already existing variable.

Mistake #4: naming results “on autopilot,” without semantic value.
Sometimes the signature ends up with a int, b int, c int as result names—and that explains nothing. Names should either be meaningful (min, max, n, err) or not exist at all. Otherwise you add noise and create a false feeling that “something complicated is going on here.”

Mistake #5: mixing different return styles within one function without a reason.
When in one function some branches do return, some do return x, y, and some do return 0, err, the reader starts doubting: “are we definitely returning the same thing here?” If you use named results, it is most often more readable to stick to explicit return value, err, and leave return without values only for very short, obvious cases.

1
Task
Go SELF, level 9, lesson 3
Locked
Two reference points
Two reference points
1
Task
Go SELF, level 9, lesson 3
Locked
Coordinate absolute value
Coordinate absolute value
1
Task
Go SELF, level 9, lesson 3
Locked
Silent parser
Silent parser
1
Task
Go SELF, level 9, lesson 3
Locked
Command parsing
Command parsing
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION