Back to blog

Sunday, August 16, 2026

Named Return Values in Go: Useful Pattern or Anti-Pattern?

Title card for 'Named Return Values in Go: Useful Pattern or Anti-Pattern?' with a short code example

Both. Names on results are a pattern when they document the contract (file, err) or when a deferred closure must assign err after Close or recover. They are an anti-pattern when you use a bare return in a long function, or := that shadows the result and silently returns 0.

The failure

This compiles. It prints computed 12, then returns 0.

package main

import "fmt"

func multiply(a, b int) (result int) {
    if a > 0 {
        result := a * b // new variable, only lives inside the if block
        fmt.Println("computed", result)
    }
    return // returns the named result, which is still 0
}

func main() {
    fmt.Println(multiply(3, 4)) // 0
}

Writing result := a * b at the top level of the function would not compile ("no new variables on left side of :="), because result already exists in that scope. The bug only appears in an inner block. If you put a bare return inside that same block, the compiler catches it with "result is shadowed during return". Return after the block, as above, and nothing warns you.

The other common failure is a 40-line helper with return and no values. The reader hunts for which named vars were last assigned. An unassigned name on an early return hands back zeros with no comment.

Why this happens

Named results are real locals, zeroed before the body runs, sitting in the return slots the caller will read. A bare return means "send whatever is in those slots." := with the same name creates a different variable. The slots never change.

A deferred func can assign those slots after return has set them, only if they are named. Anonymous results are already copied; defer cannot change what the caller gets. That is why copyFile and panic-to-error wrappers name err.

Effective Go and the code review comments agree: names help go doc; naked returns belong in short functions only.

The fix

Name results when a defer must write them, or when two ints need names in the signature. Then return explicitly unless the function is a few lines:

func readConfig(filename string) (data []byte, err error) {
    f, err := os.Open(filename)
    if err != nil {
        return nil, err
    }
    defer func() {
        if closeErr := f.Close(); closeErr != nil && err == nil {
            err = closeErr
        }
    }()
    data, err = io.ReadAll(f)
    return data, err
}

Assign with =, not :=, when you mean the named result. Prefer anonymous results plus return n, err when you are not using defer-on-err and the names add no documentation.

Do not mix return "Hi!" and bare return in the same long function. Pick one style the reader can see at the return line.