← Fundamentals
Guided lesson 6 questions · 6 languages

Function calls

Arguments in, a result out, and back to the code that made the call.

A call pauses the code that made it. The function runs with its own new variables, which receive the call’s values, and then the program continues at the place of the call with the function’s result.

Before you begin

Why this matters

Almost every line of a program calls a function. If you can follow a call step by step, you can predict what a program prints and say why a function did, or did not, change your variable.

What this helps you reason about

  • Predict the order in which a call’s pieces run.
  • Tell whether a change made inside a function reaches the code that called it, for a number, a struct, and a slice.
  • Pass a function to another function and know when it runs.

In Go, every argument is copied into the function. This lesson follows one call from start to finish and shows when that copy still shares data with the code that called the function.

Already know this? The short version
  • A call evaluates its arguments, with function calls in them run left to right, then runs the body in a new frame; the caller waits until the call returns and continues at the call with the results.
  • Every argument is copied: an int and a struct are independent copies, while a slice, map, or pointer copy still reaches the caller’s data.
  • Writing slice elements through a parameter is visible to the caller; assigning a new slice or struct to the parameter is not. Pass a pointer, or return the new value.
  • return ends the call and can return several values. Functions are values, and a function literal can be passed and called later.

Go to the final check· Go to the practice tickets

Words in this lesson (7)
argument
A value written in a call, such as 3 in area(3, 4). Each argument is computed before the function’s body starts.
parameter
A name in a function’s definition, such as width in func area(width int, height int). Each call creates its own parameters and gives them copies of the argument values, matched by position.
frame
The record of one function call that has not finished: its parameters and local variables. It is created when the call starts and removed when the call returns.
struct
A value that groups named fields, such as Card{Total: 5}. Assigning or passing a struct copies every field.
slice
A small value with three parts: a pointer to elements stored elsewhere, a length, and a capacity. Copying a slice copies these parts, not the elements.
pointer
A value that holds the address of a variable, written *int for a pointer to an int. &count makes a pointer to count, and *p reads or changes the variable that p points to.
return value
A value a call gives back to the code that called it. A Go function can return several, listed in its signature, such as (int, error).

Examples are focused snippets; imports, enclosing functions, or classes may be omitted. C examples use C11; Haskell examples use Haskell 2010. Expand the notes for language-specific depth.

01 / The call

What happens when a function is called?

A function is a named piece of code. Calling it, as in area(3, 4), runs that code. The values in the parentheses, 3 and 4, are the arguments. The names in the function’s definition, width and height, are its parameters: variables that get the argument values when the call starts.

While area runs, the code that called it waits at the call. When area finishes, the program continues at the place of the call, and the call’s result, 12, is stored in size.

The stepper shows this with a frame list. A frame is the record of one call that has not finished: its parameters and local variables. Calling area adds a frame on top of main’s frame, and returning removes it.

main.go
func area(width int, height int) int {
    result := width * height
    return result
}

func main() {
    size := area(3, 4)
    fmt.Println("after", size) // after 12
}

A picture of one call: which code is running and which names exist. It is a model, not a memory layout.

A conceptual model, not a physical memory layout or live code execution.

1 / 3

Before the call

caller
running
area
not called yet

The caller is running. size does not have a value yet.

A call pauses the caller, runs in its own frame, and comes back to the place of the call.

Precisely: frames are a model

The Go specification says that arguments are evaluated and copied into the called function’s parameters, and that a return continues the caller. Each goroutine has its own stack in the Go runtime, but the compiler decides which values live in a frame and may copy a small function’s body into its caller.

Experiment

Follow a call there and back

Calling area pauses the code that called it. area gets a new frame with its own width and height, set from the arguments 3 and 4. When area returns, its frame is removed, and the caller continues at the place of the call with the result, 12.

Why it matters A call always comes back to the place it was made. Two calls in one line run one after the other, each in its own frame.

Predict which frames the frame list shows while area runs, and whether the caller already has size. Then change the call line to size := area(3, 4) + area(5, 2) and predict: are both area frames ever in the list at the same time?

