Lesson 3 • Beginner
Functions and Methods 🐹
By the end of this lesson you'll write Go functions that take typed parameters, return one or many values, accept any number of arguments, capture state as closures, and schedule cleanup with defer — the everyday building blocks of every Go program.
What You'll Learn in This Lesson
- Declare a func with typed parameters and a return type
- Return multiple values and handle the (result, error) pattern
- Use named return values and naked returns cleanly
- Accept any number of arguments with a variadic ...int
- Treat functions as values and build closures that keep state
- Schedule cleanup that always runs with defer (and its LIFO order)
:=, basic types, and fmt.Println. Every block below is genuine Go: paste it into the free Go Playground (no install needed) or run it locally with Go from go.dev, and check your result against the listed output.1️⃣ Function Basics: Parameters and Return Type
A function packages code you can reuse by name. The header is func name(parameter type) returnType — you state the type of each parameter and the type it gives back. When neighbouring parameters share a type you write it once (a, b int). A function with no return type simply does work and hands nothing back. Read this worked example and run it first.
package main
import "fmt"
// A function = a named, reusable block of code.
// Shape: func name(parameter type) returnType { ... }
func greet(name string) string {
return "Hello, " + name + "!" // the value handed back to the caller
}
// When parameters share a type you can list it once: a and b are both int.
func add(a, b int) int {
return a + b
}
// A function with no return type just does work; it returns nothing.
func sayHi() {
fmt.Println("Hi there!")
}
func main() {
message := greet("Alice") // capture the returned string
fmt.Println(message) // Hello, Alice!
fmt.Println("add(3, 7) =", add(3, 7)) // add(3, 7) = 10
sayHi() // Hi there!
}Hello, Alice!
add(3, 7) = 10
Hi there!Your turn. The function header below is missing its parameter type, its return type, and the value it returns. Fill in the three ___ blanks using the hints, then run it.
package main
import "fmt"
// 🎯 YOUR TURN — fill in the blanks marked with ___
// 1) Finish this function: it takes two ints and returns their product.
// The header needs a parameter list and a return type.
func multiply(a, b ___) ___ { // 👉 both are int; it returns an int
return ___ // 👉 a * b
}
func main() {
result := multiply(6, 7)
fmt.Println("6 x 7 =", result)
// ✅ Expected output:
// 6 x 7 = 42
}6 x 7 = 42___ blanks, then run it in the Go Playground and check the output matches.2️⃣ Multiple and Named Return Values
Go functions can return more than one value, and this is the foundation of how Go reports errors: a function returns its result plus an error, and you check the error before trusting the result. You can also name the return values in the header; then a bare return (a "naked return") hands back those named variables. One rule the compiler enforces: you must use every returned value, or explicitly discard it with the blank identifier _.
package main
import (
"errors"
"fmt"
)
// Multiple return values — the heart of Go's error handling.
// This returns BOTH a result and an error; the caller checks the error.
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("cannot divide by zero") // error path
}
return a / b, nil // success path: nil means "no error"
}
// Named return values: 'first' and 'second' are declared in the header,
// so a bare "return" hands back their current values (a "naked return").
func swap(a, b string) (first, second string) {
first = b
second = a
return // returns first, second — no need to list them again
}
func main() {
q, err := divide(10, 4)
if err != nil {
fmt.Println("Error:", err)
} else {
fmt.Println("10 / 4 =", q) // 10 / 4 = 2.5
}
// You MUST do something with every return. Use _ to discard one:
_, err = divide(10, 0)
fmt.Println("divide by zero err:", err) // cannot divide by zero
x, y := swap("first", "second")
fmt.Println("swap:", x, y) // swap: second first
}10 / 4 = 2.5
divide by zero err: cannot divide by zero
swap: second firstNow you try. minMax already returns two values — your job is to capture them. Fill in the blanks, using _ where a value isn't needed.
package main
import "fmt"
// minMax returns the smaller AND the larger of two ints.
func minMax(a, b int) (int, int) {
if a < b {
return a, b
}
return b, a
}
func main() {
// 🎯 YOUR TURN — fill in the blanks marked with ___
// 1) Capture BOTH return values into smallest and largest.
___, ___ := minMax(8, 3) // 👉 smallest, largest
fmt.Println("smallest:", smallest)
fmt.Println("largest:", largest)
// 2) You only need the larger one here — discard the first with _
_, biggest := minMax(___, ___) // 👉 any two ints, e.g. 20, 5
fmt.Println("biggest:", biggest)
// ✅ Expected output (for 8,3 then 20,5):
// smallest: 3
// largest: 8
// biggest: 20
}smallest: 3
largest: 8
biggest: 20_. Run it in the Go Playground to check.3️⃣ Variadic Functions: Any Number of Arguments
Sometimes you don't know how many arguments you'll get — think "sum all of these". A variadic parameter, written nums ...int, accepts zero or more values, and inside the function nums is just a slice ([]int) you can range over. If you already have a slice, "spread" it into the call with slice....
package main
import "fmt"
// Variadic function: the ...int means "zero or more ints".
// Inside the function, nums behaves exactly like a []int slice.
func sum(nums ...int) int {
total := 0
for _, n := range nums { // loop over each argument
total += n
}
return total
}
func main() {
fmt.Println(sum()) // 0 (no arguments is allowed)
fmt.Println(sum(5)) // 5
fmt.Println(sum(1, 2, 3)) // 6
fmt.Println(sum(10, 20, 30, 40)) // 100
// Already have a slice? "Spread" it into the function with ...
scores := []int{90, 85, 100}
fmt.Println("total:", sum(scores...)) // total: 275
}0
5
6
100
total: 2754️⃣ Functions as Values and Closures
In Go a function is a first-class value: you can store it in a variable, pass it to another function, and return it from one. A function returned this way can "remember" variables from where it was created — that's a closure. Each closure gets its own private copy of those variables, which survives between calls. This is how you build counters, generators, and configurable behaviour without globals.
package main
import "fmt"
// makeCounter returns a FUNCTION. The returned function "closes over"
// the count variable — it keeps its own private copy that survives
// between calls. This is a closure.
func makeCounter() func() int {
count := 0
return func() int {
count++
return count
}
}
func main() {
// Functions are first-class values: store one in a variable.
double := func(n int) int { return n * 2 }
fmt.Println("double(5) =", double(5)) // double(5) = 10
// Pass a function as an argument.
apply := func(f func(int) int, x int) int { return f(x) }
fmt.Println("apply(double, 8) =", apply(double, 8)) // 16
// Each counter keeps its OWN independent state.
a := makeCounter()
b := makeCounter()
fmt.Println(a(), a(), a()) // 1 2 3
fmt.Println(b()) // 1 (b is separate from a)
}double(5) = 10
apply(double, 8) = 16
1 2 3
15️⃣ defer: Cleanup That Always Runs
defer schedules a function call to run when the surrounding function exits — perfect for cleanup like file.Close() right next to where you opened it, so you can never forget it. Two rules to burn in: deferred calls run in LIFO order (last deferred, first to run), and a defer's arguments are evaluated immediately, even though the call happens later. Watch both in the output below.
package main
import "fmt"
func main() {
// defer schedules a call to run when the surrounding function
// returns. It's perfect for cleanup (closing files, unlocking).
defer fmt.Println("3) cleanup runs LAST, as the function exits")
fmt.Println("1) function body starts")
// Multiple defers run in LIFO order: last deferred, first to run.
for i := 1; i <= 3; i++ {
defer fmt.Println(" deferred loop value:", i)
}
// IMPORTANT: defer evaluates its arguments NOW, runs the call LATER.
x := 10
defer fmt.Println("2) captured x =", x) // captures 10 right here
x = 99 // change after defer...
// ...the deferred line still prints 10, not 99.
fmt.Println("body finished")
}1) function body starts
body finished
2) captured x = 10
deferred loop value: 3
deferred loop value: 2
deferred loop value: 1
3) cleanup runs LAST, as the function exitsCommon Errors (and the fix)
- "declared and not used": you captured a return value but never read it. Either use it, or discard it with the blank identifier —
_, err := divide(10, 0)instead ofresult, err := divide(10, 0)when you don't needresult. - "not enough return values" / "too many return values": your
returndoesn't match the header. If the signature says(float64, error), every return must supply both, e.g.return a / b, nilon success andreturn 0, erron failure. - "cannot use x (type string) as type int": an argument's type doesn't match the parameter type in the header. Go won't auto-convert — pass the right type, e.g.
add(3, 7), notadd("3", 7). - defer printed the "wrong" (old) value: that's by design —
defer fmt.Println(x)capturesx's value at the defer line, not at exit. Wrap it in a closure —defer func() { fmt.Println(x) }()— if you want the value at exit. - "assignment mismatch: 2 variables but minMax returns ... ": the number of variables on the left of
:=must equal the number of values returned. Add a_for each value you skip.
Pro Tips
- 💡 Return the error last: the Go convention is
(result, error), witherroras the final value. Stick to it and your code reads like everyone else's. - 💡 defer right after you acquire: open a file, then immediately
defer f.Close(). The cleanup lives next to the setup, so it can't be forgotten on an early return. - 💡 Keep naked returns short: named returns plus a bare
returnare great for 3-line functions but get hard to follow in long ones — list the values explicitly there. - 💡 Variadic must come last: a
...Tparameter has to be the final one in the list, e.g.func log(prefix string, nums ...int).
📋 Quick Reference
| Pattern | Go Syntax | Result / Note |
|---|---|---|
| Function | func add(a, b int) int | typed params + return type |
| No return | func sayHi() | does work, returns nothing |
| Multi-return | func f() (int, error) | result + error pattern |
| Named return | func s() (n int) { return } | bare return hands back n |
| Discard value | _, err := f() | ignore with blank identifier |
| Variadic | func sum(nums ...int) int | zero or more args (a slice) |
| Spread slice | sum(scores...) | pass a slice as args |
| Function value | double := func(n int) int {...} | store a func in a variable |
| Defer | defer f.Close() | runs at exit, LIFO order |
Frequently Asked Questions
Q: Why does Go use multiple return values instead of exceptions?
Go has no try/catch. Functions that can fail return a result plus an error value, e.g. (float64, error). The caller checks the error explicitly with `if err != nil`, which makes failure paths visible in the code rather than hidden in invisible stack unwinding. It is the single most common Go pattern.
Q: What is the difference between a naked return and a normal return?
A naked return is just the word `return` with nothing after it, used only when the function declares named return values in its header — Go hands back those named variables' current values. A normal return lists the values explicitly, like `return a, b`. Naked returns are fine for very short functions but get confusing in long ones, so most Go code prefers explicit returns.
Q: When exactly does a deferred call run, and in what order?
A deferred call runs when the surrounding function returns (whether normally or via a panic), not when the line is reached. Its arguments are evaluated immediately at the defer statement, but the call itself is delayed. Multiple defers run in LIFO order — last deferred, first executed — which is why cleanup of resources happens in reverse of how they were opened.
Q: Do I have to use every value a function returns?
Yes. Go will not compile if you ignore a returned value by assigning it to a variable you never read. If you genuinely do not need one of the values, assign it to the blank identifier `_`, for example `_, err := divide(10, 0)`. That tells the compiler you are deliberately discarding it.
Mini-Challenge: Stats Helper
No blanks this time — just a brief and an outline. Write the whole stats function yourself, combining a variadic parameter with multiple return values. Run it and check your output against the expected line.
package main
import "fmt"
// 🎯 MINI-CHALLENGE: a stats helper
//
// 1. Write a variadic function stats(nums ...int) (int, int, int)
// that returns the count, the sum, and the max of its arguments.
// (Loop over nums with range; track sum and a "biggest so far".)
// 2. In main, call it as stats(4, 8, 2, 10, 6)
// 3. Capture all three returns and print them, e.g.
// "count=5 sum=30 max=10"
//
// ✅ Expected output (for 4, 8, 2, 10, 6):
// count=5 sum=30 max=10
func main() {
// your code here
}count=5 sum=30 max=10🎉 Lesson Complete!
- ✅ A function is
func name(param type) returnType— typed params, typed return - ✅ Go returns multiple values; the
(result, error)pattern is everywhere - ✅ Named returns let a bare
returnhand back the named variables - ✅ Use every return value, or discard it with the blank identifier
_ - ✅ Variadic
...inttakes any number of args; spread a slice withslice... - ✅ Functions are values; closures keep their own private state
- ✅
deferruns cleanup at exit in LIFO order, capturing arguments immediately - ✅ Next lesson: Structs and Interfaces — Go's approach to data and composition
Sign up for free to track which lessons you've completed and get learning reminders.