Multiple Return Values
Go's signature feature is returning multiple values — typically (result, error).
Introduction
Go's signature feature is returning multiple values — typically (result, error). This idiom eliminates exceptions and makes failure paths explicit at every call site. The comma-ok pattern extends to map lookups and type assertions.
Named return values enable naked returns in short functions but can reduce clarity in longer ones. Understanding when to return pointers vs values, and how to document multi-value returns in godoc, is essential for library design.
Interviewers frequently ask why Go uses error returns instead of try/catch, and how to handle partial success scenarios — this lesson prepares you for both.
The story
A Cloudflare DNS API client fetches a record and its revision token in one call: record, etag, err := client.GetRecord(ctx, zoneID, name). Returning both data and error (plus metadata) is idiomatic Go — callers handle failures explicitly instead of throwing exceptions that crash goroutines in a server handling thousands of requests.
Multiple return values eliminate out-parameters and global state, making gRPC handlers and database repositories easy to test with table-driven cases.
Understanding the topic
Key concepts
- func f() (int, error) returns two values; caller: v, err := f().
- Blank identifier discards: val, _ := map[key].
- Comma-ok: val, ok := map[key]; ok is false if missing.
- Named returns: (n int, err error) — initialized to zero values.
- Early return with named: return (sets n and err automatically).
- Return (zero, err) on failure; (result, nil) on success.
Step-by-step explanation
- Function declares return types in signature.
- Caller must assign all returns or use blank _.
- Compiler enforces error handling less strictly than Rust — discipline required.
- Multiple values without error common: map lookup, type assertion, divMod.
- Return pointer + error: nil pointer signals failure alongside err.
- Wrap errors with fmt.Errorf("context: %w", err) for chain inspection.
Practical code example
Division with error return, comma-ok map lookup, and named returns:
package mainimport ("errors""fmt")func divide(a, b float64) (float64, error) {if b == 0 {return 0, errors.New("division by zero")}return a / b, nil}func lookupUser(users map[string]string, id string) (name string, found bool) {name, found = users[id]return // named return}func main() {result, err := divide(10, 2)if err != nil {fmt.Println("error:", err)return}fmt.Printf("10/2 = %.1f\n", result)users := map[string]string{"u1": "Alice", "u2": "Bob"}if name, ok := lookupUser(users, "u1"); ok {fmt.Println("found:", name)}if _, ok := lookupUser(users, "u99"); !ok {fmt.Println("user u99 not found")}}
Output
10/2 = 5.0 found: Alice user u99 not found
Line-by-line code explanation
func ParseConfig(path string) (Config, error)returns a value and an error — the Go idiom.cfg, err := ParseConfig("app.yaml")assigns both returns in one statement.if err != nil { return ..., fmt.Errorf("parse: %w", err) }wraps and propagates errors early.return Config{}, errreturns the zero value of Config alongside the error.func divide(a, b int) (int, bool)returns a result and anokboolean for soft failures.result, ok := divide(10, 0)lets callers branch without panicking on invalid input.named returns (n int, err error)improve readability in longer functions with multiple exit points._, err = io.Copy(dst, src)discards the byte count when only the error matters.
Key takeaway: Always check err before using result. Comma-ok prevents zero-value confusion on map misses.
Real-world use
Where you'll use this in production
- Every database and HTTP call returning (data, error).
- Parsing functions: strconv.Atoi, json.Unmarshal.
- Repository pattern: (Entity, error) or (nil, ErrNotFound).
- Optional values without pointers via (T, bool) tuple.
Best practices
- Return (zero, err) on failure — never ignore err.
- Use comma-ok for map/type assertions instead of panicking.
- Limit named returns to short functions; prefer explicit return vals.
- Document which return is nil on error for pointer returns.
- Use errors.Is and errors.As for error chain inspection.
- Return errors as last value consistently across package.
Common mistakes
- Using result before checking err — nil pointer dereference.
- Returning err without context — wrap with %w.
- Returning (nil, nil) — ambiguous success with no data.
- Too many return values — use struct for 3+ related values.
- Shadowing err in inner scope preventing outer check.
Advanced interview questions
Q1BeginnerWhy multiple return values?
Q2BeginnerComma-ok pattern?
Q3IntermediateNamed return pitfalls?
Q4IntermediateReturn pointer or value on error?
Q5AdvancedHandle partial success in batch operation?
Summary
(result, error) is Go's primary error idiom. Comma-ok handles map lookups and type assertions safely. Check err before using result; wrap errors with context. Named returns useful in short functions only. Next lesson: error handling — wrapping, sentinel errors, and errors package.