Lesson 6/41 · Go Language Intro & Dev Environment Beginner m01-l06

Functions: Multiple Returns, Named Returns and Variadic Parameters

Module 1: Go Language Intro & Dev Environment | Lesson: m01-l06 | Level: Beginner After this lesson you will be able to: master multiple and named return values in Go, understand when to use variadic functions, and take a first look at function types and closures.

Learning Objectives

  • Declare and call functions that return multiple values correctly.
  • Use named return values to simplify code, and understand how a bare return interacts with defer.
  • Write and use variadic functions, and distinguish variadic parameters from slices.
  • Understand the basics of function types, function signatures and closures.

Core Concepts

6.1 Function Signatures and Multiple Returns

What In Go, a function is the basic unit of code reuse. A function's signature is determined by the types of its parameter list and the types of its return value list; the function name is not part of the signature. This means that if two functions have exactly the same parameter types and return types, they are compatible in the type system even if their names differ (although you cannot redeclare a same-named function in the same scope).

One of Go functions' most distinctive features is support for multiple return values. This differs from many other languages (such as C/C++ and Java), which usually simulate multiple returns by returning a struct or pointer, or by relying on global variables / output parameters.

Why it is designed this way 1. Error handling: Go favors explicit error handling. A function can return a result and an error object together, so the caller can check errors clearly without relying on an exception mechanism. 2. Simplicity: it avoids the boilerplate of creating temporary structs just to return several values.

How When declaring a multi-return function, the return list can be written in parentheses. If there is only one return value, the parentheses can be omitted; if there are several, parentheses are required.

// Syntax example
func functionName(params) (result1 Type1, result2 Type2) {
    // function body
}

6.2 Named Return Values and the Details of return

What Go lets you name the return values in the function signature; these are called named return values.

Why it is designed this way 1. Documentation: the names of the return values are themselves documentation, letting the caller see at a glance what each result means. 2. Simplifying bare return: when a function uses named return values, a return statement can carry no arguments (a so-called bare return), and Go automatically returns the current values of the named return variables.

Key detail: bare return and defer A bare return returns the current values of the named return variables at the point the return statement executes, not a snapshot taken when the function was called. This becomes very powerful when combined with defer: modifying a named return value inside a defer affects the result ultimately returned to the caller.

Note: this behavior was already stable before Go 1.22, but pay attention to the module declaration.

How

func divide(a, b int) (quotient int, err error) {
    if b == 0 {
        err = errors.New("division by zero")
        return // returns the current value of err (already assigned) and the zero value of quotient
    }
    quotient = a / b
    return // returns the current values of quotient and err
}

6.3 Variadic Functions

What Variadic parameters let a function accept zero or more arguments of the same type. In a function signature, a variadic parameter is written as ...Type.

Why it is designed this way When processing an indefinite number of arguments (such as logging or formatted output), variadic parameters offer great flexibility, avoiding the hassle of overloaded functions or passing around slices.

How Inside the function, the variadic parameter is treated as a slice ([]Type). When calling a variadic function with a slice, you must spread it with the ... operator.

Note: the signature of fmt.Printf is func Printf(format string, a ...any) (n int, err error).

6.4 Function Types and an Introduction to Closures

What In Go, functions are first-class citizens. You can define function types and assign functions to variables, pass them as arguments, or return them as results.

A closure is a function value that references variables declared outside its own body. Closures let a function "capture" and access the environment in which it was defined.

When defer and closures are evaluated When a defer statement runs, the function value being deferred and all its arguments are immediately evaluated and saved; what is actually postponed until the function returns is that single call. - If an argument is a value type (such as i), defer saves its value at that moment. - If an argument is a function type (a closure), the closure expression itself is evaluated at the defer point (capturing the variable references as they are then), but the code inside the closure body does not run until the function returns.

Note: since Go 1.22, the for loop variable is fresh on each iteration, which affects how closures capture loop variables. This change takes effect based on the module's go directive: only modules declaring go 1.22+ get the new semantics.

Example Code

Example 1: Multiple Returns, Named Returns and defer Interaction

This example shows how to use named return values and how defer can modify the named return values that are ultimately returned.

package main

import (
    "fmt"
)

// modifyResult demonstrates the interaction between named return values and defer:
// it returns a computed result and a status string.
func modifyResult(x int) (result int, status string) {
    // Named return values are initialized to zero values: result=0, status=""

    // Compute the result
    result = x * 2

    // Set the initial status
    status = "initial"

    // defer runs before the function returns.
    // Note: when the defer statement executes, its arguments (if any) are evaluated,
    // but here we modify the named return value status directly.
    defer func() {
        status = "defer-modified"
    }()

    // The bare return returns the current values of result and status.
    // By now status has already been modified by the anonymous function in defer.
    return
}

func main() {
    res, st := modifyResult(10)
    fmt.Printf("Result: %d, Status: %s\n", res, st)
}

Expected output:

Result: 20, Status: defer-modified

Example 2: Variadic Functions and Function Types

This example shows using a variadic function, spreading a slice, and using functions as types.

package main

import (
    "fmt"
    "strings"
)

// sum demonstrates a variadic function.
// Inside the function, nums has type []int.
func sum(nums ...int) int {
    total := 0
    for _, num := range nums {
        total += num
    }
    return total
}

// applyFunc demonstrates a function type used as a parameter.
// It takes a function f and passes it an int argument.
func applyFunc(f func(int) int, x int) int {
    return f(x)
}

