Skip to content

Functions & Multiple Return Values Beginner

Functions are the primary building block in Go. Unlike most languages, Go functions natively support multiple return values, which powers Go's distinctive error-handling pattern. Functions are also first-class values -- they can be assigned to variables, passed as arguments, and returned from other functions.


Basic Function Syntax

func add(a int, b int) int {
    return a + b
}

// Shorthand: same-type parameters share the type
func add(a, b int) int {
    return a + b
}

// No return value
func logMessage(msg string) {
    fmt.Println(msg)
}

Multiple Return Values

Go functions can return any number of values. This is not syntax sugar -- it's built into the language.

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

result, err := divide(10, 3)
if err != nil {
    log.Fatal(err)
}

The (value, error) Pattern

This is Go's most important convention. Almost every function that can fail returns (result, error).

// Standard library examples
f, err := os.Open("config.json")
data, err := io.ReadAll(f)
n, err := fmt.Println("hello")
val, err := strconv.Atoi("42")
conn, err := net.Dial("tcp", "localhost:8080")

Interview Tip

"Go uses multiple return values instead of exceptions. The convention is (value, error) where error is the last return value. This makes error paths explicit and impossible to accidentally ignore -- the compiler forces you to handle or explicitly discard the returned error."


Named Return Values

Return values can be named, giving them zero-value initialization and enabling "naked returns."

func divide(a, b float64) (result float64, err error) {
    if b == 0 {
        err = errors.New("division by zero")
        return // naked return: returns result=0.0, err=<error>
    }
    result = a / b
    return // naked return: returns result=<value>, err=nil
}

When Named Returns Are Useful

Named returns serve two legitimate purposes:

// 1. Documentation -- names appear in godoc
func ParseDuration(s string) (duration time.Duration, err error)

// 2. Deferred modification -- only way to modify returns in defer
func readFile(path string) (data []byte, err error) {
    f, err := os.Open(path)
    if err != nil {
        return nil, err
    }
    defer func() {
        if closeErr := f.Close(); closeErr != nil && err == nil {
            err = closeErr // modifies the named return
        }
    }()
    return io.ReadAll(f)
}

Avoid naked returns in long functions

Naked returns hurt readability. Use them only in short functions (< 10 lines). In longer functions, return values explicitly even if they're named.

// BAD: reader has to track what 'result' and 'err' are set to
func process(input string) (result string, err error) {
    // ... 30 lines of code ...
    return // what is being returned?
}

// GOOD: explicit return even with named values
func process(input string) (result string, err error) {
    // ... 30 lines of code ...
    return result, nil
}


Variadic Functions

A function can accept a variable number of arguments of the same type using ....

func sum(nums ...int) int {
    total := 0
    for _, n := range nums {
        total += n
    }
    return total
}

sum(1, 2, 3)       // 6
sum(1, 2, 3, 4, 5) // 15
sum()               // 0 (empty is valid)

Passing a Slice to a Variadic Function

Use ... to expand a slice into variadic arguments.

numbers := []int{1, 2, 3, 4}
total := sum(numbers...)  // expands slice into individual args

Variadic Must Be Last Parameter

func format(prefix string, values ...int) string { // OK
    // ...
}

// func bad(values ...int, suffix string) // COMPILE ERROR

Real-World Variadic Patterns

// Functional options pattern
type Option func(*Server)

func NewServer(addr string, opts ...Option) *Server {
    s := &Server{addr: addr}
    for _, opt := range opts {
        opt(s)
    }
    return s
}

srv := NewServer(":8080",
    WithTimeout(30*time.Second),
    WithMaxConns(100),
)

The Blank Identifier _

Use _ to explicitly discard return values you don't need.

// Discard the index
for _, v := range items {
    process(v)
}

// Discard the value, keep the error
_, err := fmt.Println("hello")

// Discard all returns (rare, but valid)
_ = someFunction()

Don't silently discard errors

// BAD: error ignored
data, _ := os.ReadFile("config.json")

// GOOD: handle or at minimum log
data, err := os.ReadFile("config.json")
if err != nil {
    return fmt.Errorf("reading config: %w", err)
}
Discarding errors with _ is acceptable only when the function's documentation guarantees no meaningful error (rare).


First-Class Functions

Functions are values in Go. They can be assigned to variables, stored in data structures, and passed as arguments.

Functions as Values

op := func(a, b int) int { return a + b }
fmt.Println(op(3, 4)) // 7

op = func(a, b int) int { return a * b }
fmt.Println(op(3, 4)) // 12

Functions as Parameters

func apply(nums []int, transform func(int) int) []int {
    result := make([]int, len(nums))
    for i, n := range nums {
        result[i] = transform(n)
    }
    return result
}

doubled := apply([]int{1, 2, 3}, func(n int) int { return n * 2 })
// [2, 4, 6]

Function Types

Define named function types for clearer signatures.

type Predicate func(int) bool
type Transformer func(string) string

