liveBook · Manning

Defer

As explained in 100 Go Mistakes and How to Avoid Them

A defer statement schedules a function call to execute when the surrounding function returns, including during panic unwinding.

A defer statement schedules a call for when its surrounding function returns. It is useful for closing a resource without repeating cleanup before every return. The important boundary is the function, not the nearest loop. section 4.6

func readFiles(ch <-chan string) error {
    for path := range ch {                    #1
        file, err := os.Open(path)            #2
        if err != nil {
            return err
        }
 
        defer file.Close()                    #3
 
        // Do something with file
    }
    return nil
}

Mistake: deferring file closure inside a long-running loop. Every Close waits for readFiles to return, so file descriptors accumulate; if it never returns, they remain open. section 4.6

func readFiles(ch <-chan string) error {
    for path := range ch {
        if err := readFile(path); err != nil {    #1
            return err
        }
    }
    return nil
}
 
func readFile(path string) error {
    file, err := os.Open(path)
    if err != nil {
        return err
    }
 
    defer file.Close()                            #2
 
    // Do something with file
    return nil
}

Move each iteration’s work into readFile. Its deferred Close runs when that call returns, at the end of the iteration. An immediately invoked closure can provide the same boundary, but a separate function can be clearer and tested independently. section 4.6

Another mistake is assuming a deferred call reads its arguments when it runs. Arguments are evaluated at the defer statement. A method receiver is evaluated then, too: later changes to a value receiver are not reflected in the deferred call, while changes through a pointer receiver can be. section 6.6.1 section 6.6.2

const (
    StatusSuccess  = "success"
    StatusErrorFoo = "error_foo"
    StatusErrorBar = "error_bar"
)
 
func f() error {
    var status string
    defer notify(status)               #1
    defer incrementCounter(status)     #2
 
    if err := foo(); err != nil {
        status = StatusErrorFoo        #3
        return err
    }
 
    if err := bar(); err != nil {
        status = StatusErrorBar        #4
        return err
    }
 
    status = StatusSuccess             #5
    return nil
}

Both deferred calls receive the initial empty status, even though status changes before return. Pass a pointer if the functions accept one, or defer a closure that reads status when it runs. section 6.6.1

func f() error {
    var status string
    defer func() {                   #1
        notify(status)               #2
        incrementCounter(status)     #3
    }()
 
    // The rest of the function is unchanged
}

The closure delays reading status until the deferred function executes, without changing notify or incrementCounter to accept pointers. section 6.6.1

Cleanup can fail. A plain deferred rows.Close() discards its error. If the error matters, log it or use a closure and a named result to return it; preserve an earlier query or scan error rather than replacing it with a close error. A return inside the deferred closure returns only from that closure. section 7.7

func (h handler) getStatusCode(body io.Reader) (int, error) {
    resp, err := h.client.Post(h.url, "application/json", body)
    if err != nil {
        return 0, err
    }
 
    defer func() {                #1
        err := resp.Body.Close()
        if err != nil {
            log.Printf("failed to close response: %v\n", err)
        }
    }()
 
    return resp.StatusCode, nil
}

An HTTP response body needs closing even when only its status code is used. This deferred closure logs a close failure. When repeated requests need connection reuse, reading the body before closing it also matters. section 10.5.1

How 100 Go Mistakes and How to Avoid Them covers it

How deep it goes
Traces function-return timing and immediate argument evaluation, then works through deferred cleanup errors rather than runtime internals.
Examples
Contrasts file-opening loops with per-file functions, and shows status calls receiving an unexpectedly empty string.
What it assumes
Assumes familiarity with Go functions, loops, methods, and errors while explaining defer timing and argument evaluation.
What you can do after
Diagnose/debug with it: prevent loop-bound descriptor leaks and deferred calls that receive stale values.
How it connects
Connects defer to loops, closures, method receivers, named result parameters, and resource cleanup.

Also explained in

Compare how 3 books explain defer →

Related concepts

This explainer was generated by Manning's knowledge graph from the text of 100 Go Mistakes and How to Avoid Them. The section links point to the parts of the book it draws on.