Before you begin
Why this matters
Go does not release a resource when a variable or a value ends. A defer or a Close call that you write does it. Put it in the wrong function, and a file stays open until a long function returns, or closes before the caller has read from it.
What this helps you reason about
- Predict when a deferred release runs when the defer is inside a block.
- Write a Close method for a struct that owns other resources.
- Spot a function that defers the release of a value it returns.
A night market lends cool boxes to its stalls. Each box must come back once, and only when nobody uses it any more. Cleanup on a failing path used defer so that every way out of a function returns the box. This lesson asks which function should hold that defer: the one that takes the box, a smaller one inside it, or the one the box is handed to.
Already know this? The short version
- Go has no RAII: nothing is released when a variable or a value ends. defer ties a release to the end of the surrounding function, by any return or a panic.
- A defer inside a block or a loop pass still waits for the whole function; wrap the use in a function literal, func() { … }(), or a named function, to release sooner.
- A struct that owns resources gets a Close method that releases every part; the code that owns the struct calls Close, usually with defer.
- A function that returns a resource must not defer its release; the caller owns it and defers it. Finalizers run some time after the value is unreachable, or never, so they are not a release.
Words in this lesson (8)
- resource
- Something a program takes for a while and must give back, such as a booking, a lock, or an open file. While one part of the program holds it, other parts cannot have it.
- cleanup
- The code that releases what a function took, such as giving back a booking or closing a file. It must run on every way out of the function.
- RAII
- A way of tying a resource to a value or a scope, so that ending the value or the scope releases the resource. The name comes from C++ and means “resource acquisition is initialization”.
- owner
- The one part of a program, such as a function or a value, that is responsible for releasing a resource. When the resource is handed on, the duty to release it goes with it.
- defer
- A statement, defer f(x), that makes the call f(x) wait until the surrounding function ends, by any return or by a panic. Deferred calls run newest first, and their arguments are computed when the defer statement runs.
- function literal
- A function written as a value, without a name, such as func(n int) int { return n + 1 }. It can be stored in a variable, passed, or returned.
- garbage collector
- The part of the Go runtime that finds values that are no longer reachable and reuses their memory, at a time it chooses.
- finalizer
- A function that the Go runtime may run some time after a value becomes unreachable, set with runtime.SetFinalizer or runtime.AddCleanup. There is no promise that it runs before the program exits.
Examples are focused snippets; imports, enclosing functions, or classes may be omitted. C examples use C11; Haskell examples use Haskell 2010. Expand the notes for language-specific depth.
01 / Scope
What does a deferred release wait for?
A resource is something a program takes for a while and must give back. RAII ties a resource to a value or a scope, so that ending the value or the scope releases the resource. The name comes from C++ and means “resource acquisition is initialization”. Go has no RAII. It has defer, which ties a release to a function.
A deferred call waits for the end of the function that contains the defer statement. A block inside that function, such as { … } or the body of an if, does not count. Cleanup on a failing path showed this for a loop; it holds for every block.
To release at the end of a block, make the block a function. func() { … }() is a function literal that is called at once: the () after its closing brace calls it. Its defer runs when that call ends.
func() {
box := &CoolBox{Number: 1}
defer box.Return() // runs when this call ends
fmt.Println("fill box", box.Number)
}() defer waits for the surrounding function; to release sooner, put the use in a smaller function.
Coming from Rust: A Go value is never released when its variable goes out of scope. Only a defer or a call that you write releases it.
Coming from Java: A Go block is not a try-with-resources block; a defer inside it belongs to the whole function.
Experiment
Defer a return inside a block
A defer belongs to a function. The block ends, and the name box goes out of scope, but main keeps the deferred call and runs it when main returns.
Why it matters When a release must happen before the function ends, give the use its own function.
Predict the order of the four lines. Then change the opening { of the block to func() { and its closing } to }(), and predict again.
package main
import "fmt"
type CoolBox struct {
Number int
}
func (b *CoolBox) Return() {
fmt.Println("return box", b.Number)
}
func main() {
{
box := &CoolBox{Number: 1}
defer box.Return()
fmt.Println("fill box", box.Number)
}
fmt.Println("wipe the stall")
fmt.Println("market closed")
}
What to look for
Step past the closing brace of the block. box leaves the variables list, but nothing is printed: wipe the stall and market closed come first. Only after the last line of main does the stepper enter Return. The frame list then shows main.main.deferwrap1 between Return and main: a wrapper the compiler makes for the deferred call, with a variable .autotmp_1 that you can ignore. The run prints fill box 1, wipe the stall, market closed, return box 1. After the edit, the stepper enters a frame named main.main.func1 for the function literal, and Return runs as that call ends: fill box 1, return box 1, wipe the stall, market closed.
Expected output
fill box 1
wipe the stall
market closed
return box 102 / Owners
Who releases the resources inside another value?
A struct can own other resources: a stall owns its fish box and its cheese box. Go releases nothing when a struct is no longer used, so the owner needs a method that releases its parts. By convention it is called Close. The code that owns the stall calls Close once, usually with defer.
Close decides the order. Release the parts in the reverse order of taking them, unless they need another order. And Close must name every part: a part that Close forgets is never released, and nothing reports it.
func (s *Stall) Close() {
s.Cheese.Return() // taken last, returned first
s.Fish.Return()
} A struct that owns resources needs a Close method that releases every part.
Coming from Java: Go has no AutoCloseable, but the method name is the same by convention, and defer stall.Close() takes the place of the try block.
Precisely: io.Closer, and errors from Close
The standard library names the method in the io.Closer interface: Close() error. A real Close returns an error. An owner with several parts should close every part even when one fails, and report the errors together; errors.Join, since Go 1.20, does that. Go error values, wrapping, and errors.Is shows how to keep the error from a deferred Close.
Experiment
Close a stall that owns two boxes
Go calls Close because of the defer. Close releases exactly the parts its own lines name, in the order they are written.
Why it matters Give a struct that owns resources a Close method that releases every part, and let its owner defer Close.
Predict the four lines and their order. Then delete the line s.Cheese.Return() and predict again.
package main
import "fmt"
type CoolBox struct {
Number int
}
func (b *CoolBox) Return() {
fmt.Println("return box", b.Number)
}
type Stall struct {
Fish *CoolBox
Cheese *CoolBox
}
func (s *Stall) Close() {
fmt.Println("close stall")
s.Cheese.Return()
s.Fish.Return()
}
func main() {
stall := &Stall{
Fish: &CoolBox{Number: 1},
Cheese: &CoolBox{Number: 2},
}
defer stall.Close()
fmt.Println("stall open")
}
What to look for
Step to the end of main. The deferred Close runs, with main.main.deferwrap1 between Close and main on the frame list. Inside Close, the stepper enters Return twice: b shows Number: 2, then Number: 1. The run prints stall open, close stall, return box 2, return box 1. After the edit, the run prints stall open, close stall, return box 1. Box 2 is never returned, and no error or warning says so.
Expected output
stall open
close stall
return box 2
return box 103 / Handing on
What happens when a resource is handed on?
A resource has one owner at a time: the code that must release it. When a function takes a resource and hands it to its caller, the duty to release goes with it. So that function must not defer the release. The defer would run as the function returns, and the caller would receive a box that is already back at the market.
The caller is the new owner. It writes the defer, right after the call that gave it the resource, and after checking any error from that call.
func lend(number int) *CoolBox {
box := &CoolBox{Number: number, InUse: true}
return box // no defer here: the caller owns the box
}
box := lend(3)
defer box.Return() // the owner releases it A function that returns a resource does not defer its release; the caller does.
Coming from Rust: Go has no moves: after lend returns, the pointer could be kept in two places, and only an agreement on who calls Return keeps the box returned once.
Experiment
Lend a box that is already back
lend’s defer runs when lend returns. Returning the pointer gives main a way to reach the box, but the release was tied to lend, which has just ended.
Why it matters A function that returns a resource must not defer its release; the caller defers it.
Predict the three lines. Then move the line defer box.Return() from lend into main, just below box := lend(3), and predict again.
package main
import "fmt"
type CoolBox struct {
Number int
InUse bool
}
func (b *CoolBox) Return() {
b.InUse = false
fmt.Println("return box", b.Number)
}
func lend(number int) *CoolBox {
box := &CoolBox{Number: number, InUse: true}
defer box.Return()
fmt.Println("lend box", box.Number)
return box
}
func main() {
box := lend(3)
fmt.Println("fill box", box.Number, "in use:", box.InUse)
}
What to look for
Step through lend. From return box, the next stop is inside Return: the deferred call runs before main gets the box. Under Return, the frame list shows main.lend.deferwrap1 and lend, and b changes from InUse: true to InUse: false. Back in main, box shows the same address with InUse: false. The run prints lend box 3, return box 3, fill box 3 in use: false. After the edit, the run prints lend box 3, fill box 3 in use: true, return box 3.
Expected output
lend box 3
return box 3
fill box 3 in use: false04 / Collector
Why not leave the release to the garbage collector?
The garbage collector is the part of the Go runtime that finds values that are no longer reachable and reuses their memory, at a time it chooses. It reuses memory. It does not call Return or Close.
runtime.SetFinalizer(box, f) asks the runtime to call f some time after box becomes unreachable. That is a finalizer, and Go makes no promise that it runs soon, or at all before the program exits. A program can run out of open files long before the collector notices. Release with defer or Close; a finalizer is at most a safety net that reports a forgotten resource.
box := &CoolBox{Number: 7}
box = nil // box 7 is unreachable, and still not returned The garbage collector frees memory, not resources; release with defer or Close.
Precisely: files and AddCleanup
An *os.File has a finalizer that closes the file when the collector finds it unreachable; it is a last attempt, not a plan. Go 1.24 added runtime.AddCleanup, which has the same lack of timing. Reachability and garbage collection, a later lesson, shows what reachable means and when the collector runs.
05 / Try it
Can you predict when each tray goes back?
A bakery stall bakes on one tray and sells from another. Follow each tray from the line that makes it to the point where its owner ends.
package main
import "fmt"
type Tray struct {
Number int
}
func (t *Tray) Return() {
fmt.Println("return tray", t.Number)
}
func bake() *Tray {
first := &Tray{Number: 1}
defer first.Return()
second := &Tray{Number: 2}
fmt.Println("bake bread on tray", first.Number)
return second
}
func main() {
tray := bake()
defer tray.Return()
fmt.Println("sell bread from tray", tray.Number)
}
What does the program print, in order?
Reveal the reasoning
bake bread on tray 1, return tray 1, sell bread from tray 2, return tray 2. bake keeps tray 1 and defers its return, so it goes back as bake ends. bake hands tray 2 to main without a defer; main owns it and returns it when main ends.
Practice tickets
- DETC-101 · Late cheeses on the tasting list find no knife free → Free
- DETC-102 · The gate counts trolleys as back while stalls still use them → Sign in · Pro
- DETC-103 · A failed swap leaves the stall under a returned umbrella → Sign in · Pro
Keep exploring
Reachability and garbage collection →