package main

import "fmt"

func area(width int, height int) int {
    result := width * height
    return result
}

func main() {
    fmt.Println("before")
    size := area(3, 4)
    fmt.Println("after", size)
}
What to look for

Step until the stop inside area. The frame list shows area on top of main, the code that called it. area’s frame has width 3 and height 4. Select main: it does not list size yet, because size gets its value only when area returns. After the return, area’s frame is gone and size shows 12. After the edit, area’s frame appears twice, one call after the other: first with width 3, then with width 5. The two frames are never in the list together, and the run prints after 22.

Expected output

before
after 12

02 / Arguments

When are the arguments computed?

Before a function’s body starts, every argument is computed. Function calls inside the arguments run from left to right: in add(left(), right()), left() runs first, then right(), and only then does add start, with a = 1 and b = 2. An argument is computed even if the body never uses it.

Go has no default parameters. Every call passes a value for every parameter.

main.go
func add(a int, b int) int {
    return a + b
}

// left() runs, then right(), then add
total := add(left(), right())

Arguments are computed before the body, with calls in them running left to right.

Precisely: what the order covers

The specification fixes the order of function calls, method calls, and channel operations inside an expression: left to right. It does not fix when a plain variable is read relative to those calls. In []int{a, f()}, if f changes a, the slice may hold the old a or the new one. A deferred call is a separate case: defer f(x) computes x when the defer line runs, and calls f when the surrounding function returns.

Experiment

Watch the arguments run first

Before add can start, the call needs a value for each parameter. So the program computes each argument first, calling left and then right, and only then starts add with a = 1 and b = 2.

Why it matters An argument is computed even when the function never uses it. If computing it prints, changes something, or takes a long time, that happens before the call.

Predict the order of the three lines printed before 3. Then change return a + b to return a, so add no longer uses b, and predict whether evaluate right is still printed.

package main

import "fmt"

func left() int {
    fmt.Println("evaluate left")
    return 1
}

func right() int {
    fmt.Println("evaluate right")
    return 2
}

func add(a int, b int) int {
    fmt.Println("add runs")
    return a + b
}

func main() {
    fmt.Println(add(left(), right()))
}
What to look for

Watch the output and the frame list together. left gets a frame, prints, and returns; then right does the same. Only then does add’s frame appear, already holding a 1 and b 2. After the edit, evaluate right is still printed and the run ends with 1: every argument is computed before the body starts, whether the body uses it or not.

Expected output

evaluate left
evaluate right
add runs
3

03 / Parameters

What does the function receive?

Arguments are matched to parameters by position: the first argument goes to the first parameter, whatever the variables are called.

Each parameter is a new variable that holds a copy of the argument. As in Variables, what the copy shares depends on the type. A struct is a value that groups named fields, and passing it copies every field, so addBonus changes its own Card. A slice copies its pointer, length, and capacity, so both slices use the same elements, and scores[0] = 15 is seen by the caller.

Assigning a new slice or a new struct to the parameter changes only the parameter. To let a function change the caller’s variable, pass a pointer to it, such as &card.

main.go
func addBonus(card Card, scores []int) {
    card.Total = card.Total + 10
    scores[0] = scores[0] + 10
}

card := Card{Total: 5}
scores := []int{5}
addBonus(card, scores)
fmt.Println(card.Total, scores[0]) // 5 15

Every argument is copied; a slice’s copy still uses the caller’s elements.

Coming from Java: Passing a struct copies it, unlike passing a Java object, which copies a reference. Methods follow the same rule. A Go method names its receiver, the value it is called on, before the method name. func (c Card) addBonus() receives a copy of the Card, so its changes are lost; func (c *Card) addBonus() receives a pointer to the Card, so it can change the caller’s struct.

Coming from Python: There are no keyword arguments or default values. Go code passes a struct of options, or provides a second function for the common case.

Precisely: append through a parameter

append on a slice parameter writes into the caller’s array when there is spare capacity, but the caller’s slice keeps its old length, so the caller does not see the new element. When the capacity is full, append makes new storage that the caller never sees. Return the new slice, as append itself does: s = add(s, v).

