Course outline

Go / Basic: Values and Program Contracts / Lesson 3

Functions and Control Flow

Turn calculations into small contracts and justify branches, loops, and deferred cleanup from explicit rules.

About 5 min readContent reviewed
On this page

Lesson goals

  • Explain value parameters and multiple results.
  • Trace conditions, loops, scope, and defer.
  • Recognize Go 1.22 loop-variable semantics.

A function exposes inputs and promises

A function declaration states parameter and result types. Arguments are passed by value: an int parameter is a copy, while copying a slice or pointer value can still give access to shared underlying storage. Describe allowed input, mutation, result meaning, and failure behavior. Multiple results make a value-plus-error contract explicit without using a magic sentinel.

Prefer a clear operation name and a small coherent responsibility. A helper that calculates a share should not secretly read terminal input or terminate the process. Keeping computation separate from presentation lets tests provide known values and inspect errors without controlling an entire interactive session.

Reject invalid inputs before calculation

The first example accepts a nonnegative total and a strictly positive divisor. It returns a descriptive error before division if the domain is invalid. The fixed call satisfies the contract and demonstrates an ordinary successful result. In a larger API, decide whether callers need a stable error category rather than parsing a message.

An if condition is Boolean in Go; integers are not implicit truth values. Short declarations in an if initializer are scoped to that if statement and its branches. Read the scope carefully when a name resembles an outer variable, because a newly declared local can shadow the original.

go A checked multiple-result function
package main

import (
	"errors"
	"fmt"
)

func share(total, people int) (int, error) {
	if total < 0 || people <= 0 {
		return 0, errors.New("invalid share inputs")
	}
	return total / people, nil
}

func main() {
	value, err := share(12, 3)
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println("share", value)
}
Expected output
share 4

The checked inputs allow integer division and produce four with a nil error.

Explain the invariant and the end condition

Go uses for for counted, conditional, and indefinite loops. Before each iteration of a summation loop, state which values already contributed to the accumulator. Check the empty case and the exact upper boundary. break exits the applicable loop or switch, while continue proceeds to the next iteration. An expression switch does not fall through automatically.

The second example sums one through five. Before iteration i, total is the sum of earlier positive integers. Adding i preserves the invariant, and increasing i eventually makes i <= 5 false. These fixed values fit int; an externally supplied large upper limit would require a fresh arithmetic and resource bound.

go A bounded accumulation with a simple invariant
package main

import "fmt"

func main() {
	total := 0
	for i := 1; i <= 5; i++ {
		total += i
	}
	fmt.Println("total", total)
}
Expected output
total 15

The updates add one, two, three, four, and five, giving fifteen.

Know which variable is reused

Under Go 1.22 language semantics, iteration variables declared by the loop have a fresh instance for each iteration. This matters for closures and stored addresses. It does not make variables declared outside the loop fresh: a range assignment using = still assigns to existing variables. Older language versions had different loop-declaration behavior.

Go 1.22 also permits range over an integer, producing values from zero up to the bound minus one for a positive bound. Go 1.23 adds range over iterator functions. These are versioned features; this course uses ordinary counted loops when that avoids an unnecessary dependency on a newer convenience.

Deferred work belongs to the surrounding function

defer schedules a call for the surrounding function’s return. Its arguments are evaluated when the defer statement executes, and deferred calls run in last-in, first-out order. A deferred closure can instead read a variable’s later value, so choose deliberately between capturing a current argument and accessing evolving state.

A defer inside a long loop does not run at the end of each iteration; resources may accumulate until the function returns. Move one iteration’s resource work into a helper or close explicitly when needed. Defer simplifies ownership but does not replace checking operation results or designing a bounded lifetime.

Common errors and fixes

A local result unexpectedly remains unchanged.

Why it happens
A short declaration introduced a shadowing variable.
How to fix it
Use assignment to the intended existing variable or choose a distinct name.
Prevent it next time
Review scopes of if initializers and inner blocks.

Many open resources remain during a loop.

Why it happens
Deferred cleanup waits for the whole function to return.
How to fix it
Use an iteration helper or explicit per-iteration cleanup.
Prevent it next time
Draw the resource lifetime rather than assuming block-end defer.

A closure still shares an iteration value.

Why it happens
The code assigns to an outer variable or uses older language semantics.
How to fix it
Declare per-iteration values and check the module language version.
Prevent it next time
Distinguish := from = and record the go directive.

Check your understanding

1. Change the accumulation to include one through six and justify both the result and termination.

Show worked solution

Use i <= 6. The invariant still says total contains all earlier positive integers. The final update adds six to fifteen, yielding twenty-one. The next i is seven, which fails the condition, so execution ends. Do not generalize the arithmetic safety to arbitrary bounds without checking representable intermediate sums and the allowed runtime.

2. Describe the behavior of share(12, 0), including why its returned number must not be treated as a valid answer.

Show worked solution

The guard returns zero and a non-nil error before evaluating division. The caller must inspect the error and reject the numeric result on failure; zero is a placeholder result, not an accepted share. A test should verify the error condition and confirm that no external state was mutated. The application decides whether to ask for new input or propagate the error.

Key takeaways

  • Function contracts include mutation and failure meaning.
  • A loop needs an invariant, a boundary, and progress toward termination.
  • Loop variable behavior and defer timing follow explicit scope and version rules.

Official references