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.
Search across all documentation pages
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.
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.
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:
error as the last value.defer + named err).return statements.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:
%w for errors.Is / errors.As.Settings alongside an error - callers must check err before using s.err could pair with defer in more complex loaders; here explicit returns stay clearer.0, "", nil) before the first statement runs.return without arguments assigns nothing new - it exits with current named values.return to list values explicitly; the compiler enforces arity.| 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 |
err Patternfunc 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.
// 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.
err with := - Inner foo, err := creates a new err that defer will not update. Fix: use = on the existing named result or avoid := on err.return x, y except in short defer helpers.err and use s anyway. Fix: document "undefined on error" or return pointers (*Settings, error).error type collecting failures.| 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 |
Errors are values in the normal control flow.
Callers see failure paths in source order without hidden stack unwinding.
return with no operands in a function that declares named result parameters.
It returns whatever those variables hold at exit.
Yes - to their type zero values at function entry, before any statements run.
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.
Rarely.
Name err for defer patterns; keep other results unnamed unless godoc truly benefits.
No - result types are fixed in the signature.
Use interfaces, structs, or generics for variant outcomes.
Beyond three correlated values, prefer a result struct.
Arity explosion makes call sites error-prone.
Convention places error last: (T, error) or (int, string, error).
Linters and readers expect that position.
Valid for pointers or interfaces when "not found" is not an error.
Document semantics - some callers confuse nil, nil with bugs.
No meaningful difference - they are a compile-time mechanism.
Readability concerns dominate the decision.
They behave like ordinary locals for coverage.
Style guides (including golangci-lint revive rules) may flag naked returns.
Handlers return nothing - they write http.ResponseWriter.
Extract logic into functions returning (T, error) and map errors to status codes in the handler.
nil, nil through interfacesStack 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).
Reviewed by Chris St. John·Last updated Jul 19, 2026