Experiment

Pass a struct and a slice

Go copies every argument. A struct is copied field by field, so addBonus changes its own Card. A slice is a small value, a pointer to elements plus a length and a capacity; copying it copies those parts, so both slices use one array of elements. Assigning a new slice to scores changes only addBonus’s scores.

Why it matters Passing a struct gives the function its own copy. To let a function change the caller’s struct, pass a pointer, as in addBonus(&card) with card *Card; or return the new struct.

Predict both printed numbers. Then add the line scores = []int{0} just before scores[0] = scores[0] + 10 and predict again.

package main

import "fmt"

type Card struct {
    Total int
}

func addBonus(card Card, scores []int) {
    card.Total = card.Total + 10
    scores[0] = scores[0] + 10
}

func main() {
    card := Card{Total: 5}
    scores := []int{5}
    addBonus(card, scores)
    fmt.Println(card.Total, scores[0])
}
What to look for

Step into addBonus and select each frame in turn. After card.Total changes, addBonus’s card shows Total 15 while main’s card still shows 5: the struct was copied. After scores[0] changes, both frames show scores as [15]: the copied slice still uses main’s elements. The run prints 5 15. After the edit, addBonus’s scores is a new slice: it shows [0], then [10], main’s scores keeps [5], and the run prints 5 5.

Expected output

5 15

04 / Return

How does the result get back to the code that called the function?

return ends the call at once and gives its values back. A Go function can return several values, listed in its signature. The caller receives them in order, as in fee, ok := entryFee(4).

When the call ends, its frame is removed, and its parameters and local variables can no longer be used by name. Only the returned values reach the caller. A function with no result types returns nothing, and a call to it cannot be used as a value.

main.go
func entryFee(age int) (int, bool) {
    if age < 0 {
        return 0, false
    }
    if age < 5 {
        return 0, true
    }
    return 3, true
}

return ends the call; its values replace the call in the caller’s code.

Precisely: returning a pointer to a local

A function may return &local. Go keeps that variable alive while anything can reach it, as Variables showed. Where it is stored is the subject of Stack and heap in the Runtimes module.

05 / Functions as values

Can a function be passed to another function?

A function is a value, like a number. You can store it in a variable or pass it to another function, which can call it later. In applyTo(4, triple), triple is passed without parentheses, so it is not called yet; applyTo calls it as step(value).

The parameter type func(int) int means “a function that takes an int and returns an int”. func(n int) int { return n + 1 } is a function literal: a function without a name, written where it is used.

A function that uses variables from the code around it is called a closure. Those variables can stay alive after the function that created them has finished; how that works is the subject of Stack and heap in the Runtimes module.

main.go
func applyTo(value int, step func(int) int) int {
    return step(value)
}

func triple(n int) int {
    return n * 3
}

fmt.Println(applyTo(4, triple)) // 12

Without parentheses a function is passed, not called.

Coming from JavaScript: A function literal needs the full func keyword, parameter types, and return type; there is no short arrow syntax.

06 / Try it

Can you follow a fresh call?

Follow the int and the slice separately, and then the returned value. The next lesson, Recursion, uses these two facts: each call gets its own parameters and variables, and a call returns to the code that called it. Its link is at the end of the page.

main.go
package main

import "fmt"

func shift(value int, list []int) int {
    value = value + 1
    list[0] = list[0] + 1
    return value
}

func main() {
    value := 1
    list := []int{1}
    result := shift(value, list)
    fmt.Println(value, list[0], result)
}

What is printed?

Reveal the reasoning

1 2 2. shift’s value is a copy of the caller’s 1, so the caller’s value stays 1. The copied slice uses the same elements, so list[0] becomes 2 for both. The returned value, 2, is stored in result.

Practice tickets

  1. CALL-101 · Charge both rides to the bus pass → Free
  2. CALL-102 · Grow the plant for two weeks → Sign in · Pro
  3. CALL-103 · Number the orders at each coffee cart → Sign in · Pro

Keep exploring

Recursion →

Primary references