Skip to main content
    Courses/Go/Functions and Methods

    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)

    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.

    Worked example: typed parameters and return values
    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!
    }
    Output
    Hello, Alice!
    add(3, 7) = 10
    Hi there!
    This is real code — run it for free atthe Go Playgroundor in your own editor.

    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.

    🎯 Your turn: finish the multiply function
    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
    }
    Output
    6 x 7 = 42
    Fill in the ___ 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 _.

    Worked example: (result, error) and named returns
    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
    }
    Output
    10 / 4 = 2.5
    divide by zero err: cannot divide by zero
    swap: second first
    This is real code — run it for free atthe Go Playgroundor in your own editor.

    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.

    🎯 Your turn: capture two return values
    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
    }
    Output
    smallest: 3
    largest: 8
    biggest: 20
    Capture both values, and discard the one you don't need with _. 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....

    Worked example: variadic sum and slice spreading
    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
    }
    Output
    0
    5
    6
    100
    total: 275
    This is real code — run it for free atthe Go Playgroundor in your own editor.

    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.

    Worked example: function values and closures
    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)
    }
    Output
    double(5) = 10
    apply(double, 8) = 16
    1 2 3
    1
    This is real code — run it for free atthe Go Playgroundor in your own editor.

    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.

    Worked example: defer order and argument capture
    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")
    }
    Output
    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 exits
    This is real code — run it for free atthe Go Playgroundor in your own editor.

    Common 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 of result, err := divide(10, 0) when you don't need result.
    • "not enough return values" / "too many return values": your return doesn't match the header. If the signature says (float64, error), every return must supply both, e.g. return a / b, nil on success and return 0, err on 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), not add("3", 7).
    • defer printed the "wrong" (old) value: that's by design — defer fmt.Println(x) captures x'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), with error as 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 return are great for 3-line functions but get hard to follow in long ones — list the values explicitly there.
    • 💡 Variadic must come last: a ...T parameter has to be the final one in the list, e.g. func log(prefix string, nums ...int).

    📋 Quick Reference

    PatternGo SyntaxResult / Note
    Functionfunc add(a, b int) inttyped params + return type
    No returnfunc sayHi()does work, returns nothing
    Multi-returnfunc f() (int, error)result + error pattern
    Named returnfunc s() (n int) { return }bare return hands back n
    Discard value_, err := f()ignore with blank identifier
    Variadicfunc sum(nums ...int) intzero or more args (a slice)
    Spread slicesum(scores...)pass a slice as args
    Function valuedouble := func(n int) int {...}store a func in a variable
    Deferdefer 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.

    🎯 Mini-Challenge: write stats(nums ...int) (int, int, int)
    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
    }
    Output
    count=5 sum=30 max=10
    Write the function yourself, then run it in the Go Playground and compare with the expected output.

    🎉 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 return hand back the named variables
    • ✅ Use every return value, or discard it with the blank identifier _
    • Variadic ...int takes any number of args; spread a slice with slice...
    • ✅ Functions are values; closures keep their own private state
    • defer runs 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.

    Previous

    Cookie & Privacy Settings

    We use cookies to improve your experience, analyze traffic, and show personalized ads. You can manage your preferences below.

    By clicking "Accept All", you consent to our use of cookies for analytics and personalized advertising. You can customize your preferences or reject non-essential cookies.

    Privacy PolicyTerms of Service

    Install LearnCodingFast

    Learn faster with the app on your home screen.