func main() {
    // 1. Variadic call: pass multiple arguments directly
    fmt.Println("Sum of 1, 2, 3:", sum(1, 2, 3))

    // 2. Variadic call: pass a slice.
    // strings.Join takes []string (exactly the internal form of a variadic parameter),
    // so we pass parts directly here, without ...
    parts := []string{"Go", "is", "awesome"}
    fmt.Println("Joined:", strings.Join(parts, " "))

    // 3. Function type: define a function variable
    square := func(x int) int {
        return x * x
    }

    // 4. Pass a function as an argument
    fmt.Println("Square of 5:", applyFunc(square, 5))
}

Expected output:

Sum of 1, 2, 3: 6
Joined: Go is awesome
Square of 5: 25

Common Mistakes & Pitfalls

  1. Forgetting to spread a slice when calling a variadic function

    • Wrong code: go nums := []int{1, 2, 3} sum(nums) // error: cannot use nums (variable of type []int) as int value in argument to sum
    • Why: inside the function the variadic parameter really has type []int, but at the call site sum(nums) is interpreted as passing one argument of type []int, while the element type expected is int — a type mismatch.
    • Correct approach: when passing a slice, you must write sum(nums...) to spread it.
  2. Misunderstanding what a bare return returns

    • Wrong assumption: thinking a bare return returns a snapshot of the named return values from when the function was called (i.e. their zero values).
    • Why: a bare return returns the current values of the named return variables at the point that return statement runs.
    • Correct approach: if you need to return specific values, write return val1, val2 explicitly; if you rely on defer to modify the return values, make sure the defer runs before the return.
  3. Confusing function signatures with function names

    • Wrong assumption: thinking the function name is part of the signature, so two same-named functions with different parameters (overloading) can coexist.
    • Why: Go does not support function overloading. The signature is determined solely by the types of the parameters and return values.
    • Correct approach: if you need functions with different parameters, use different names, or simulate defaults with variadic parameters (Go has no default arguments; you need multiple functions or struct configuration).

Best Practices

  1. Prefer named return values: when a function returns multiple values, named return values improve readability, especially when the meaning of the results is not obvious (e.g. func Read(p []byte) (n int, err error)).
  2. Use bare returns carefully: bare returns are concise but can obscure errors in complex logic. If the return logic is nontrivial, return explicitly so that code review is easier.
  3. Spread slices when calling variadic functions: if you pass a slice to a variadic function, always use .... This is one of the most common sources of compile errors.
  4. Watch out for closure variable capture in loops: before Go 1.22, loop variables were shared across iterations. If you need to capture the current loop variable's value on pre-1.22 versions, pass the variable as a parameter to the closure or make a local copy.

Exercises

  1. Concept question In Go, what makes up a function signature? Is the function name part of the signature?

  2. Coding question Write a function processData that takes a slice of integers and a processing function (func(int) int), and returns a new slice whose elements are each processed by the function.

  3. Advanced/debugging exercise The code below behaves incorrectly on Go 1.21 and earlier (or on a module whose go.mod declares go < 1.22). Find the problem and fix it, so that the output is 0 1 2 3 4 (order may vary by scheduling, but the values must be correct).

    ```go package main

    import ( "fmt" "sync" )

    func main() { var wg sync.WaitGroup for i := 0; i < 5; i++ { wg.Add(1) go func() { defer wg.Done() fmt.Print(i, " ") }() } wg.Wait() } ```

Reference Answers

Reference Answers (click to expand / collapse)

Question 1

Answer: a function signature consists of the types of the parameter list and the types of the return value list. The function name is not part of the signature.

Question 2

Reference code:

package main

import "fmt"

// processData takes a slice of ints and a processing function,
// and returns a new slice with each element transformed.
func processData(nums []int, f func(int) int) []int {
    result := make([]int, len(nums))
    for i, num := range nums {
        result[i] = f(num)
    }
    return result
}

func main() {
    nums := []int{1, 2, 3, 4, 5}
    // Use an anonymous function as the processor, doubling each element
    doubled := processData(nums, func(x int) int {
        return x * 2
    })
    fmt.Println(doubled) // prints: [2 4 6 8 10]
}

Question 3

Problem analysis: In Go 1.21 and earlier, the for loop variable i is shared across the loop body. When the goroutines start, they capture a reference to the variable i, not its value. Because goroutine scheduling is nondeterministic, by the time a goroutine actually runs fmt.Print(i), i may already have become 5 (the post-loop value), so the output is all 5s or otherwise unpredictable. In Go 1.22, each iteration gets a fresh loop variable, so this code runs correctly on a Go 1.22+ module as-is. But to stay compatible with older versions or to make intent explicit, pass the argument explicitly.

Fixed code:

package main

import (
    "fmt"
    "sync"
)

func main() {
    var wg sync.WaitGroup
    for i := 0; i < 5; i++ {
        wg.Add(1)
        // Fix: pass i as an argument to the goroutine,
        // so each goroutine captures the value of the current iteration.
        go func(val int) {
            defer wg.Done()
            fmt.Print(val, " ")
        }(i)
    }
    wg.Wait()
    fmt.Println() // newline
}

(Note: the output order may vary by scheduling, e.g. 0 1 2 3 4, 4 2 3 0 1, etc., but the set of values should be 0 through 4.)

Lesson Summary

  • Go functions support multiple return values, commonly used to return a result and an error.
  • Named return values let you use a bare return, and defer can modify their values.
  • Inside a function, variadic parameters behave as a slice; when calling you must spread the slice with ....
  • Functions are first-class citizens: you can define function types and closures.
  • Since Go 1.22, the for loop variable is fresh on each iteration, but mind the module declaration.

Next lesson preview: arrays, slices and maps — the three containers you will use most often.

Feedback

This site is front-end only — no backend, no accounts — so feedback goes to GitHub Issues, Discussions, or email.

Paste it into the issue or email to help track it down — it never includes anything you typed

Also:Browse existing issues · Email feedback (no GitHub account needed):278975598@qq.com