defer in Depth
Argument evaluation time, rewriting named results, defer inside a loop, and what deferring actually costs.
defer func() { /* may read and assign named results */ }()A defer statement schedules a call to run when the surrounding function returns. The basics lesson covers the everyday use: open something, defer closing it. Three details decide whether deferred code does what you meant.
When things are evaluated
The function value and its arguments are worked out the moment the defer statement runs. Only the call itself waits. So defer fmt.Println(n) prints the n of that moment, not the n at return time. Deferred calls then run last in, first out: whatever was deferred last goes first.
Changing the result
A deferred function literal can read and assign the surrounding function's named results. A return err sets the named result, then the deferred code runs, then the caller receives the value. One deferred block can therefore wrap every failure path with context. This is also a shorthand-and-longhand choice: defer f.Close() throws away the error Close gives back, while defer func() { err = f.Close() }() keeps it. Same timing, only one can report a problem.
Loops and hot paths
defer belongs to the function, not to the block. A defer inside a loop does not run at the end of the iteration. It piles up and runs when the whole function returns, holding every file open until then. Move the loop body into its own small function instead. On cost: Go 1.14 made most defers nearly free by compiling them in place, so advice to avoid them on principle is out of date. A defer inside a loop is exactly the case that misses that treatment and stays slower.
Example
package main
import "fmt"
// result is a named result, so the deferred literal can change it
// after the return statement has already set it.
func doubled() (result int) {
defer func() {
result *= 2
}()
result = 1
return result // sets result to 1, then the defer doubles it
}
// Deferred arguments are read now; the calls run last in, first out.
func trace() {
n := 1
defer fmt.Println("second, n was", n)
defer fmt.Println("first, n was", n)
n = 99
fmt.Println("n ends as", n)
}
func main() {
fmt.Println("doubled:", doubled())
trace()
}
// Output:
// doubled: 2
// n ends as 99
// first, n was 1
// second, n was 1When to use it
- Add the file name or request id to every error a function can return, from one place.
- Close each file as its loop pass ends instead of at the end of the whole job.
- Report a failure from Close on a file the program has written to.
- Record how a function finished for a log line, without touching any of its return statements.
More examples
A defer in a loop, and the fix
Both versions look correct and the wrong one usually works, until the list is long enough to hit the open-file limit and the program fails with "too many open files". Pulling the body into sizeOf is the standard fix: a defer there runs on every return from that one call. os.Open is func(string) (*os.File, error) and f.Close is func() error, so the deferred Close here is also discarding an error.
package main
import (
"fmt"
"os"
)
// Wrong: one defer per pass, and none of them run until
// sizesWrong returns. With 5000 names that is 5000 open files.
func sizesWrong(names ...string) {
for _, name := range names {
f, err := os.Open(name)
if err != nil {
fmt.Println("skip", name)
continue
}
defer f.Close() // not at the end of this pass
st, err := f.Stat()
if err == nil {
fmt.Println(name, st.Size())
}
}
}
// Right: the body is its own function, so its defer runs per file.
func sizes(names ...string) {
for _, name := range names {
sizeOf(name)
}
}
func sizeOf(name string) {
f, err := os.Open(name)
if err != nil {
fmt.Println("skip", name)
return
}
defer f.Close() // runs when sizeOf returns, one file at a time
st, err := f.Stat()
if err == nil {
fmt.Println(name, st.Size())
}
}
func main() {
// Printed sizes depend on the files in your own directory.
sizes("go.mod", "main.go")
}Keeping the error from Close
For a file you only read, defer f.Close() is fine, because a failed close tells you nothing useful. For a file you wrote, a dropped Close error means silently losing data the program believes it saved. The if err == nil test matters: without it a Close error would overwrite a more informative write error. os.Create is func(string) (*os.File, error) and WriteString is func(string) (int, error).
package main
import (
"fmt"
"os"
)
// writeFile reports a failure from Close as well as from writing.
// Closing a written file is where the last bytes reach the disk,
// so Close is a real failure point, not a formality.
func writeFile(path, data string) (err error) {
f, err := os.Create(path)
if err != nil {
return err
}
// The longhand defer: a function literal, so the Close error
// is not thrown away. Compare "defer f.Close()", which drops it.
defer func() {
cerr := f.Close()
if err == nil {
err = cerr
}
}()
_, err = f.WriteString(data)
return err
}
func main() {
if err := writeFile("/does/not/exist/out.txt", "hello"); err != nil {
fmt.Println("write failed:", err)
}
}
Discussion