Skip to content
JavaGo
Part III · Functions & Errors

Defer

defer replaces try/finally and try-with-resources: cleanup next to acquisition, LIFO order, and panic/recover for the truly exceptional.

Section 11 of 31 5 min read

defer schedules a function call to run when the surrounding function returns, whether it returns normally, returns early from any branch, or panics. It is Go’s answer to finally and to try-with-resources.

defer vs try-with-resources

Java — try-with-resources
List<Message> load(DataSource ds) throws SQLException {
    try (Connection conn = ds.getConnection();
         Statement st = conn.createStatement();
         ResultSet rs = st.executeQuery("SELECT ...")) {
        List<Message> out = new ArrayList<>();
        while (rs.next()) {
            out.add(map(rs));
        }
        return out;
    } // closed in reverse order, even on exceptions
}
Go — defer
func load(ctx context.Context, db *sql.DB) ([]Message, error) {
    rows, err := db.QueryContext(ctx, "SELECT ...")
    if err != nil {
        return nil, err
    }
    defer rows.Close() // registered right after acquiring

    var out []Message
    for rows.Next() {
        var m Message
        if err := rows.Scan(&m.ID, &m.Topic); err != nil {
            return nil, err // rows.Close() still runs
        }
        out = append(out, m)
    }
    return out, rows.Err()
}

Deferred calls run last-in, first-out

cleanup mirrors acquisition order
func transfer(ctx context.Context, db *sql.DB) error {
    conn, err := db.Conn(ctx)
    if err != nil {
        return err
    }
    defer conn.Close() // registered 1st, runs LAST

    tx, err := conn.BeginTx(ctx, nil)
    if err != nil {
        return err
    }
    defer tx.Rollback() // registered 2nd, runs FIRST
    // After a successful Commit, Rollback is a harmless no-op
    // (it returns sql.ErrTxDone, which we ignore here).

    if err := debit(ctx, tx); err != nil {
        return err // Rollback, then Close
    }
    return tx.Commit()
}

Each defer pushes a call

Returning pops them: last in, first out

Fig. 11.1Each defer statement, when it runs, pushes its call onto the function's stack of deferred calls. When the function returns, they run in reverse: tx.Rollback() runs before conn.Close().

Arguments are evaluated immediately

the call is deferred; its arguments are not
start := time.Now()
defer log.Printf("took %v", time.Since(start)) // BUG: Since runs NOW, logs ~0s

// Wrap it in a closure to evaluate at exit time
defer func() {
    log.Printf("took %v", time.Since(start)) // runs at return
}()

Changing the return value with defer

named results let deferred code inspect and replace errors
// Closing a file you WROTE can fail (data may not be flushed).
// Ignoring that error can silently lose data.
func writeReport(path string, data []byte) (err error) {
    f, err := os.Create(path)
    if err != nil {
        return err
    }
    defer func() {
        // Keep the first error, but never drop a Close error
        err = errors.Join(err, f.Close())
    }()

    _, err = f.Write(data)
    return err
}

panic and recover

Java — unchecked exception
void riskyOp() {
    throw new IllegalStateException("broken invariant");
}

try {
    riskyOp();
} catch (RuntimeException e) {
    log.error("recovered", e);
}
Go — panic, recovered in a deferred function
func riskyOp() {
    panic("broken invariant")
}

func safeOp() (err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("recovered: %v", r)
        }
    }()
    riskyOp()
    return nil
}

A panic in any goroutine kills the whole process

In Java an uncaught exception kills one thread. In Go, a panic that unwinds to the top of any goroutine without being recovered terminates the entire program. net/http recovers panics inside handlers so one bad request cannot crash the server, but if a handler starts its own goroutine and that goroutine panics, nothing recovers it. Long-lived background goroutines should recover, log and report their own panics (see Defer — Advanced).

↑ ↓ to navigateEnter to openEsc to close