Chapter 3: Functions and Multiple Returns
The Wednesday morning air carried a hint of cinnamon. Ethan descended the familiar stairs to the archive, this time carrying a white paper bag from Russ & Daughters alongside the coffee tray.
Eleanor looked up and smiled. “Cinnamon raisin?”
“How did you—”
“I’ve been coming to this library since 1987, Ethan. I know the smell of every bakery in lower Manhattan.” She gestured to the chair. “What made you choose bagels today?”
“I figured if we’re learning about functions, we should have something functional for breakfast?” He winced at his own joke.
Eleanor’s laugh was warm and genuine. “That was terrible. I approve. Sit.”
She took a bagel and her coffee, then opened her laptop. “Today we learn about functions—the building blocks of organized code. Tell me, what’s a function?”
“A… piece of code that does something?”
“Close. A function is a named piece of code that takes input, does work, and produces output. Think of it like a machine in a factory. You feed it raw materials, it processes them, and it gives you a finished product.” She typed:
Input → Function → Output
“Let’s write a function together. Here’s what we want it to do—calculate the age someone will be next year:”
Input: 23
Output: Next year you'll be 24
“Simple, right? Now let’s look at the code that does this:”
package main
import "fmt"
func nextYear(age int) int {
return age + 1
}
func main() {
currentAge := 23
futureAge := nextYear(currentAge)
fmt.Println("Next year you'll be", futureAge)
}
Ethan studied the screen. “There are two functions now.”
“Exactly. The main function we know—it’s where execution starts. But now we have nextYear, a function we created.” Eleanor pointed to the first line of nextYear. “Let’s break down the anatomy of a function declaration:”
She wrote in her notebook:
func - keyword that declares a function
nextYear - the name of the function
(age int) - parameter: what it receives as input
int - return type: what it gives back as output
“So func nextYear(age int) int means: here’s a function called nextYear that takes an integer called age and returns an integer.”
“And inside the curly braces?”
“That’s the function body—what the function actually does. In this case, return age + 1 calculates the result and sends it back to whoever called the function.”
Eleanor scrolled to main. “Look at line 9. We call nextYear(currentAge). We’re passing the value 23 to the function. Inside nextYear, that value becomes the parameter age. The function adds 1, returns 24, and we store that in futureAge.”
“So the function is like… a reusable calculation?”
“Precisely. Write it once, use it anywhere. If we needed to calculate next year’s age in fifty places, we’d call nextYear fifty times rather than writing age + 1 fifty times.”
Let’s look at the structure of what we just wrote:
📦 Package: main
📥 Import: "fmt"
🔧 Function: nextYear(age int) → int
📤 Return: age + 1
🔧 Function: main()
📝 Variable: currentAge = 23
📝 Variable: futureAge = nextYear(currentAge)
📞 Call: fmt.Println("Next year you'll be", futureAge)
“See how functions are defined at the same level as main?” Eleanor traced the structure. “They’re peers. main doesn’t own nextYear—they’re both top-level functions in the package.”
Now let’s break this down into plain English:
Package Declaration: This file is part of the 'main' package
Import Statement: We need 'fmt' for printing
Function Definition: nextYear
- Takes: one parameter named 'age' of type int
- Returns: one value of type int
- Does: adds 1 to the age parameter and returns the result
Function Definition: main (entry point)
- Create variable 'currentAge' with value 23
- Call function 'nextYear' with currentAge as argument
- Store the returned value in variable 'futureAge'
- Print "Next year you'll be" followed by futureAge
“Notice the difference between ‘parameter’ and ‘argument,'” Eleanor said. “A parameter is what the function expects to receive—it’s in the function definition. An argument is what you actually pass when calling the function. age is a parameter. currentAge is an argument.”
Ethan nodded slowly. “So parameters are the promise, arguments are the delivery.”
“Beautiful way to think about it.” Eleanor pulled out her desk checking paper. “Let’s trace through this step by step:”
Step | Line | Code | Variables
-----|------|---------------------------------------|--------------------------------
1 | 8 | currentAge := 23 | currentAge = 23
2 | 9 | futureAge := nextYear(currentAge) | currentAge = 23
| | [Call nextYear with argument 23] | [Jump to nextYear function]
3 | 4 | func nextYear(age int) int | age = 23 (parameter receives value)
4 | 5 | return age + 1 | age = 23, returns 24
| | [Return to main, line 9] | [Back to main]
5 | 9 | futureAge := nextYear(currentAge) | currentAge = 23, futureAge = 24
6 | 10 | fmt.Println("Next year...", futureAge)| currentAge = 23, futureAge = 24
| | [Prints: Next year you'll be 24] |
“See how the execution jumps?” Eleanor tapped steps 2 and 3. “We call the function, execution moves to the function, the function does its work, then execution returns with the result. It’s like a detour on a journey—you leave the main road, take a side trip, then come back with what you found.”
Ethan took a bite of his bagel, thinking. “In Python, I’d just write the calculation inline. Why go to the trouble of making a function?”
“For one calculation, you’re right—it’s overkill. But watch what happens when we need more complexity.” Eleanor typed a new example:
package main
import "fmt"
func greet(name string, age int) string {
greeting := "Hello, " + name + "!"
message := fmt.Sprintf("You are %d years old.", age)
return greeting + " " + message
}
func main() {
result := greet("Ethan", 23)
fmt.Println(result)
}
“Now our function takes two parameters—a string and an int—and returns a string. The function body does multiple things: creates a greeting, creates a message with Sprintf (like Println but returns the string instead of printing it), and combines them.”
“So fmt.Sprintf is the string version of Printf?”
“Exactly. The ‘S’ stands for ‘string.’ It uses format verbs like %d for integers and %s for strings.” Eleanor ran the code:
Hello, Ethan! You are 23 years old.
“The function encapsulates the logic of creating a personalized greeting. If we needed this in ten places, we’d call greet ten times. If we needed to change how greetings work, we’d change it in one place.”
She leaned back. “Now, here’s where Go gets interesting. Watch this:”
package main
import "fmt"
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, fmt.Errorf("cannot divide by zero")
}
return a / b, nil
}
func main() {
result, err := divide(10, 2)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("Result:", result)
result2, err2 := divide(10, 0)
if err2 != nil {
fmt.Println("Error:", err2)
return
}
fmt.Println("Result:", result2)
}
Ethan stared. “The function returns… two things?”
“Two things.” Eleanor’s eyes gleamed. “This is one of Go’s most elegant features. A function can return multiple values. In this case, divide returns the result of the division AND an error value.”
“Why?”
“Because division by zero is impossible. In many languages, you’d throw an exception—a special kind of error that disrupts the normal flow of the program. Go doesn’t have exceptions. Instead, functions return errors as ordinary values.”
She pointed to the function signature. “Notice how I wrote (a, b float64) instead of (a float64, b float64)? When parameters share the same type, Go lets you declare the type once. It’s a small convenience.”
Eleanor moved to the return type: (float64, error). “When you see multiple return values in parentheses like this, the function returns all of them. The caller receives them in order.”
“So result, err := divide(10, 2) gets both values?”
“Exactly. The first value goes into result, the second into err. Then we check: if err != nil—if the error is not nil (nil means ‘nothing’ or ‘no error’), we handle it.”
She pointed to the error case: return 0, fmt.Errorf("cannot divide by zero"). “We return 0 for the result because Go requires us to return both values, even though the caller should ignore it when there’s an error. It’s a throwaway value.”
Eleanor ran the code:
Result: 5
Error: cannot divide by zero
“See? The first division succeeds—we get 5 and nil for the error. The second division fails—we get 0 and an error message. No exceptions, no try-catch blocks. Just honest return values that you check.”
“That seems… simpler?”
“It is. In Python or Java, you might have a try-catch block that’s twenty lines away from the code that could fail. In Go, error handling is right there, explicit and visible. You can’t accidentally ignore an error—well, you can, but you have to explicitly ignore it, which makes it obvious.”
Let’s look at the structure of this error-handling pattern:
📦 Package: main
📥 Import: "fmt"
🔧 Function: divide(a float64, b float64) → (float64, error)
❓ If: b == 0
📤 Return: 0, error("cannot divide by zero")
📤 Return: a / b, nil
🔧 Function: main()
📝 Variables: result, err = divide(10, 2)
❓ If: err != nil
📞 Print error and return
📞 Print result
📝 Variables: result2, err2 = divide(10, 0)
❓ If: err2 != nil
📞 Print error and return
📞 Print result2
In plain English:
Function Definition: divide
- Takes: two parameters, 'a' and 'b', both float64
- Returns: two values - a float64 result and an error
- Logic:
1. Check if b is zero
2. If zero: return 0 and an error message
3. If not zero: return the division result and nil (no error)
Main Function:
1. Call divide with 10 and 2
2. Check if an error occurred
3. If error: print it and stop
4. If no error: print the result
5. Call divide with 10 and 0
6. Check if an error occurred
7. If error: print it and stop
8. If no error: print the result
Ethan traced through the logic. “So every time you call a function that might fail, you check the error right away?”
“That’s the Go way. It’s explicit. It’s verbose. But it’s clear. You never wonder if a function might fail—if it returns an error, it might fail, and you handle it.”
Eleanor closed the laptop. “There’s more to functions—variadic parameters, named returns, function types as values—but this is enough for today. You understand the fundamentals: functions take input, produce output, and in Go, they can produce multiple outputs.”
She finished her coffee. “Next time, we’ll talk about collections—arrays and slices. How Go lets you work with groups of data.”
Ethan gathered the empty coffee cups. “Eleanor?”
“Yes?”
“When you said Go doesn’t have exceptions… that seems like a big thing to leave out of a language.”
Eleanor smiled. “Rob Pike said exceptions were the wrong model. They’re invisible control flow—code jumps to a handler somewhere else, and you can’t see it just by reading. Go’s error values are visible. They’re in your face. You can’t miss them.” She paused. “Some people hate it. They say it’s repetitive. But repetition has a virtue—it’s clear. And in programming, clarity saves lives.”
“Saves lives?”
“Go is used in production systems. Cloud infrastructure. Network services. Medical devices. When code fails, the consequences can be severe. Go’s design says: make errors visible. Make them explicit. Don’t let them hide.”
Ethan climbed the stairs, thinking about functions and errors and the philosophy of making things explicit. In Python, he’d caught exceptions when he remembered to. In Go, he’d check errors because the language insisted.
Maybe that was the pattern: Go doesn’t trust you to remember. It makes you state your intentions. And in return, it catches your mistakes before they become disasters.
Key Concepts from Chapter 3
Function declaration: func name(parameters) returnType { body } – defines a reusable piece of code.
Parameters vs arguments: Parameters are what a function expects (in the definition). Arguments are what you pass (when calling).
Parameter type shorthand: When consecutive parameters share the same type, you can declare the type once: (a, b float64) instead of (a float64, b float64).
Return values: Functions can return zero, one, or multiple values. Multiple returns use parentheses: (int, string, error).
Error handling: Go functions return errors as values, not exceptions. Check errors explicitly with if err != nil.
The error type: A special built-in type representing something that went wrong. nil means no error.
Function calls: Calling a function transfers execution to that function, then returns with the result.
Explicit over implicit: Go’s error handling is verbose but clear. You can’t accidentally ignore an error—you must explicitly handle or ignore it.
Next chapter: Arrays and Slices—where Ethan learns how to work with collections of data, and Eleanor explains why Go has two ways to handle lists.
Aaron Rose is a software engineer and technology writer at tech-reader.blog and the author of Think Like a Genius.