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
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
}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
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
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
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
// 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
void riskyOp() {
throw new IllegalStateException("broken invariant");
}
try {
riskyOp();
} catch (RuntimeException e) {
log.error("recovered", e);
}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).