AV
HomeAboutProjectBlog

© 2026 Ave syah Shina. All rights reserved.

  1. Home
  2. Blog
  3. 08 — Functions — First-Class, Multi-Return, and the Closure Habit

08 — Functions — First-Class, Multi-Return, and the Closure Habit

August 13, 20266 min read
Download as Markdown

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

[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

[3] learn-golang.org, "Learn Go Functions," 2024. [Online]. Available: 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

[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

[6] Golang Docs, "Anonymous functions in Golang," 2024. [Online]. Available: 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

[8] yourbasic, "Named return values," 2024. [Online]. Available: 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

Knowledge check · Question 1 of 5

When you pass a slice to a function, what gets copied?

Comments

Leave a Comment

You must be signed in to comment

0 Comments

No comments yet. Be the first to comment!