func filter(items []int, pred Predicate) []int {
    var result []int
    for _, item := range items {
        if pred(item) {
            result = append(result, item)
        }
    }
    return result
}

evens := filter([]int{1, 2, 3, 4, 5}, func(n int) bool {
    return n%2 == 0
})

Functions as Return Values

func multiplier(factor int) func(int) int {
    return func(n int) int {
        return n * factor
    }
}

double := multiplier(2)
triple := multiplier(3)
fmt.Println(double(5))  // 10
fmt.Println(triple(5))  // 15

Closures

Functions returned from other functions capture variables from their enclosing scope. This is covered in depth in the intermediate topic on Closures & Anonymous Functions.


defer (Brief Overview)

defer schedules a function call to run when the enclosing function returns. It's essential for cleanup.

func readConfig(path string) ([]byte, error) {
    f, err := os.Open(path)
    if err != nil {
        return nil, err
    }
    defer f.Close() // runs when readConfig returns

    return io.ReadAll(f)
}

Key behaviors:

  • Deferred calls execute in LIFO order (last deferred runs first).
  • Arguments are evaluated immediately, but the call is delayed.
  • Deferred functions can read and modify named return values.

Deep dive

defer, panic, and recover are covered in detail in the intermediate topic on Defer, Panic & Recover.


Quick Reference

Feature Syntax Notes
Basic function func f(a int) int { } Types after parameter names
Multiple returns func f() (int, error) { } Parenthesized return types
Named returns func f() (n int, err error) { } Enables naked return and defer modification
Naked return return (no values) Only with named returns; avoid in long functions
Variadic func f(args ...int) { } Must be last parameter; receives as []int
Expand slice f(slice...) Passes slice elements as individual args
Discard return _, err := f() Blank identifier
Function value fn := func(x int) int { return x } Anonymous function assigned to variable
Function type type Op func(int, int) int Named function type
Defer defer f() Runs on function exit, LIFO order

Best Practices

  1. Always return error as the last value -- this is the universal Go convention.
  2. Handle every error -- use _ only when you're certain the error is irrelevant.
  3. Keep functions short -- if a function needs named returns for readability, it might be too long.
  4. Use named returns for documentation, not for naked returns in complex functions.
  5. Use function types to simplify signatures with callback parameters.
  6. Prefer small, focused functions -- Go's multi-return makes it easy to compose.
  7. Use defer for cleanup immediately after acquiring a resource.

Common Pitfalls

Ignoring errors

// The most common Go antipattern
result, _ := riskyOperation()
// If riskyOperation failed, result is the zero value
// and you'll get confusing bugs downstream

Named return shadowing

func fetch(url string) (body []byte, err error) {
    resp, err := http.Get(url)
    if err != nil {
        return // returns nil, err -- OK
    }
    // SUBTLE BUG: := creates a NEW err, shadows named return
    body, err := io.ReadAll(resp.Body)
    //        ^ should be = not := (if err is already declared)
    // Actually this is fine since body is new, but watch for cases
    // where all vars on the left are already declared
}

Variadic nil vs empty

func log(args ...string) {
    fmt.Println(args == nil) // check if no args passed
}

log()               // args is nil
log("a", "b")       // args is []string{"a", "b"}

var s []string
log(s...)           // args is nil (nil slice expanded)
s = []string{}
log(s...)           // args is [] (empty, NOT nil)

Defer in a loop

// BAD: deferred closes accumulate until function returns
for _, path := range files {
    f, err := os.Open(path)
    if err != nil {
        return err
    }
    defer f.Close() // won't close until outer function returns!
}

// FIX: wrap in an anonymous function
for _, path := range files {
    if err := func() error {
        f, err := os.Open(path)
        if err != nil {
            return err
        }
        defer f.Close() // closes at end of each iteration
        return process(f)
    }(); err != nil {
        return err
    }
}

Performance Considerations

  • Function call overhead: Go function calls are cheap (~2-5ns). Don't inline manually -- the compiler does it for small functions (use go build -gcflags="-m" to check).
  • Defer cost: defer adds ~35-50ns overhead (significantly improved since Go 1.14). Use it freely for correctness; avoid it only in the tightest hot-path loops.
  • Closures and allocation: A closure that captures local variables may cause them to escape to the heap. In hot paths, consider passing values as parameters instead of capturing.
  • Variadic allocation: Calling a variadic function allocates a slice. For hot paths with known argument counts, consider non-variadic overloads.
  • Named returns: No performance difference versus unnamed returns. Choose based on readability.

Key Takeaways

  1. Go functions support multiple return values natively -- this powers the (value, error) pattern.
  2. Named returns are useful for documentation and defer modification, but avoid naked returns in long functions.
  3. Variadic functions use ... and must be the last parameter; expand slices with slice....
  4. Functions are first-class values -- assign, pass, and return them freely.
  5. Use _ to discard returns, but never silently discard errors in production code.
  6. defer ensures cleanup runs on function exit -- use it immediately after resource acquisition.
  7. The error return convention is (result, error) with error always last.