Named Return Values
In Go, a function’s return values can be given names right in the signature, turning them into ordinary local variables that are automatically declared, zero-initialized, and returned when you write a bare return statement. Named return values make a function’s purpose clearer at a glance, document what each result represents without a comment, and — combined with defer — provide the standard idiom for rewriting a return value or an error after recovering from a panic. This lesson covers the mechanics, when to reach for them, and the shadowing bug that trips up nearly every Go newcomer at least once.
Overview: How Named Return Values Work
A normal Go function declares only the types of its results: func divide(a, b int) (int, int). A function with named returns additionally gives each result a name, written inside the parentheses just like a parameter: func divide(a, b int) (quotient, remainder int). The moment the function starts executing, quotient and remainder already exist as local variables in scope, initialized to the zero value of their type — 0 for numeric types, "" for strings, false for booleans, and nil for pointers, slices, maps, channels, functions, interfaces, and error values.
Because those variables already exist, you can write a naked return: a bare return statement with no operands. Go fills in the current values of the named result variables automatically. This is purely a convenience and readability feature — under the hood, the compiler generates the exact same code whether you write return quotient, remainder or a naked return after setting those variables; there is no performance difference.
The feature that makes named returns more than cosmetic is how they interact with defer. A deferred function runs after the surrounding function’s return statement has assigned the result variables, but before control actually passes back to the caller. If the return values are named, a deferred closure can read them — and, crucially, write to them — and that write becomes the function’s real, final result. This is the only reliable way for a deferred function to change what gets returned, and it is exactly how Go’s idiomatic panic-recovery pattern works: a defer calls recover(), and if it caught a panic, it sets the named err return value instead of letting the panic crash the program.
One more detail worth knowing: named return variables are exempt from Go’s usual "declared and not used" compile error. A normal local variable you never read is a compile error; a named return variable you never explicitly read is fine, because the return mechanism itself counts as using it.
Syntax
func functionName(param1 Type1, param2 Type2) (result1 ReturnType1, result2 ReturnType2) {
// result1 and result2 exist here already, initialized to their zero values
result1 = someValue
result2 = anotherValue
return // naked return: sends back the current values of result1 and result2
}
| Part | Meaning |
|---|---|
(result1 ReturnType1, result2 ReturnType2) |
The named return list — each name behaves like a local variable of the given type, scoped to the whole function body. |
| Zero-initialization | Every named result starts at its type’s zero value before any code runs. |
Naked return |
A return with no operands; it sends back whatever the named variables currently hold. |
Explicit return |
You may still write return value1, value2 even with named results — this overrides the named variables’ current values for that particular exit point. |
Examples
Example 1: Basic Named Returns
package main
import "fmt"
func divide(a, b int) (quotient, remainder int) {
quotient = a / b
remainder = a % b
return
}
func main() {
q, r := divide(17, 5)
fmt.Println("quotient:", q, "remainder:", r)
}
Output:
quotient: 3 remainder: 2
quotient and remainder are declared once, in the function signature, and never redeclared inside the body — they are simply assigned. The naked return sends back whatever they hold at that point. Naming both results also makes the signature self-documenting: a caller reading (quotient, remainder int) immediately knows which value is which, which plain (int, int) could not tell them.
Example 2: Naked Returns with an Early Exit
package main
import (
"errors"
"fmt"
)
func safeDivide(a, b int) (result int, err error) {
if b == 0 {
err = errors.New("division by zero")
return
}
result = a / b
return
}
func main() {
r, err := safeDivide(10, 2)
if err != nil {
fmt.Println("error:", err)
} else {
fmt.Println("result:", r)
}
r, err = safeDivide(10, 0)
if err != nil {
fmt.Println("error:", err)
} else {
fmt.Println("result:", r)
}
}
Output:
result: 5
error: division by zero
When b is zero, the function sets err and returns naked immediately — result is simply left at its zero value, 0, which the caller never even looks at because it checks err first. This is a very common Go pattern: guard clauses that set the error result and bail out with a naked return.
Example 3: Defer Modifying a Named Return (Recover Pattern)
package main
import "fmt"
func compute(x int) (result int, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("recovered from panic: %v", r)
}
}()
if x < 0 {
panic("negative input not allowed")
}
result = x * 2
return
}
func main() {
result, err := compute(5)
fmt.Println(result, err)
result, err = compute(-3)
fmt.Println(result, err)
}
Output:
10 <nil>
0 recovered from panic: negative input not allowed
This is the pattern named returns exist for. Because err is a named result, the deferred closure can overwrite it after a panic, turning a crash into an ordinary error return. If result and err were not named, the deferred function would have no variable to write into, and there would be no way to convert the panic into a returned error at all.
How It Works Step by Step
Trace the compute(-3) call from Example 3:
- 1.
computeis called;resultanderrare created and zero-initialized to0andnil. - 2. The
deferstatement registers the recovery closure to run when the function exits, by any means. - 3.
x < 0is true, sopanic("negative input not allowed")runs, immediately halting normal execution and beginning to unwind the call stack. - 4. Before
computecan be abandoned, Go runs its deferred calls (in last-in-first-out order). The recovery closure runs and callsrecover(), which captures the panic value and stops the unwind. - 5. Inside that closure,
err = fmt.Errorf(...)writes directly into the named result variableerr— the very variablecomputeis about to hand back to its caller. - 6. With the panic recovered,
computereturns normally.resultis still0(the lineresult = x * 2never executed), anderrnow holds the wrapped message. - 7. The caller receives
(0, err)exactly as ifcomputehad returned an error the ordinary way — the panic never escapes.
Common Mistakes
Mistake 1: Shadowing Named Returns with :=
Because := declares a new variable whenever it appears in a new block, using it inside an if (or any nested block) creates local variables that merely share a name with the named returns — they do not assign to them. The naked return then sends back the untouched, still-zero-valued outer variables.
package main
import (
"errors"
"fmt"
)
func validate(age int) (msg string, err error) {
if age < 0 {
msg, err := "invalid", errors.New("age cannot be negative")
fmt.Println("inside if:", msg, err)
}
return
}
func main() {
msg, err := validate(-5)
fmt.Println("result:", msg, err)
}
Output:
inside if: invalid age cannot be negative
result: <nil>
Inside the if, msg and err print correctly — but they are shadow copies local to that block. The outer, named msg and err that the naked return actually sends back were never touched, so the caller gets "" and nil instead of the error. The fix is to use plain assignment (=), not declaration (:=), so the existing named variables are updated instead of shadowed:
package main
import (
"errors"
"fmt"
)
func validate(age int) (msg string, err error) {
if age < 0 {
msg, err = "invalid", errors.New("age cannot be negative")
}
return
}
func main() {
msg, err := validate(-5)
fmt.Println("result:", msg, err)
msg, err = validate(30)
fmt.Println("result:", msg, err)
}
Output:
result: invalid age cannot be negative
result: <nil>
Now the negative-age call correctly returns the message and error, while a valid age still returns the zero values, exactly as intended.
Mistake 2: Overusing Naked Returns in Long Functions
Naked returns save typing, but scattering several of them through a long, multi-branch function forces the reader to scroll back up to the top of the function every time to remember what each named variable currently holds at that point.
func classify(score int) (grade string, passed bool) {
if score >= 90 {
grade, passed = "A", true
return
}
if score >= 70 {
grade, passed = "B", true
return
}
if score >= 50 {
grade, passed = "C", true
return
}
grade, passed = "F", false
return
}
Each naked return here is correct, but a future edit that adds a branch or reorders one is one typo away from returning the wrong values silently. Writing the values out explicitly at each exit point is just as short and removes the ambiguity entirely:
func classify(score int) (grade string, passed bool) {
if score >= 90 {
return "A", true
}
if score >= 70 {
return "B", true
}
if score >= 50 {
return "C", true
}
return "F", false
}
Best Practices
- Name return values when the names genuinely clarify a function’s contract — especially when two or more results share the same type, like
(min, max int). - Reach for named returns specifically when a deferred function needs to modify a result, such as wrapping errors or recovering from a panic.
- Avoid naked returns in long or multi-branch functions; write
return value1, value2explicitly at each exit so readers never have to scroll up to check what is being returned. - Never use
:=inside a nested block expecting it to set a named return — it declares a shadow variable instead. Use plain=. - Don’t name returns purely to skip a
vardeclaration; only do it when the names add clarity or you need the defer behavior. - Remember named results start at their zero value — an early naked return before you’ve set them is sometimes the intended guard-clause behavior, and sometimes a bug. Be deliberate about which.
Practice Exercises
- Write a function
minMax(nums []int) (min, max int)that uses named returns to find the smallest and largest values in a slice. Test it against[]int{4, 1, 7, 3}— it should reportmin = 1andmax = 7. - Write a function
parsePercentage(s string) (value float64, err error)that callsstrconv.ParseFloatand uses adeferto wrap any resulting error with extra context, such as"invalid percentage: ...", before a naked return. - Take the
classifyfunction from Common Mistakes and rewrite it with explicitreturn grade, passedstatements at every exit. Confirm the behavior is unchanged for scores95,72, and40.
Summary
- Named return values give a function’s results names in its signature; they exist as ordinary local variables, zero-initialized before the function body runs.
- A naked
returnsends back whatever the named variables currently hold — no operands required. deferruns after return values are set but before the caller regains control, so a deferred closure can read and rewrite named returns — the standard idiom for panic-recovery error wrapping.- Named return variables are exempt from Go’s "declared and not used" check.
- Using
:=inside a nested block shadows named returns instead of assigning them — a classic, silent bug. Use=to assign to the outer variable. - Prefer explicit
return value1, value2over naked returns in long or multi-branch functions for readability and safety.
