---
title: Functions
source: 'https://academia.sh/en/courses/go-fundamentals/functions'
course: 'Go Fundamentals'
language: en
updated: '2026-09-13T05:08:45+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Measures which zero value an untouched named return value carries'
    - 'Compiles proof that separates an unwritten return value from an unused one'
    - 'Runs proof of the boundary between fully ignoring a return value and capturing it into a variable and using only part of it'
---

# Functions

A named return value left completely untouched carries its type's zero value; a function never writing a return value at all is rejected at compile time, but a caller ignoring what comes back without ever catching it says nothing at all — since error is itself just a return value in Go, this open behavior is the course's most important example. Limiting measurement: once a return value is captured into a variable and left unused, this openness closes and the unused-variable rejection takes over.

The previous lesson measured the "early return without else" form but never once looked at
what the function actually returned. This lesson looks at that value: what happens when a
function returns more than one value and one of them is left unwritten, what value does a
named return carry when it is left completely untouched, and what does the language say when a
caller never catches a returned value at all?

The Programming Foundations course (M01/K02) built function definition using Python; there, a
function returned a single value or none at all, and more than one value was packed into a
tuple and passed through a single return slot. **That concept is not rebuilt here** — one
sentence is enough to state the difference: in Go, multiple return is part of the **signature
itself**; it is not packed into a tuple, each value is written separately with its own type.
This lesson measures what these separate slots carry when they are left unwritten.

## Multiple Return Values and the Zero of a Named Return

In Go, a function can return more than one value, and these values can be **named** — the
names written in the signature can be used as ordinary variables inside the function's body.
The `divide` function shows this: `result` and `err` are named in the `func` line, and the
body returns them with a bare `return`, never writing `return result, err`.

The `namedReturn` function measured in the Zero Values lesson already showed this behavior in
a single line: `func namedReturn() (result int, err error) { return }` returned two zero
values without running any body at all. This lesson measures the same mechanism in a function
whose body really does run and branches based on a condition — `divide` can take two different
paths, and how much of the named return is touched changes on each path.

```go
// function.go — multiple return values and a named return returning zero when unwritten
package main

import "fmt"

func divide(a, b int) (result int, err error) {
	if b == 0 {
		err = fmt.Errorf("division by zero")
		return
	}
	result = a / b
	return
}

func main() {
	r1, e1 := divide(10, 2)
	fmt.Printf("%-24s result=%v err=%v\n", "successful division", r1, e1)

	r2, e2 := divide(10, 0)
	fmt.Printf("%-24s result=%v err=%v\n", "division by zero", r2, e2)

	divide(10, 0)
	fmt.Printf("%-24s %s\n", "ignored call", "no panic, program continues")
}
```

```
successful division      result=5 err=<nil>
division by zero         result=0 err=division by zero
ignored call             no panic, program continues
```

**LB20.** In the `b == 0` branch, only `err` is assigned a value; `result` is never touched at
all. The printed `result=0` comes out even though the source has no line anywhere reading
`result = 0` — `int`'s zero value is a direct extension of the frame measured in the Zero
Values lesson. A named return value behaves, from the moment the function starts, like a local
variable **pre-initialized** with its own type's zero value; if the body never touches it, what
it returns is that initial value.

This initialization is refreshed **every single time** the function is called. `divide(10, 2)`
and `divide(10, 0)` are two separate calls to the same function, and in both `result` starts
from zero; in the first call it is assigned `5`, in the second it is never assigned and stays
zero. The previous call's `result` has no effect at all on the next call — every call starts
its named returns fresh, with their own zero values.

The second line makes this even clearer: because `err` is assigned, `err=division by zero`
appears, yet `result` is still `0`. Both values return with the same `return` statement, but
only one was written by hand in the source. A named return opens a way to write **some** of a
multiple return value's values and leave the rest at their zero value — even though two names
appear in the signature, the body is not required to touch both.

The bare `return` (instead of `return result, err`) here is more than shorthand. In an
unnamed multiple return, every `return` statement has to list the values to be returned
explicitly; once named, it becomes possible to return the function's current state as is,
without writing the names twice in the source. This shows that `result` and `err` behave, for
the whole function, like **local, not package-level**, variables — even though they are
declared in the signature, they can be read and changed anywhere in the body.

