---
title: "08 — Functions — First-Class, Multi-Return, and the Closure Habit"
uid: golang-functions
tags: ["named-returns", "golang", "anonymous-functions", "closures", "roadmap:golang", "variadic", "functions", "call-by-value", "fundamentals"]
excerpt: "Go functions are first-class, return multiple values natively, close over their scope — and every call is strictly call-by-value. The multiple returns are the foundation of Go error handling."
date: 2026-08-13T03:28:11+0000
source: https://www.aveshina.my.id/en/blog/golang-functions
---

Go's functions felt familiar until the moment they didn't — multiple returns, then a call-by-value rule that rewired my mental model. The model that clicked: **Go functions are first-class values that natively return multiple results, support variadic arguments and named returns, and capture their enclosing scope as closures; and every call is strictly call-by-value, so what looks like pass-by-reference is just a copied pointer or copied slice header.** [1][2] The multiple-return feature is not a gimmick — it is the foundation of Go's error handling. And call-by-value-everything is the one rule that explains why pointers exist at all.

## The basics — func, parameters, returns

A function is declared with func, a name, a parameter list, and a return type [1]:

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

// Consecutive parameters of the same type share the type
func scale(x, y float64) float64 { return x * y }
```

Functions are first-class: they can be assigned to variables, passed as arguments, and returned from other functions [3]. This makes higher-order patterns — callbacks, middleware, strategies — straightforward without any special "function pointer" syntax.

## Multiple return values — and why they matter

A Go function can return more than one value, and the most common shape is "result and error" [4]:

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

result, err := divide(10, 2)
if err != nil {
    return err
}
fmt.Println(result)
```

This is the core of Go's error model. Functions that can fail return a value _plus_ an error, and the caller is expected to check if err != nil before using the value. There are no exceptions for ordinary control flow — errors are values, returned and checked explicitly. The caller can discard a return with the blank identifier: result, _ := divide(10, 2) says "I am ignoring the error," which the linters will flag if the error matters.

## Variadic functions

A function can accept a variable number of arguments of the same type with the ...Type syntax [5]:

```
func sum(nums ...int) int {
    total := 0
    for _, n := range nums {   // nums is a []int inside the function
        total += n
    }
    return total
}

sum(1, 2, 3)            // 6
nums := []int{1, 2, 3}
sum(nums...)            // spread a slice with ...
```

Inside the function, nums is a regular []int slice. The ... syntax appears twice: in the declaration (marking the parameter variadic) and at the call site (spreading a slice into individual arguments). The standard library leans on this heavily — fmt.Printf, append, and errors.Join are all variadic.

## Anonymous functions and closures

Go has anonymous functions — function literals with no name, assignable to variables or invoked immediately [6]:

```
add := func(a, b int) int { return a + b }
add(2, 3)

func() { fmt.Println("runs immediately") }()
```

The powerful case is the **closure** — an anonymous function that captures variables from its enclosing scope and keeps them alive even after that scope returns [7]:

```
func counter() func() int {
    count := 0
    return func() int {
        count++
        return count
    }
}

c := counter()
c()   // 1
c()   // 2
```

Each call to counter creates a fresh count that the returned function closes over and mutates. Closures are how Go builds iterators, accumulators, and stateful callbacks without objects — the captured variable _is_ the object's state. This is also why goroutines can capture loop variables (with a well-known footgun addressed in modern Go) and why middleware composition works so cleanly.

## Named return values

Return parameters can be named, which declares them as variables initialized to their zero value at the top of the function [8]:

```
func split(sum int) (x, y int) {
    x = sum * 4 / 9
    y = sum - x
    return            // "naked" return — returns current x, y
}
```

A naked return with no arguments sends back the current values of the named returns. This reads cleanly in short functions but hurts readability in long ones — tracking which named return was last assigned becomes a chore. The convention is to use named returns for documentation (the names show up in go doc) and in the one pattern where they are genuinely load-bearing: defer cleanup that must observe the final return value, such as recovering from a panic.

## Call by value — everything is copied

The rule that explains pointers, slices, and maps in one stroke: **Go is strictly call-by-value.** Every argument is copied when it is passed [9]. The nuance is _what_ gets copied depends on the type:

- **Value types** (int, struct, array) — the data is copied. Changes inside the function do not affect the caller.
- **Reference-like types** (slice, map, channel) — the _header_ is copied, but both headers point at the same underlying data. So appending to a slice parameter may not be visible to the caller (the header was copied), but modifying an existing element is.
- **Pointers** — the pointer (an address) is copied, but both pointers address the same variable. Mutations through *p are visible to the caller.

