Named Result Parameters

Understand how to declare and use named return parameters in Go functions, including automatic zero initialization, bare returns, and practical patterns with deferred error handling.

Named result parameters let you declare the return variables directly in the function signature. Instead of writing func add(a, b int) int, you write func add(a, b int) (sum int). That sum is a real, accessible variable inside the function body, initialized to its zero value before the function starts, and can be returned with a bare return statement — a return with no arguments.

How Auto-Initialization to Zero Values Works

When a function with named result parameters is called, Go allocates space for those variables on the stack and sets them to their zero value. For an int that’s 0, for a string that’s "", for a bool that’s false, and so on. This happens before the first line of the function body executes. You don’t need to explicitly initialize them; they’re ready to use immediately.

func empty() (x int, s string) {
    return // returns 0 and ""
}
func main() {
    i, str := empty()
    fmt.Printf("i=%d, str=%q\n", i, str) // i=0, str=""
}

The function body does nothing, but the named returns x and s are already zero-valued. The bare return hands those zero values back to the caller. This is a small but significant departure from functions with anonymous returns: you can never return a garbage value because the variable is always initialized. The benefit shows when you use defer to modify return values, which we’ll see shortly.

Zero values are type-dependent:

The zero value for a named return parameter follows the usual Go rules: 0 for numbers, false for booleans, "" for strings, nil for slices/maps/pointers/interfaces/channels/functions. This behavior is identical to declaring a local variable with var inside the function.

Using Named Return Parameters in Practice

Assigning Values and the Bare Return

Assign values to the named return variables anywhere in the function body. A bare return statement — simply the keyword return with no following expressions — returns the current values of those named variables.

func split(sum int) (x, y int) {
    x = sum * 4 / 9
    y = sum - x
    return
}
func main() {
    fmt.Println(split(17)) // 7 10
}

Here, x and y are assigned inside the body, and return without arguments sends their current values back. This is the signature example from the Go Tour. The bare return works because the function signature already declared x and y as the return values. In a very short function like this, the bare return can improve readability by removing redundant variable names after return.

A clean, self-documenting signature:

When the naming directly answers “what does this return?”, the signature becomes a clear contract: (x, y int) tells you this function returns two integers that represent coordinates, dimensions, or — as here — the result of a split calculation. The caller can understand the function without reading the body.

Overriding with an Explicit Return

A bare return isn’t the only option. You can still supply an explicit return expression list. When you do, the explicit values are returned, and the values of the named variables are ignored for that return statement. This doesn’t reset the variables; it simply bypasses them for that one return path.

func getGreeting(short bool) (msg string) {
    msg = "Hello, World!"
    if short {
        return "Hi!" // explicit return, ignores msg
    }
    return // bare return, returns "Hello, World!"
}
func main() {
    fmt.Println(getGreeting(true))  // Hi!
    fmt.Println(getGreeting(false)) // Hello, World!
}

The first return discards the value "Hello, World!" that was stored in msg and returns "Hi!" instead. The second return uses the bare form and returns the current value of msg. Mixing explicit and bare returns in a single function is legal, but it asks the reader to track which path uses which style. In longer functions, that cognitive overhead adds up quickly.

Why Named Return Parameters Exist

Self-Documenting Function Signatures

The most immediate advantage is documentation. When a function returns multiple values of the same type, an anonymous return signature like (int, int) tells the caller nothing about what each integer means. Naming them — (sum, product int) — turns the signature into a specification. Tools like go doc display those names, and an IDE can surface them during auto-completion.

The Go project itself follows a guideline: use named returns on public functions when they make the documentation clearer, but avoid them when they only save a few keystrokes inside the body. You’ll see many standard library signatures like func Open(name string) (file *File, err error). The names file and err act as lightweight in‑line documentation.

Interaction with Deferred Function Calls

Named result parameters unlock a pattern that is impossible with anonymous returns: modifying return values from inside a deferred closure. When a defer statement captures a named return variable, it can alter what the function ultimately returns. This is the foundation for robust error handling and panic recovery.

func divide(a, b int) (result int, err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("recovered from panic: %v", r)
        }
    }()
    if b == 0 {
        panic("division by zero")
    }
    result = a / b
    return
}
func main() {
    res, e := divide(10, 0)
    fmt.Println(res, e) // 0, recovered from panic: division by zero
}