Naming also has a documentation side. Had `func divide(a, b int) (int, error)` been written,
the signature would only declare two types; understanding which value carries the result and
which carries the error would require reading the body. Writing the names `result` and `err`
in the signature gives that information without going down into the body — the signature says,
on its own, which return slot carries what. This is why named returns work in exactly the
opposite direction from short declaration: short declaration **removed** the type name; a
named return **adds** a name of meaning.

This documentation gain should be read together with naming being **not mandatory**.
`func divide(a, b int) (int, error)` is also a valid signature and returns exactly the same two
values in exactly the same way; naming is chosen only for readability, it does not change the
behavior. Whether a function needs a named return does not depend on how many values it
returns, it depends on how clearly the meaning of those values already reads from the
signature.

## An Unwritten Return Value Is Rejected, an Unused One Is Not

One of the four rejections measured in the first lesson was the return value being **left
unwritten**: the body of `func F() int { }` ended without ever producing a value, and the
compiler rejected it. This lesson measures the exact opposite of that rejection: is the
**caller** never using the return value rejected the same way? Intuition might say "no" — a
call being made and its result not used is, in many contexts, entirely ordinary (a log call's
return value is usually unimportant). This lesson measures the real behavior, not the
intuition.

The third line answers this by running it. The call `divide(10, 0)` is written on its own, as
a single expression, without being assigned to anything — both return values (the result and
the error) are silently dropped. The program does not panic, the compiler says nothing at all;
execution continues without interruption.

**LB21.** No variable at all is defined on this line — only a call is made. The
"unused variable" rejection measured in the first lesson applies to a name that is defined and
never read; here no name is defined at all, so that rule never comes into play. The two
returned values vanish, along with the call itself, without ever being written anywhere.

Read together with the four rejections in the first lesson, this reveals a striking asymmetry.
A body like `func F() int { }` was rejected because it did not **write** its return value — the
compiler would not accept a body that fails to keep the promise its signature made. But the
call `divide(10, 0)` **produces** its return value in full — the function keeps its promise,
both values really are computed — only the caller never asks for these produced values at all.
The compiler's concern stops at production itself; what happens to a produced value afterward
is a matter that lies outside the function's boundary.

This is the most important example so far of the course's **open** class, because in Go an
error is an ordinary **return value** — there is no separate mechanism, no exception type, no
mandatory catch construct. If a function returns an `error`, whether to read that value or not
is entirely the caller's decision. The language does not require, **in any way**, that a
returned error be checked; the error returned by `divide(10, 0)` vanishes as silently as an
event that was never logged.

This behavior leaves no trace in the source either. The third line's output ("no panic,
program continues") confirms this: the "division by zero" error really is produced, but since
it is never written anywhere, it has no effect at all — no panic, no log line, no change in
exit code. Someone reading the source cannot even tell from the `divide(10, 0)` line whether
that call produced an error at all; understanding that requires reading the function's
**definition**. This is the shared trait of open behavior: seeing the result is not enough from
the call line, the definition itself has to be read.

## Limiting Measurement: What Is Ignored Does Not Always Stay Open

This openness has a limit. Ignoring returned values by **never catching them at all** is free,
but capturing them into a variable and then not using them is not free. To measure this, we
return to the first lesson's compiler call and call `divide` in two separate pieces of source —
one never catching the results, the other catching them and using only one.

```go
// compiler.go — helper that calls the compiler from inside the program
package main

import (
	"os"
	"os/exec"
	"path/filepath"
)

func compiles(source string) bool {
	dir, err := os.MkdirTemp("", "trial")
	if err != nil {
		return false
	}
	defer os.RemoveAll(dir)
	os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module trial\n\ngo 1.24\n"), 0o644)
	os.WriteFile(filepath.Join(dir, "code.go"), []byte(source), 0o644)
	cmd := exec.Command("go", "build", "./...")
	cmd.Dir = dir
	cmd.Env = append(os.Environ(), "GOTOOLCHAIN=local")
	return cmd.Run() == nil
}

func classify(ok bool) string {
	if ok {
		return "compiled"
	}
	return "not compiled"
}
```

```go
// trial.go — fully ignoring is not the same as capturing and half-using
package main

import "fmt"

func main() {
	fullyIgnored := `package trial
import "fmt"
func divide(a, b int) (result int, err error) {
	if b == 0 {
		err = fmt.Errorf("division by zero")
		return
	}
	result = a / b
	return
}
func F() { divide(10, 2) }`

	halfIgnored := `package trial
