How to Use Named Return Values in Go

Named return values in Go allow you to declare return variable names in the function signature for automatic return and cleaner code.

When the signature tells the story

You are writing a function that calculates the bounding box of a set of points. It needs to return the minimum x, minimum y, maximum x, and maximum y. Without named returns, the signature looks like this:

func BoundingBox(points []Point) (float64, float64, float64, float64)

The caller writes x1, y1, x2, y2 := BoundingBox(pts) and hopes the order matches the documentation. If the order changes, the compiler won't catch the swap because all four types are identical. The code works, but the intent is buried.

Named return values let you put names in the signature. The function becomes:

func BoundingBox(points []Point) (minX, minY, maxX, maxY float64)

Now the signature is self-documenting. The caller sees exactly what each value represents. The names also act as local variables inside the function body, which can reduce boilerplate in certain patterns. Named returns are a Go feature that bridges the gap between concise syntax and clear documentation.

Named returns are just local variables

Named return values are not a special runtime mechanism. They are local variables declared in the function scope. The compiler treats them exactly like any other local variable, with one difference: the return statement without arguments sends their current values back to the caller.

Think of a shipping manifest. The manifest lists labeled slots for items. You fill the slots as you pack the box. When you hand the box to the courier, you don't point to each item; the courier reads the manifest and ships the contents. The named returns are the labeled slots. The function body fills them. The return statement hands the box over.

The variables are initialized to their zero values when the function starts. If you declare (count int, err error), count starts at 0 and err starts at nil. You can assign to them anywhere in the function. If you exit the function with a naked return, the current values are returned.

This design means named returns have no performance cost. They live on the stack just like unnamed locals. The generated machine code is identical to a function that declares locals and returns them explicitly. The feature exists to improve readability and reduce repetition, not to change how the program runs.

Minimal example

Here's the simplest case: a function that returns two values, and the names make the signature self-documenting.

// Divide returns the quotient and remainder of a division.
func Divide(a, b float64) (quotient, remainder float64) {
	// Assign directly to the named variables.
	// The compiler treats quotient and remainder as locals.
	quotient = a / b
	remainder = a % b

	// Naked return sends the current values of quotient and remainder.
	// This is equivalent to: return quotient, remainder
	return
}

The function signature declares quotient and remainder as float64. Inside the body, you assign to them using simple assignment. The return at the end has no arguments. The compiler sees the named returns and generates code to return those variables. The caller receives the values in the order declared.

Named returns also support multiple variables of the same type in a single declaration. You can write (min, max int) instead of (min int, max int). This keeps the signature compact when the types match.

Trust the zero value. If you forget to assign a named return, it returns the zero value for its type. This can be a source of bugs if you expect a non-zero result. Always verify that every code path assigns the values you intend.

How the compiler handles named returns

At compile time, the compiler parses the function signature and creates local variable entries for each named return. These entries are added to the function's symbol table. The compiler generates code to initialize them to zero at the start of the function.

When the compiler encounters a return statement, it checks the arguments. If the return has no arguments, the compiler verifies that the function has named returns. If it does, the compiler generates code to load the named return variables and pass them to the caller. If the function has no named returns, the compiler rejects the program with return with no values in function returning values.

If the return has arguments, the compiler checks that the number and types match the return signature. Named returns are ignored in this case. The arguments override the named returns. This allows you to mix named and explicit returns, though it can be confusing. It is better to be consistent: either use named returns with naked returns, or use unnamed returns with explicit arguments.

The compiler also enforces that named returns are used correctly. If you declare a named return but never assign to it, the compiler may warn about the unused variable, depending on the context. In practice, named returns are almost always assigned, so this warning is rare.

Gofmt handles the formatting of named returns automatically. If the signature is too long for one line, gofmt wraps it neatly. You don't need to worry about indentation or alignment. The tool decides the layout. Most editors run gofmt on save, so the code stays consistent across the team.

Realistic example: reducing error boilerplate

Named returns shine in functions with multiple early returns, especially when error handling is involved. In Go, the pattern if err != nil { return nil, err } is common. Named returns can reduce this repetition.

Here's a function that processes input and returns a result or an error. Named returns keep the signature readable and simplify the error paths.

// ProcessInput validates and transforms input, returning the result or an error.
// Named returns reduce repetition in early return statements.
func ProcessInput(input string) (result string, err error) {
	// result is initialized to "" and err to nil by default.
	// Check for empty input.
	if input == "" {
		// Set the error. result is already "".
		err = fmt.Errorf("input cannot be empty")
		return
	}

	// Simulate processing.
	result = "processed: " + input

	// Check for a condition that might fail.
	if len(input) > 100 {
		// Set the error. result is already set, but we return it anyway.
		// In real code, you might want to clear result on error.
		err = fmt.Errorf("input too long")
		return
	}

	// Success path.
	return
}