Without named return parameters, the deferred function would have no way to set err before the function returns — the deferred closure runs after the return values have been determined if they’re anonymous. Named parameters change that: the deferred function still has access to the stack locations where the caller expects the results, so modifying err inside the defer modifies the actual return values the caller receives.

Deferred closures execute after return values are set:

When you use anonymous returns and a deferred function, the return values are already evaluated and copied before the defer runs. Named return parameters remain accessible to the defer, which is why they enable this pattern. If you need a defer to influence a return value, name the return you want to influence.

Common Mistakes and How to Avoid Them

Shadowing the Named Return Variable

If you accidentally use a short variable declaration with the same name as a named return parameter inside the body, you create a new local variable that shadows the return parameter. The named return variable remains unchanged, and a bare return will return its original (often zero) value, not the value you just computed.

func multiply(a, b int) (result int) {
    result := a * b  // shadows the outer result
    return           // returns 0, the original result
}

The result created by := is an entirely new variable scoped to the function body. The outer result (the actual return parameter) is untouched. This code compiles without warning and returns 0. To avoid this, assign with = (not :=) when you intend to modify the named return parameter.

Short variable declarations silently shadow:

The Go compiler allows shadowing because it’s legal in a new scope. However, the function body shares the same scope as the named return parameters, so a := with the same name declares a new variable that hides the return parameter. Always use plain assignment = with named returns.

Naked Returns in Long Functions

A bare return in a function that spans more than a handful of lines forces the reader to scan the entire body to know what is being returned. The variable names might be set far from the return point, or reassigned in multiple conditional branches. The mental effort required to verify correctness makes bugs harder to spot.

The Go style guide suggests bare returns only for very short functions — think a few lines at most. In any function where you can’t see the entire body on one screen, prefer an explicit return statement that lists the values.

Bare returns harm readability in non‑trivial functions:

If a function is longer than about five to seven lines, consider using explicit return statements even when you have named return parameters. The small keystroke savings of a bare return is not worth the loss of clarity.

Forgetting to Assign Before a Bare Return

If you leave a named return parameter unassigned and then use a bare return, the caller gets the zero value. This might be intentional, but it’s often a bug where you forgot to set the variable on a particular code path.

func fetchUser(id int) (name string, ok bool) {
    if id < 0 {
        return // name = "", ok = false
    }
    name, ok = dbLookup(id)
    return
}

The early return path forgets to set name and ok, so the caller sees a zero‑value name and false. This may match the intended semantics (“invalid id returns zero values”), but if the caller expects an explicit error, it’s misleading. The best practice is to always assign named return variables on every return path, even if you’re explicitly setting them to their zero values, so the intention is clear.

Unassigned named returns deliver zero values silently:

A bare return with an unassigned named return is equivalent to returning the zero value of that type. If the caller interprets that as “no data,” your code may behave correctly; if the caller expects a valid result, you have a subtle bug. Assign explicitly even on early exit paths.

Real-World Usage Patterns

Named return parameters appear most frequently in public library functions where the signature doubles as documentation, and in functions that use defer to handle cleanup or error post-processing. A classic example is wrapping file operations while ensuring close errors are not lost.

func readConfig(filename string) (data []byte, err error) {
    f, err := os.Open(filename)
    if err != nil {
        return nil, err
    }
    defer func() {
        if closeErr := f.Close(); closeErr != nil && err == nil {
            err = closeErr
        }
    }()
    data, err = io.ReadAll(f)
    return
}

Here, the named err lets the deferred function attach a file close error to the return value, but only if the function hasn’t already encountered an earlier error. This ensures the caller receives the most important error (the one from the operation) while still being informed of a close failure if the operation succeeded.

The data and err names in the signature immediately tell a developer what the function returns without reading the implementation — a pattern you’ll find throughout the standard library and well‑crafted Go packages.

Summary

Named result parameters are a language feature designed for two specific purposes: making function signatures more self‑documenting, and enabling deferred code to influence what gets returned. They are not a shortcut to avoid typing return expressions.

The two behaviors — auto‑initialization to zero and the ability to use bare returns — follow directly from the fact that named returns are ordinary local variables that happen to live at the caller‑expected return positions on the stack. That implementation detail also explains why deferred closures can modify them.

When you name your return parameters, ask whether the names genuinely add clarity to the function’s contract. If they do, the feature is a valuable documentation tool. If they only save a line of code inside the body, prefer anonymous returns with explicit return statements.