Functions and Methods

Reviewed & published by Brayan K

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.

Part of the free Go course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

What You'll Learn in This Lesson

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!
}

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
}

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
}

Now you try. minMax already returns two values — your job is to capture them. Fill in the blanks, using _ where a value isn't needed.

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
}

4️⃣ 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)
}

5️⃣ 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")
}

Common Errors (and the fix)

Pro Tips

📋 Quick Reference

PatternGo SyntaxResult / Note
Functiontyped params + return type
No returndoes work, returns nothing
Multi-returnresult + error pattern
Named returnbare return hands back n
Discard valueignore with blank identifier
Variadiczero or more args (a slice)
Spread slicepass a slice as args
Function valuestore a func in a variable
Deferruns at exit, LIFO order

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
}

🎉 Lesson Complete!

Practice quiz

What is the shape of a Go function header?

  • func returnType name(params)
  • def name(params) -> type
  • func name(parameter type) returnType
  • function name(params): type

Answer: func name(parameter type) returnType. Go writes func name(parameter type) returnType, with parameter types after the names.

For func add(a, b int) int, what does the shared type mean?

  • both a and b are int
  • a is int, b is untyped
  • a and b are different types
  • it is a syntax error

Answer: both a and b are int. When neighbouring parameters share a type you list it once; a and b are both int.

By Go convention, the error is which return value?

  • the first
  • the only
  • it is never returned
  • the last

Answer: the last. Fallible functions return (result, error) with error as the final value.

What does a bare return require?

  • no special setup
  • named return values in the header
  • a single return value
  • a deferred call

Answer: named return values in the header. A naked (bare) return hands back the named return variables declared in the header.

What does fmt.Println(sum()) print for a variadic func sum(nums ...int) int?

  • 0
  • nil
  • an error
  • nothing compiles

Answer: 0. Calling a variadic with no arguments is allowed; the empty sum is 0.

How do you spread a slice scores into a variadic call?

  • sum(scores)
  • sum(...scores)
  • sum(scores...)
  • sum(&scores)

Answer: sum(scores...). Append ... after the slice: sum(scores...) passes its elements as the variadic args.

What must you do with every value a function returns?

  • log it
  • use it or discard it with _
  • store it in a global
  • ignore it freely

Answer: use it or discard it with _. Go won't compile if you capture a value you never read; discard with the blank identifier _.

When are a deferred call's arguments evaluated?

  • when the function exits
  • never
  • at the next defer
  • immediately at the defer statement

Answer: immediately at the defer statement. Arguments are evaluated at the defer line; the call itself runs at function exit.

In what order do multiple deferred calls run?

  • FIFO (first deferred, first run)
  • LIFO (last deferred, first run)
  • alphabetical
  • random

Answer: LIFO (last deferred, first run). Defers run in LIFO order — the last deferred call runs first.

A function stored in a variable that remembers surrounding variables is called...

  • a method
  • a goroutine
  • a closure
  • an interface

Answer: a closure. A closure captures variables from where it was created and keeps its own state.

Continue this course

Frequently asked questions

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.

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.

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.

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.

Related lessons