import "fmt"
func divide(a, b int) (result int, err error) {
	if b == 0 {
		err = fmt.Errorf("division by zero")
		return
	}
	result = a / b
	return
}
func F() { r, e := divide(10, 2); _ = r }`

	fmt.Printf("%-34s %s\n", "fully ignored", classify(compiles(fullyIgnored)))
	fmt.Printf("%-34s %s\n", "captured and half used", classify(compiles(halfIgnored)))
}
```

```
fully ignored                      compiled
captured and half used             not compiled
```

**LB22.** Both sources call the same `divide` function and ignore the same result (the error);
the only difference is that the second source **captures** the returned values into variables
named `r` and `e`. In the first source no name is defined at all, so the unused-variable
rejection never comes into play. In the second source, `e` is defined and never read — `_ = r`
only rescues `r`, leaving `e` an unused variable, and compilation is rejected.

This sharpens the boundary of the `open` class. Never catching a return value is a decision the
language can never see at all — no name appears on the call line, there is nothing to check.
But once it is bound to a name, that name falls under the scope of the rule measured in the
first lesson: every written name must either be used or never written in the source at all.
The error itself stays `open` — the language still does not force you to read it — but binding
it to a **name** and then forgetting it runs into a different rule.

This distinction also explains why error checking in Go is most often written in the form
`result, err := divide(...)`. The moment `err` is captured into a variable, leaving it unread
stops compilation — writing `if err != nil` is not a style preference, it is the most natural
way to count the name `err` as **used**. Ignoring a return value by never catching it at all is
still possible, but once it is caught, ignoring it is no longer possible.

This also carries a signal for someone reading the source: if a call's result was never
caught, that call's error was certainly never checked — but if a result was caught, it is also
certain that result was used **at least enough to compile**. Using a value does not mean
checking it — `_ = e` could also have been written and `e` never read, and would still count as
"used" — but it at least shows the name stood in the source for a purpose. This is a third use
of the blank identifier measured in the first lesson: writing `_ = e` is the written form of "I
know this error but I am deliberately not checking it." The unused-variable rejection does not
force checking an error — it only refuses to let it be **named and forgotten**.

The difference looks small but matters in practice: finding which errors were never caught at
all requires scanning the call lines across a codebase (the compiler leaves no trace here at
all), but finding which caught errors went unchecked is somewhat easier — at least a deliberate
disregard like `_ = err` can be searched for. The compiler cannot see the first group at all,
and sees the second group only through the question "was it unused," never through "was it
checked."

This lesson's three measurements — the zero of a named return, the rejection of an unwritten
return, the openness of an unused return — show all three of the course's classes on a single
function. `divide` itself never changes; what changes is **how we call it**. The same function
carries a defined zero value when called with a named return, stops compilation when left
without a signature, and disappears silently when left uncaught at the call site. For the first
time in this lesson, what stands in for the unwritten depends not on the function itself but on
**how it is called**.

Reading the three measurements together shows Go's function design repeating the same
principle three times: **production** is mandatory, **consumption** is not. A function must
produce every value its signature promises — either by writing it by hand or relying on a
named return's zero. But the caller **taking** that produced value is never something the
language forces. Only once it is taken does that taken thing also need to serve a purpose in
the source.

## Summary

- If the body never touches a named return value, that value carries its type's zero value —
  `result=0` appears even though the source never writes `result = 0` anywhere.
- A return value being left entirely unwritten (`func F() int { }`) is rejected at compile
  time.
- A caller ignoring returned values without ever catching them is not rejected in any way —
  since error is a return value in Go, this is the course's most important example of open
  behavior.
- Once returned values are captured into a variable, the situation changes: the unused-variable
  rejection takes over, and a half-used capture no longer compiles.
- The error itself always stays open; binding it to a name and then forgetting it runs into a
  separate, different rule.
- The function design's principle is single: a function must produce every value it promises,
  but the caller taking that value is never something the language forces.

## Next Step

In this lesson the `divide` function was always called with two fixed arguments, and what was
measured was the unwritten cases on the return side — there was no ambiguity at all on the
argument side, the signature said exactly how many arguments it wanted. But some functions do
not know in advance how many arguments they will take — the caller can pass zero, one, or
twenty values. The next lesson, the last lesson of this topic, measures that ambiguity: what
happens to a variadic parameter when it is given no argument at all, and what relationship does
this lesson's "name of a captured value" have with a slice itself?
