Multiple Returns, Named Returns & Naked Returns
Go functions can return multiple values - usually a result and error - and may optionally name those results in the signature.
Named returns enable naked return statements and integrate with defer, but overuse hides data flow and frustrates readers.
Summary
Multiple return values are Go's primary mechanism for signaling failure without exceptions.
Callers receive (T, error) tuples and handle errors explicitly at each step.
Named result parameters declare variables scoped to the entire function body, initialized to zero values on entry.
A naked return (no operands) returns the current values of those named results.
Use named returns sparingly: they shine in short functions with defer that adjusts an error, and they hurt readability in long functions with many branches.
Prefer unnamed returns when the logic is easier to follow with explicit return value, err statements.
Recipe
Quick-reference recipe card - copy-paste ready.
func ReadConfig(path string) ([]byte, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("read config %s: %w", path, err)
}
return data, nil
}
// Named returns + defer: adjust err on exit (idiomatic pattern).
func WriteAtomic(path string, data []byte) (err error) {
f, err := os.CreateTemp(filepath.Dir(path), "tmp-*")
if err != nil {
return err
}
defer func() {
closeErr := f.Close()
if err == nil {
err = closeErr
}
}()
if _, err = io.Copy(f, bytes.NewReader(data)); err != nil {
return err
}
return os.Rename(f.Name(), path)
}When to reach for this:
- Any function that can fail should return
erroras the last value. - Defer blocks need to update the outgoing error after cleanup (
defer+ namederr). - Documentation tools should show result names in godoc for tiny parsers.
- You want to avoid repeating long tuple types in many
returnstatements.
Working Example
package config
import (
"encoding/json"
"fmt"
"os"
)
type Settings struct {
Port int `json:"port"`
Host string `json:"host"`
}
func Load(path string) (s Settings, err error) {
raw, err := os.ReadFile(path)
if err != nil {
return s, fmt.Errorf("config: read: %w", err)
}
if err = json.Unmarshal(raw, &s); err != nil {
return s, fmt.Errorf("config: decode: %w", err)
}
if s.Port == 0 {
return s, fmt.Errorf("config: port required")
}
return s, nil
}What this demonstrates:
- Explicit final return with values even when results are named - readers see what leaves the function.
- Wrapped errors preserve causes with
%wforerrors.Is/errors.As. - Validation returns the zero
Settingsalongside an error - callers must checkerrbefore usings. - Named
errcould pair withdeferin more complex loaders; here explicit returns stay clearer.
Deep Dive
How It Works
- The compiler treats result parameters like local variables declared at function entry.
- Named results initialize to zero values (
0,"",nil) before the first statement runs. returnwithout arguments assigns nothing new - it exits with current named values.- Unnamed results require every
returnto list values explicitly; the compiler enforces arity.
Named vs Unnamed Returns
| Style | Choose when | Avoid when |
|---|---|---|
Unnamed (T, error) | Most business logic, many branches | Never - this is the default |
Named + explicit return vals | Godoc clarity for tuple-heavy APIs | Function body exceeds ~40 lines |
Named + naked return | Short defer/error adjustment | Multiple writers to same result names |
Named only for err | defer closes resources and sets err | err shadowed in inner blocks |
Defer and the Named err Pattern
func do() (err error) {
defer func() {
if err != nil {
err = fmt.Errorf("do: %w", err)
}
}()
// ...
return nil
}The deferred closure observes the named result variable by reference (addressable), so assignments in defer affect the value returned to callers.
Shadowing err with := in an inner block breaks this pattern - use = on the named result instead.
Go Notes
// Anti-pattern: naked return far from declaration.
func parseAll(input string) (tokens []string, err error) {
for _, part := range strings.Split(input, ",") {
if part == "" {
return // which values? reader must scroll up
}
tokens = append(tokens, part)
}
return
}Prefer return tokens, nil at the bottom and after errors for scanability.
Gotchas
- Shadowing named
errwith:=- Innerfoo, err :=creates a newerrthat defer will not update. Fix: use=on the existing named result or avoid:=onerr. - Naked return in long functions - Readers cannot see returned values at the exit point. Fix: use explicit
return x, yexcept in short defer helpers. - Returning zero value on error - Callers may ignore
errand usesanyway. Fix: document "undefined on error" or return pointers(*Settings, error). - Mixing naked and explicit returns - Inconsistent style in one function confuses review. Fix: pick one style per function.
- Named results in public API without benefit - Adds cognitive load for marginal godoc wins. Fix: name only when defer/error pattern requires it.
- Multiple errors - Go has no built-in error aggregation in returns. Fix: return first error, or define a custom
errortype collecting failures. - Changing result names is a breaking change - Result names appear in godoc and can affect generated wrappers. Fix: treat exported signatures as stable API.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
Single struct return Result | Many correlated outputs | Only (T, error) needed - extra struct noise |
Pointer out-params func(*T) error | C interop or mutation of large state | Idiomatic Go libraries - prefer returns |
| Panic/recover | Truly unrecoverable programmer bugs | Expected failures (I/O, validation) |
(T, bool) ok tuple | Map lookups, presence checks | Operations that fail with rich errors |
FAQs
Why does Go use multiple return values instead of exceptions?
Errors are values in the normal control flow.
Callers see failure paths in source order without hidden stack unwinding.
What is a naked return?
return with no operands in a function that declares named result parameters.
It returns whatever those variables hold at exit.
Are named return values initialized?
Yes - to their type zero values at function entry, before any statements run.
When is the defer + named err pattern idiomatic?
When cleanup (close, rollback) must run and may itself fail after a primary error.
The named err lets defer augment or preserve the first failure.
Should I name non-error results?
Rarely.
Name err for defer patterns; keep other results unnamed unless godoc truly benefits.
Can I return different types from the same function?
No - result types are fixed in the signature.
Use interfaces, structs, or generics for variant outcomes.
How many return values is too many?
Beyond three correlated values, prefer a result struct.
Arity explosion makes call sites error-prone.
Does return order matter for errors?
Convention places error last: (T, error) or (int, string, error).
Linters and readers expect that position.
What happens if I return nil, nil?
Valid for pointers or interfaces when "not found" is not an error.
Document semantics - some callers confuse nil, nil with bugs.
Are named returns slower?
No meaningful difference - they are a compile-time mechanism.
Readability concerns dominate the decision.
How do named returns interact with coverage tools?
They behave like ordinary locals for coverage.
Style guides (including golangci-lint revive rules) may flag naked returns.
Should HTTP handlers return multiple values?
Handlers return nothing - they write http.ResponseWriter.
Extract logic into functions returning (T, error) and map errors to status codes in the handler.
Related
- Functions & Methods Basics - intro examples for returns and methods
- Functions and Interfaces: Go's Composition Model - how errors compose through layers
- Variadic Functions & Function Types - functions as values returning tuples
- Nil Interface vs Nil Pointer - returning
nil, nilthrough interfaces - Functions, Methods & Interfaces Best Practices - error and API conventions
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).