Go functions can return several values, carry variables around as closures, and schedule cleanup with defer. See how each works, including the order deferred calls run in and when their arguments are fixed.
Functions in Go look like functions anywhere else, until you notice three things. They can return more than one value. They’re values themselves, so you can store them and hand them around. And defer lets a function schedule work for the moment it exits.
This post covers all three, and the closures that come from treating functions as values. Every program below was run on Go 1.26, and its output is pasted from the run.
Declaring a function
A Go function lists its parameters with the type after the name, and its result type after the parameter list. When two parameters in a row share a type, you can write the type once.
package main
import (
"fmt"
"strconv"
)
func add(a, b int) int {
return a + b
}
func minMax(nums []int) (int, int) {
lo, hi := nums[0], nums[0]
for _, n := range nums[1:] {
lo = min(lo, n)
hi = max(hi, n)
}
return lo, hi
}
func main() {
fmt.Println(add(2, 3))
lo, hi := minMax([]int{7, 2, 9, 4})
fmt.Println(lo, hi)
n, err := strconv.Atoi("42")
fmt.Println(n, err)
n, err = strconv.Atoi("forty-two")
fmt.Println(n, err)
}
It prints:
5
2 9
42 <nil>
0 strconv.Atoi: parsing "forty-two": invalid syntax
add(a, b int) is short for add(a int, b int). minMax returns two values, so its result types go in parentheses, and the caller receives them with lo, hi :=. (min and max are builtins since Go 1.21.)
strconv.Atoi shows the most common shape in Go: a value and an error. When the conversion works, err is nil. When it fails, you get a zero value and an error that says what went wrong. Go has no exceptions for this. The error is just the last return value, and you check it with if err != nil. The part on errors covers that in depth.
You can’t ignore a second result by accident
A function that returns two values can’t be squeezed into one variable:
package main
import (
"fmt"
"strconv"
)
func main() {
n := strconv.Atoi("42")
fmt.Println(n)
}
The compiler says:
./main.go:9:7: assignment mismatch: 1 variable but strconv.Atoi returns 2 values
If you really don’t want the error, you have to say so with the blank identifier: n, _ := strconv.Atoi("42"). That _ is visible in code review, which is the point. Throwing an error away is a decision someone can see.
Named results
Go lets you give result values names, the same way you name parameters. Named results are declared at the top of the function, they start at their zero value, and a bare return returns whatever they hold.
package main
import (
"fmt"
"strings"
)
func splitPair(s string) (key, value string, ok bool) {
i := strings.Index(s, "=")
if i < 0 {
return
}
key = s[:i]
value = s[i+1:]
ok = true
return
}
func main() {
k, v, ok := splitPair("host=example.com")
fmt.Printf("%q %q %v\n", k, v, ok)
k, v, ok = splitPair("no equals sign")
fmt.Printf("%q %q %v\n", k, v, ok)
}
It prints:
"host" "example.com" true
"" "" false
The early return sends back "", "" and false, because nothing was assigned yet and those are the zero values. (The standard library already has this function, as strings.Cut. Writing it by hand shows the mechanics.)
The names help most as documentation. (key, value string, ok bool) tells a reader what comes back. (string, string, bool) makes them guess which string is which.
A bare return like that is called a naked return. It’s fine in a function this short. In a long one, avoid it. The reader has to scroll up to find out what’s being returned, and a variable declared in an inner block can hide a result without you noticing. Go catches the worst version of that:
package main
import (
"fmt"
"strconv"
)
func parsePort(s string) (port int, err error) {
if s != "" {
port, err := strconv.Atoi(s)
if err != nil {
return
}
fmt.Println("parsed", port)
}
return
}
func main() {
fmt.Println(parsePort("8080"))
}
The compiler says:
./main.go:12:4: result parameter port not in scope at return
./main.go:12:4: result parameter err not in scope at return
Inside the if, := declared a brand new port and err that hide the named results. A naked return there would send back the outer ones, which were never set, so Go refuses to compile it. Writing return port, err explicitly makes the code say what it means.
Variadic functions
A variadic function accepts any number of arguments of one type. You mark the last parameter with ..., and inside the function it’s a slice.
package main
import "fmt"
func sum(nums ...int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}
func main() {
fmt.Println(sum())
fmt.Println(sum(1, 2, 3))
scores := []int{10, 20, 30}
fmt.Println(sum(scores...))
}
It prints:
0
6
60
sum() gets an empty nums, so the total is 0. sum(1, 2, 3) gets a slice of three.
When you already have a slice, you can’t pass it as sum(scores), because a []int isn’t an int. Writing scores... spreads it into the call. Go doesn’t copy the elements when you do that. nums inside the function is the same slice as scores, sharing the same array. The part on slices shows why that matters.
You’ve been calling a variadic function all along. fmt.Println is declared as func Println(a ...any) (n int, err error).
Functions are values
A Go function is a value with a type, like an int or a string. You can assign it to a variable, pass it to another function, and return it from one.
package main
import (
"fmt"
"strings"
)
func apply(words []string, f func(string) string) []string {
out := make([]string, 0, len(words))
for _, w := range words {
out = append(out, f(w))
}
return out
}
func exclaim(s string) string {
return s + "!"
}
func main() {
words := []string{"go", "is", "fun"}
shout := strings.ToUpper
fmt.Println(shout("hello"))
fmt.Println(apply(words, exclaim))
fmt.Println(apply(words, strings.ToUpper))
fmt.Println(apply(words, func(s string) string {
return "<" + s + ">"
}))
fmt.Printf("%T\n", exclaim)
}
It prints:
HELLO
[go! is! fun!]
[GO IS FUN]
[<go> <is> <fun>]
func(string) string
shout := strings.ToUpper has no parentheses after ToUpper, so it doesn’t call the function. It stores it. apply takes any function of type func(string) string and calls it once per word. Your own exclaim, the library’s strings.ToUpper, and a function literal written right in the call all fit, because they all have that type.
The last line prints the type itself. A function’s type is its parameter and result types. The name isn’t part of it.
Closures
A function literal can use variables from the function around it. When it does, it’s called a closure, and it keeps those variables alive for as long as the closure exists.
package main
import "fmt"
func newCounter() func() int {
count := 0
return func() int {
count++
return count
}
}
func main() {
a := newCounter()
b := newCounter()
fmt.Println(a(), a(), a())
fmt.Println(b())
fmt.Println(a())
}
It prints:
1 2 3
1
4
newCounter returns, and normally its local count would be gone. But the function it returned still uses count, so count stays. Each call to a adds one to it.
b is a separate call to newCounter, so it got its own count, starting from 0. That’s why b() prints 1, and why a carries on to 4 afterwards. The two counters don’t share anything.
Explain it like I’m ten
Think of a closure as a function that carries a backpack.
When newCounter makes the little counting function, it puts count in that function’s backpack before sending it out. Wherever the function goes, the backpack goes too. Every time you call it, it opens the backpack, adds one to the number inside, and tells you the new number.
Call newCounter again and you get a second function with a second backpack. What’s in one backpack has nothing to do with the other.
The precise version
A function literal that refers to a variable from an enclosing function captures the variable itself, not a copy of its value. The closure and the enclosing function share that one variable. If either changes it, the other sees the change.
Because the closure can outlive the call that declared the variable, the compiler usually puts such a variable on the heap, where it lives as long as something refers to it. The part on memory looks at how the compiler decides that.
Where the analogy breaks: a backpack sounds like a private copy packed at the moment the function was made. It isn’t a copy. If the enclosing function changes count after creating the closure, the closure sees the new value, because they share one variable.
Closures in a loop
That sharing used to cause a famous bug with loop variables. Since Go 1.22, each iteration of a for loop gets a fresh variable, so a closure made in the loop captures that iteration’s value:
package main
import "fmt"
func main() {
var printers []func()
for i := range 3 {
printers = append(printers, func() {
fmt.Println("i is", i)
})
}
for _, p := range printers {
p()
}
}
It prints:
i is 0
i is 1
i is 2
Before Go 1.22, the whole loop shared one i, and this printed i is 3 three times. You’ll still see old code that works around it with a line like i := i inside the loop. On Go 1.22 and later that line does nothing, and you can delete it.
defer: run this when the function exits
A defer statement schedules a function call to run when the surrounding function returns, and not before. When there are several, they run in reverse order.
package main
import "fmt"
func main() {
fmt.Println("start")
defer fmt.Println("deferred 1")
defer fmt.Println("deferred 2")
defer fmt.Println("deferred 3")
fmt.Println("end of body")
}
It prints:
start
end of body
deferred 3
deferred 2
deferred 1
The body runs top to bottom, printing start and end of body. The three deferred calls wait. When main returns, they run, and the last one deferred runs first. Watch it step by step:
The defer stack for the program above. The body prints start, then each defer pushes a call onto the stack without running it. The body prints end of body and main returns. The deferred calls then come off the top of the stack one at a time, so they print deferred 3, deferred 2 and deferred 1.
Here are those steps in words, in case the animation doesn’t play for you:
- The body starts at the top and prints
start. - Each
deferline puts its call on a stack.deferred 1goes on first, thendeferred 2, thendeferred 3on top. Nothing is printed. - The last line of the body prints
end of body. mainreturns. The call on top of the stack,deferred 3, runs first.- Then
deferred 2runs. - Then
deferred 1, the first one deferred, runs last. The stack is empty, andmainis finished.
Explain it like I’m ten
Picture a stack of plates by the sink. Every time you say defer, you write a job on a plate and put it on top of the stack. You don’t do the job yet. You just keep going with whatever you were doing.
When you’re finished and about to leave the kitchen, you do the jobs on the plates. You can only take the top plate, so the last job you wrote down is the first one you do.
The precise version
Each time a defer statement runs, Go evaluates the function value and its arguments right then, and saves the call. When the function returns, whether through a return statement, by reaching its closing brace, or because of a panic, the saved calls run in last-in-first-out order. They run after the result values are set and before the function actually hands control back to its caller.
A defer inside a loop saves one call per iteration, and none of them run until the whole function returns. That’s worth knowing before you defer inside a loop that runs a million times.
Where the analogy breaks: you’d write the job on the plate in words and work out the details later. Go works out the arguments at the moment you say defer, as the next section shows.
Arguments are evaluated when you defer, not when the call runs
A deferred call waits until the function exits, but its arguments don’t wait. They’re worked out on the defer line, and that surprises most people the first time.
package main
import "fmt"
func main() {
x := 1
defer fmt.Println("deferred sees x =", x)
defer func() {
fmt.Println("closure sees x =", x)
}()
x = 2
fmt.Println("main sets x =", x)
}
It prints:
main sets x = 2
closure sees x = 2
deferred sees x = 1
The first defer evaluated x straight away, while it was still 1, and saved that 1 as an argument. Changing x later made no difference to it.
The second defer saved a closure that takes no arguments. There was nothing to evaluate early. When it finally ran, it read x, and by then x was 2. It shares the variable, exactly like the counter did.
So if you want a deferred call to see the latest value, defer a closure. If you want to freeze the value, pass it as an argument.
The everyday use: cleanup
Most defer statements in real Go code release something as soon as it’s acquired: close a file, close a response body, unlock a mutex. Writing the cleanup on the very next line means you can’t forget it on some return path you add later.
package main
import (
"errors"
"fmt"
)
type resource struct {
name string
}
func open(name string) *resource {
fmt.Println("open", name)
return &resource{name: name}
}
func (r *resource) Close() error {
fmt.Println("close", r.name)
return nil
}
func process(name string) error {
r := open(name)
defer r.Close()
if name == "broken.txt" {
fmt.Println("bad data, returning early")
return errors.New("bad data")
}
fmt.Println("processing", name)
return nil
}
func main() {
fmt.Println(process("notes.txt"))
fmt.Println(process("broken.txt"))
}
It prints:
open notes.txt
processing notes.txt
close notes.txt
<nil>
open broken.txt
bad data, returning early
close broken.txt
bad data
resource stands in for a file here, so the program doesn’t touch the disk. Its Close method just prints. (Methods get their own part. For now, read r.Close() as “call Close on r“.)
Both calls closed the resource, and both closed it before process handed its error back to main. The early return didn’t need its own Close, because the defer covers every way out of the function.
The same pattern protects a lock. You’ll write this constantly once you reach concurrency:
mu.Lock()
defer mu.Unlock()
One thing that program quietly skips: Close returns an error, and defer r.Close() throws it away. For a file you only read, that’s normal. For a file you wrote, a failed close can mean lost data. The next section shows one way to catch it.
Changing the result with a deferred closure
A deferred closure runs after the return values are set, and if those results are named, it can read and change them. That’s the one place where named results are more than documentation.
package main
import (
"errors"
"fmt"
)
func save(name string) (err error) {
defer func() {
if err != nil {
err = fmt.Errorf("save %q: %w", name, err)
}
}()
if name == "" {
return errors.New("empty name")
}
return nil
}
func main() {
fmt.Println(save("notes"))
fmt.Println(save(""))
}
It prints:
<nil>
save "": empty name
return errors.New("empty name") sets err. Then the deferred closure runs, sees a non-nil err, and replaces it with a wrapped error that adds the file name. The caller gets the replaced value.
With an unnamed result, the closure would have no name to reach the return value through, and it couldn’t do this. The same trick lets a deferred Close report its error: if cerr := f.Close(); cerr != nil && err == nil { err = cerr }.
defer also runs when a function panics, which is what makes recover possible. Both get their full treatment in the part on errors.
What to remember
- Parameters of the same type can share one type name, and a function can return several values. The last one is usually an
error, checked withif err != nil. - Named results start at their zero value and document what comes back. Keep naked returns for short functions.
- A variadic parameter
...Tis a slice inside the function. Spread an existing slice into it withs.... - Functions are values with a type such as
func(string) string. You can store them, pass them and return them. - A closure shares the variables it uses from its surrounding function, and keeps them alive. Since Go 1.22, each loop iteration gets its own variable.
deferruns calls when the function exits, last in first out. Its arguments are evaluated at thedeferline. A deferred closure reads variables when it runs, and can change named results.
Put the cleanup on the line right after the thing that needs cleaning up.