The function declares result and err as named returns. The zero values are appropriate: result is an empty string, and err is nil. When an error occurs, you set err and call return. The function returns the current values of result and err. This avoids writing return "", err or return result, err repeatedly.

The convention in Go is to handle errors explicitly. Named returns don't change this rule. The caller still writes if err != nil. The benefit is inside the function: the error paths are shorter and less prone to typos. You set the error variable and return. The compiler ensures the types match.

Be careful with stale state. In the example above, if the input is too long, the function returns a non-empty result along with the error. This can confuse the caller. If you return an error, the result should usually be the zero value or clearly invalid. You can clear result before returning the error, or use a local variable for the result and assign it to the named return only on success.

Named returns are documentation, not magic. They help the reader understand the function, but they don't enforce correctness. You still need to write the logic carefully.

The defer superpower

Named returns enable a powerful pattern with defer. A deferred function can access and modify the named return values. This is useful for error handling, logging, and panic recovery.

Here's how you can use named returns to convert a panic into an error. The deferred function captures the named returns by reference. If a panic occurs, the deferred function recovers it and sets the error.

// SafeProcess demonstrates using named returns to convert a panic into an error.
// The deferred function modifies the named return values.
func SafeProcess() (result string, err error) {
	// The deferred function captures result and err by reference.
	// It runs after the function returns, but before the caller gets the values.
	defer func() {
		// Recover from a panic if one occurred.
		if r := recover(); r != nil {
			// Set the error to describe the panic.
			err = fmt.Errorf("recovered from panic: %v", r)
		}
	}()

	// Simulate a panic.
	panic("something went wrong")

	// This line is unreachable, but the defer runs.
	result = "success"
	return
}

The function declares result and err as named returns. The defer statement registers an anonymous function. This function has access to result and err because they are in the enclosing scope. When the function panics, the deferred function runs. It calls recover() to catch the panic and sets err to an error value. The function then returns. The caller receives result (which is empty) and err (which describes the panic).

This pattern is common in Go for wrapping functions that might panic. It allows you to turn a panic into a recoverable error. Without named returns, you would need to use a local variable for the error and return it explicitly, which is more verbose.

The deferred function runs after the return statement but before the caller receives the values. This means the deferred function can modify the return values. This is a subtle but important detail. The return statement captures the values, but if the return values are named, the deferred function can change them before the caller sees them.

Defer can rewrite your return values. Use that power to catch panics and clean up state.

Pitfalls and traps

Named returns are simple, but they have pitfalls. The most common issue is readability. Naked returns in long functions make it hard to see what is being returned. You have to scroll up to the signature to check the names. This breaks the flow of reading the code.

Another pitfall is stale state. If you assign a value to a named return, then encounter an error, and return without clearing the value, the caller gets a result and an error. This is confusing. The caller might use the result even though an error occurred. Always ensure that error paths return zero values for results, or use local variables to avoid the issue.

Shadowing is a subtle bug. If you declare a local variable with the same name as a named return, it shadows the named return. The named return stays at its zero value. The compiler doesn't catch this as an error, but go vet warns about it.

// BadShadow demonstrates a shadowing bug.
func BadShadow() (x int) {
	// This declares a new local variable x, shadowing the named return.
	x := 10
	// The named return x is still 0.
	return
}

The function declares x as a named return. Inside the body, x := 10 declares a new local variable x. This shadows the named return. The assignment sets the local variable, not the named return. The return statement returns the named return, which is still 0. The caller gets 0, not 10.

The compiler rejects code with obvious errors, but shadowing is a logic bug. The compiler sees the local declaration and allows it. go vet helps here. It warns with shadow: declaration of 'x' shadows declaration. Run go vet regularly to catch these issues.

Another error occurs if you use a naked return in a function without named returns. The compiler rejects this with return with no values in function returning values. This is a hard error. You must provide arguments to the return statement.

Named returns also don't affect visibility. The visibility of the return values is determined by the types, not the names. A function returning (Result, error) exports based on the function name and the types. The names result and err are just names. They don't make the values public or private.

Naked returns are a readability risk. Use them sparingly. Long functions need explicit returns.

Decision: when to use named returns

Named returns are a tool. They help in some situations and hurt in others. Use them when they improve clarity and reduce boilerplate. Avoid them when they make the code harder to read.

Use named returns when the function signature is complex and names improve readability for the caller.

Use named returns when you have a short function with a single return path and want to reduce boilerplate.

Use named returns when you need to return multiple values of the same type and the names distinguish them.

Use named returns when you need defer to modify return values, such as for panic recovery or logging.

Use unnamed returns when the function is long and has multiple return sites to avoid confusion about what is being returned.

Use unnamed returns when the return values are obvious from the context or type, like func (s *Server) Start() error.

Use unnamed returns when you want to prevent the accidental return of stale state via naked returns.

Named returns are for the reader, not the compiler. Choose the style that makes the code easiest to understand.

Where to go next