Recommended resources
The freely available resources this lesson will direct you to.
Hello! In the previous lesson, you saw that Go passes arguments by value and that function boundaries matter: changing an existing slice element is different from replacing the caller’s slice. This lesson adds another function-boundary rule. defer schedules a call now but runs it just before the surrounding function finishes.
You’ll use that rule to keep cleanup beside resource acquisition, predict the order of multiple deferred calls, and distinguish values saved at a defer statement from values read later. Allow about 40 minutes, including the short video, reading, and a run in your existing module.
A call scheduled for the end of a function
A deferred call belongs to the function invocation that executes its defer statement. It does not run when an if block or loop iteration ends. It runs after that function’s ordinary work is done, immediately before control returns to its caller.
func report() {
defer fmt.Println("leaving")
fmt.Println("working")
}
Calling report() prints working, then leaving. If report has several return paths, the deferred call runs on whichever path is taken—as long as execution reached the defer statement first.
When several defers have been reached, they run last in, first out:
func report() {
defer fmt.Println("first registered")
defer fmt.Println("second registered")
fmt.Println("working")
}
The output is working, second registered, first registered. The diagram shows the same arrangement after three defers have been registered within one function invocation.

Watch these two portions of Mastering the Defer Statement in Go with Code Examples from Code & Learn. The first states the timing and ordering rules; the demonstration adds a variable whose value changes before the deferred calls run.
Mastering the Defer Statement in Go with Code Examples
Watch these portions of Mastering the Defer Statement in Go with Code Examples from Code & Learn to see registration and execution as separate events.
Start with timing and order, which explains immediate argument evaluation and last-in, first-out execution. Then watch the demonstration. Notice which output appears before any deferred call, and keep track of the order in which the two calls were registered.
Put cleanup next to acquisition
Suppose a function opens a file and reads it. Once opening succeeds, that function is responsible for closing the file. Registering the close immediately makes that responsibility visible even if reading fails:
func readFile(path string) ([]byte, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
return io.ReadAll(f)
}
This example uses os and io. If os.Open fails, there is no open file to close, so the function returns before registering the defer. If it succeeds, f.Close() runs after io.ReadAll completes, including when the function returns a read error. Here, the close error is ignored; that can be acceptable for a read-only example, but it deserves more care when writing data.
Read the opening portion of Andrew Gerrand’s Defer, Panic, and Recover on the Go Blog. Its two-file example exposes a cleanup bug that is easy to introduce when a function acquires resources at different points. Although the article also discusses panic and recovery, stop after its three rules for defer; those are the relevant rules for this lesson.
Read the opening of Andrew Gerrand’s Go Blog article for the file-cleanup problem and its three concise rules for deferred calls.
In the opening section, begin at the paragraph introducing a function that copies between two files. Read the file example, including both versions of CopyFile. Identify the return path that leaks the source file in the first version. Then continue through the list introduced as “three simple rules”: read the three rules and the immediately following example c. Stop before the paragraph beginning “Panic is a built-in function.”
There is a useful design pattern in that example: acquire a resource, check that acquisition succeeded, then register its cleanup. If you acquire two resources in order, their deferred cleanups run in reverse order. That often matches the dependency between them.
A defer is not a substitute for checking whether acquisition succeeded, nor does it run if execution never reaches its statement. It also cannot guarantee cleanup if the process exits without returning through the function—for example, os.Exit does not run deferred calls.
What is saved now, and what is read later?
The deferred call runs later, but its function value, arguments, and any method receiver are evaluated when the defer statement executes. This connects directly to last lesson’s pass-by-value rule:
func showValues() {
label := "open"
defer fmt.Println("argument:", label)
defer func() {
fmt.Println("closure:", label)
}()
label = "closed"
}
The output is:
closure: closed
argument: open
The first defer saves the current string value as an argument to fmt.Println. The second defers a function literal with no arguments; when that function runs, its body reads the label variable as it then exists. Last-in, first-out order makes the closure print first.
A method call follows the argument rule too. With defer f.Close(), the receiver f is evaluated when the defer is registered. Reassigning the local variable f afterward would not redirect that saved close call to a different file. As with a copied pointer in the previous lesson, saving a receiver does not necessarily freeze the state of the object it refers to; the deferred method runs against that object later.
There is one more timing detail when a function returns a value. Go evaluates the return expression before running deferred calls. A deferred closure can then change a named return value:
func adjusted() (result int) {
defer func() {
result++
}()
return 1
}
adjusted() returns 2: return 1 sets result to 1, then the deferred closure increments it before the caller receives it. This behavior is useful when cleanup itself can fail, but changing return values invisibly can also make code harder to follow. Reserve it for cases where the cleanup result genuinely belongs in the function’s return value.
Choose the right cleanup boundary
Because defer is function-scoped, this loop keeps every successfully opened file open until processFiles returns:
func processFiles(paths []string) error {
for _, path := range paths {
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close()
if _, err := io.Copy(io.Discard, f); err != nil {
return err
}
}
return nil
}
For a few files, that may be harmless. For many files, it can exhaust available file handles. If each file should be closed at the end of its own iteration, give each iteration a function boundary:
func processFiles(paths []string) error {
for _, path := range paths {
if err := processOne(path); err != nil {
return err
}
}
return nil
}
func processOne(path string) error {
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close()
_, err = io.Copy(io.Discard, f)
return err
}
Now each call to processOne closes its file before the next iteration begins. The distinction is not “defer inside a loop is always wrong”; it is whether the surrounding function has the resource lifetime you intend.
For output files, closing can itself report a failure. Here is a compact pattern that returns a close error if writing otherwise succeeded:
func save(path string, data []byte) (err error) {
f, err := os.Create(path) // Creates or truncates the file.
if err != nil {
return err
}
defer func() {
closeErr := f.Close()
if err == nil {
err = closeErr
}
}()
_, err = f.Write(data)
return err
}
The deferred closure reads the named err after the write result has been assigned. It preserves a write error if one occurred; otherwise it returns the close error, if any. The next lesson will develop error-handling choices in more detail.
To check the ordering rules yourself, add this function to a new file in your existing package main, call demonstrateDefer() from your existing main, and run go run .:
package main
import "fmt"
func demonstrateDefer() {
fmt.Println("before")
showOrder()
fmt.Println("after")
}
func showOrder() {
label := "first"
defer fmt.Println("early:", label)
defer func() {
fmt.Println("late:", label)
}()
defer fmt.Println("last")
label = "changed"
fmt.Println("body")
}
You should see before and body first. Then showOrder runs its defers in reverse registration order: last, late: changed, and early: first. Only after showOrder has finished does demonstrateDefer print after.
Takeaways
defer attaches cleanup to a function invocation, not to a block. Register cleanup immediately after successful acquisition; expect multiple deferred calls to run in reverse order; and remember that call arguments are evaluated at registration time even though the call runs later. For resources acquired in a loop, choose a function boundary that closes each one when it is no longer needed.
Next, you’ll build on the close-error example to create, return, wrap, and inspect errors idiomatically.