This is why pointers exist: when a function must modify a caller's variable, or when a struct is large enough that copying is wasteful, you pass a pointer. Slices and maps "feel" like references precisely because their headers are tiny and share storage — but the rule is uniform. Nothing is implicitly passed by reference; some types just happen to carry a pointer inside their header.

## How I use this

The multiple-return-plus-error pattern dictates how I write every function that can fail: I return (result, error), and at the call site I check the error before touching the result, handling it early to keep the happy path un-nested. Closures are my go-to for small stateful helpers — a counter, an accumulator, a middleware wrapper — far before I would reach for a struct. And the call-by-value rule shapes my parameter choices: small value types go by value, large structs and anything I need to mutate go by pointer, and I treat the shared-storage behavior of slices and maps as a feature to use deliberately rather than a surprise. The discipline of naming return values only where they document intent or support a defer keeps the simple functions simple.

## References

[1] The Go Authors, "Go Tour: Functions," go.dev, 2024. [Online]. Available: [https://go.dev/tour/basics/4](https://go.dev/tour/basics/4)

[2] Backend Forge, "Functions in Golang: Complete Guide with Examples," Medium, 2025. [Online]. Available: [https://medium.com/backend-forge/functions-in-golang-complete-guide-with-examples-2025-e07db0f98fd3](https://medium.com/backend-forge/functions-in-golang-complete-guide-with-examples-2025-e07db0f98fd3)

[3] learn-golang.org, "Learn Go Functions," 2024. [Online]. Available: [https://www.learn-golang.org/en/Functions](https://www.learn-golang.org/en/Functions)

[4] Labex, "How to manage Go function multiple returns," 2024. [Online]. Available: [https://labex.io/tutorials/go-how-to-manage-go-function-multiple-returns-419825](https://labex.io/tutorials/go-how-to-manage-go-function-multiple-returns-419825)

[5] DigitalOcean, "How To Use Variadic Functions in Go," 2024. [Online]. Available: [https://www.digitalocean.com/community/tutorials/how-to-use-variadic-functions-in-go](https://www.digitalocean.com/community/tutorials/how-to-use-variadic-functions-in-go)

[6] Golang Docs, "Anonymous functions in Golang," 2024. [Online]. Available: [https://golangdocs.com/anonymous-functions-in-golang](https://golangdocs.com/anonymous-functions-in-golang)

[7] The Go Authors, "Go Tour: Closures," go.dev, 2024. [Online]. Available: [https://go.dev/tour/moretypes/25](https://go.dev/tour/moretypes/25)

[8] yourbasic, "Named return values," 2024. [Online]. Available: [https://yourbasic.org/golang/named-return-values-parameters/](https://yourbasic.org/golang/named-return-values-parameters/)

[9] M. Fardi, "Parameter Passing in Golang: The Ultimate Truth," dev.to, 2024. [Online]. Available: [https://dev.to/mahdifardi/parameter-passing-in-golang-the-ultimate-truth-1h0o](https://dev.to/mahdifardi/parameter-passing-in-golang-the-ultimate-truth-1h0o)

```quiz
Q: When you pass a slice to a function, what gets copied?
- the entire underlying array
- the slice header (ptr/len/cap); both copies point at the same underlying array
correct: 1
explain: Go is call-by-value, and for a slice the value being copied is the three-word header. The backing array is shared, so modifications to existing elements are visible to the caller — but appending may reallocate into a new array the caller cannot see.

Q: A variadic parameter `nums ...int` inside the function body is usable as…
- a linked list of ints
- a []int slice
correct: 1
explain: The variadic parameter becomes a slice of the declared type inside the function. You can range over it, append to a local copy, and pass it to other variadic functions with the ... spread.

Q: What is the purpose of a closure in Go?
- to hide a function's name from the package
- to capture variables from an enclosing scope and keep them alive, enabling stateful functions like counters and iterators
correct: 1
explain: A closure is an anonymous function that captures outer variables by reference. Those captured variables persist as long as the closure does, which is how Go builds stateful helpers without structs.

Q: Named return values in Go…
- are required for every function
- declare return parameters as named, zero-initialized variables, and allow a naked `return` to send back their current values
correct: 1
explain: Named returns are optional. They document the return values and permit a bare `return` statement. They are most useful for short functions and for `defer` blocks that must observe the final return value.

Q: Why does Go use multiple return values (result, error) instead of exceptions?
- because the runtime cannot implement exceptions
- because treating errors as explicit returned values forces callers to handle them and makes the failure path visible in the function signature
correct: 1
explain: Go's philosophy is that errors are ordinary values. Returning them alongside the result makes failure part of the type signature and avoids the hidden control flow that exceptions introduce.
```
