Skip to main content

Multiple Return Values in Go

Published: December 17, 2025 Updated: August 29, 2026 Larry Qu 5 min read

Multiple return values are Go’s primary mechanism for returning a result alongside a possible error. Unlike exceptions (which flow out-of-band), errors are explicit return values that every call site must either handle or propagate. This design makes error paths visible in code review and forces callers to think about failure at the point of the call.

For error types and wrapping see Go error handling and Go custom errors.

The Error Pattern

The convention: functions that can fail return (result, error), with error last. The caller checks if err != nil immediately:

func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, fmt.Errorf("division by zero")
    }
    return a / b, nil
}

result, err := divide(10, 3)
if err != nil {
    log.Fatal(err)
}
fmt.Printf("%.4f\n", result)

The zero value for the result (0 here) is returned alongside the error. This is conventional — callers who check err != nil won’t use the zero value, but it keeps the return statement simple.

Returning More Than Two Values

Multiple returns work for any combination, not just result+error:

// Return min and max in one pass
func minMax(nums []int) (min, max int) {
    if len(nums) == 0 {
        return 0, 0
    }
    min, max = nums[0], nums[0]
    for _, n := range nums[1:] {
        if n < min { min = n }
        if n > max { max = n }
    }
    return  // bare return — uses named values
}

lo, hi := minMax([]int{3, 1, 4, 1, 5, 9})
fmt.Println(lo, hi)  // 1 9

// Return data alongside metadata
func fetchPage(url string) (body []byte, statusCode int, err error) {
    resp, err := http.Get(url)
    if err != nil { return nil, 0, err }
    defer resp.Body.Close()
    body, err = io.ReadAll(resp.Body)
    return body, resp.StatusCode, err
}

Named Return Values

Named return values declare the return variables in the function signature. They’re zero-initialized and a bare return returns their current values:

func openAndRead(path string) (data []byte, err error) {
    // Deferred function can access named returns by reference
    defer func() {
        if err != nil {
            err = fmt.Errorf("openAndRead(%s): %w", path, err)
        }
    }()

    f, err := os.Open(path)
    if err != nil {
        return  // deferred func wraps the error
    }
    defer f.Close()

    data, err = io.ReadAll(f)
    return  // deferred func wraps err if non-nil
}

The deferred closure adds "openAndRead(path): " context to every error returned from the function — without repeating fmt.Errorf at each return site. This is the primary practical use case for named returns.

Avoid named returns just to enable bare return in simple functions — it hides what’s being returned:

// ❌ Unhelpful — reader must check the signature to know what's returned
func getUser(id int) (user *User, err error) {
    // ...
    return  // what are we returning?
}

// ✅ Explicit — clear at each return site
func getUser(id int) (*User, error) {
    // ...
    return user, nil
}

The Comma-Ok Idiom

Several standard operations return a second bool indicating success or presence. The pattern is value, ok := ...:

// Map lookup: ok=false if key absent
val, ok := m["key"]
if !ok {
    fmt.Println("key not found")
}

// Type assertion: ok=false if wrong type
str, ok := iface.(string)
if !ok {
    fmt.Println("not a string")
}

// Channel receive: ok=false if channel closed
v, ok := <-ch
if !ok {
    fmt.Println("channel closed")
}

This is different from the error pattern — bool for “did this succeed” vs error for “why did this fail.” Use bool for operations that have a natural absent/present distinction without detail (map lookups, type assertions). Use error when callers need to know why something failed.

Discarding Return Values

_ discards a return value. Use it deliberately — ignoring errors should be a conscious choice, not accidental:

// Explicit discard — acceptable for truly optional operations
io.WriteString(w, "hello")  // error from ResponseWriter is usually network close
data, _ := json.Marshal(obj) // only if obj is known to be marshalable

// ✅ Document why you're ignoring the error
if err := os.Remove(tmpFile); err != nil && !errors.Is(err, os.ErrNotExist) {
    log.Printf("cleanup failed: %v", err)
}
// Or: intentionally ignore cleanup errors
_ = os.Remove(tmpFile)  // best-effort cleanup

Never _ = someImportantOperation() — if the operation matters, handle its error.

Returning a Struct vs Multiple Values

When a function returns three or more related values, consider whether a struct is clearer:

// ❌ Hard to use without named returns — what is each float64?
func parseAddress(s string) (float64, float64, string, error) { ... }
lat, lon, city, err := parseAddress("...")

// ✅ Self-documenting struct
type Location struct {
    Lat, Lon float64
    City     string
}
func parseAddress(s string) (Location, error) { ... }
loc, err := parseAddress("...")
fmt.Println(loc.City)

The rule of thumb: two return values (result + error) are idiomatic. Three or more is a signal to consider a struct.

Error Propagation Chain

Multiple return values make error propagation explicit. Each function adds context and passes it up:

func loadUser(id string) (*User, error) {
    row, err := db.QueryRow("SELECT * FROM users WHERE id = $1", id)
    if err != nil {
        return nil, fmt.Errorf("loadUser(%s): %w", id, err)
    }
    // ...
}

func handleRequest(w http.ResponseWriter, r *http.Request) {
    user, err := loadUser(r.PathValue("id"))
    if err != nil {
        // Error chain: "handleRequest: loadUser(42): db query: connection refused"
        http.Error(w, "internal error", 500)
        log.Printf("handleRequest: %v", err)
        return
    }
    json.NewEncoder(w).Encode(user)
}

Summary

  • Return (result, error) for operations that can fail; check if err != nil immediately at each call site
  • Named return values are primarily useful for deferred error wrapping — add context in one place rather than repeating it at every return site
  • The comma-ok idiom (val, ok := ...) for map lookups, type assertions, and channel receives — bool for presence, error for failure detail
  • Discard with _ deliberately, not lazily — document why you’re skipping an error
  • Three or more return values often signal a struct would be clearer

Resources

Comments

👍 Was this article helpful?