RAII & Dispose — Practice Tasks¶
Category: Resource & Type-Safety Patterns — hands-on drills for deterministic cleanup in Go, Java, and Python.
10 practice tasks with full Go, Java, and Python solutions.
Table of Contents¶
- Task 1: Safe File Read on Every Path
- Task 2: Scope-Bound Lock Guard
- Task 3: Transaction — Commit or Rollback
- Task 4: Custom Context Manager / AutoCloseable
- Task 5: Fix
deferInside a Loop - Task 6: Idempotent Close + Use-After-Close Guard
- Task 7: Multiple Resources, LIFO Cleanup
- Task 8: Dispose Pattern with Finalizer Backstop
- Task 9: Pool Lease (close == return)
- Task 10: Surface a Close/Flush Error on Writes
Task 1: Safe File Read on Every Path¶
Goal: Read a file and parse it; the handle must close even if parsing throws or you return early.
Python¶
def read_config(path: str) -> dict:
with open(path) as f: # closed on any exit
raw = f.read()
return parse(raw) # parse outside the hold — minimal scope
Go¶
func readConfig(path string) (Config, error) {
f, err := os.Open(path)
if err != nil {
return Config{}, err
}
defer f.Close() // runs on every return below
raw, err := io.ReadAll(f)
if err != nil {
return Config{}, err
}
return parse(raw)
}
Java¶
String readConfig(String path) throws IOException {
try (var r = new BufferedReader(new FileReader(path))) {
return r.lines().collect(Collectors.joining("\n"));
} // r.close() always runs
}
Task 2: Scope-Bound Lock Guard¶
Goal: A function that mutates shared state under a lock and never leaves the lock held on error.
Python¶
def transfer(lock, a, b, amount):
with lock: # released even if the check raises
if a.balance < amount:
raise InsufficientFunds()
a.balance -= amount
b.balance += amount
Go¶
func Transfer(mu *sync.Mutex, a, b *Account, amount int) error {
mu.Lock()
defer mu.Unlock() // unlocked on every return
if a.Balance < amount {
return ErrInsufficient
}
a.Balance -= amount
b.Balance += amount
return nil
}
Java¶
void transfer(Lock lock, Account a, Account b, int amount) {
lock.lock();
try {
if (a.balance() < amount) throw new InsufficientFundsException();
a.debit(amount); b.credit(amount);
} finally {
lock.unlock(); // always unlocks
}
}
Task 3: Transaction — Commit or Rollback¶
Goal: Commit on success, roll back on any exception, always close.
Python¶
from contextlib import contextmanager
@contextmanager
def transaction(db):
tx = db.begin()
try:
yield tx
tx.commit()
except Exception:
tx.rollback()
raise
finally:
tx.close()
with transaction(db) as tx:
tx.execute("UPDATE accounts SET balance = balance - 100 WHERE id = 1")
tx.execute("UPDATE accounts SET balance = balance + 100 WHERE id = 2")
Go¶
func WithTx(db *sql.DB, fn func(*sql.Tx) error) (err error) {
tx, err := db.Begin()
if err != nil {
return err
}
defer func() {
if p := recover(); p != nil {
tx.Rollback()
panic(p)
} else if err != nil {
tx.Rollback()
} else {
err = tx.Commit()
}
}()
return fn(tx)
}
Java¶
void withTx(DataSource ds, Consumer<Connection> work) throws SQLException {
try (Connection c = ds.getConnection()) {
c.setAutoCommit(false);
try {
work.accept(c);
c.commit();
} catch (RuntimeException e) {
c.rollback();
throw e;
}
} // connection closed regardless
}
Task 4: Custom Context Manager / AutoCloseable¶
Goal: Wrap a non-RAII resource so callers get scope-bound cleanup.
Python¶
class Socket:
def __init__(self, host): self._host = host
def __enter__(self):
self._conn = raw_connect(self._host)
return self._conn
def __exit__(self, exc_type, exc, tb):
self._conn.close()
return False # never swallow exceptions
with Socket("example.com:80") as conn:
conn.send(b"GET / HTTP/1.0\r\n\r\n")
Java¶
public final class Socket implements AutoCloseable {
private final RawConn conn;
public Socket(String host) { this.conn = RawConn.connect(host); }
public void send(byte[] b) { conn.write(b); }
@Override public void close() { conn.shutdown(); }
}
try (Socket s = new Socket("example.com:80")) {
s.send(request);
}
Go¶
// Go's idiom is a Close() method + defer at the call site
type Socket struct{ conn net.Conn }
func Dial(host string) (*Socket, error) {
c, err := net.Dial("tcp", host)
return &Socket{conn: c}, err
}
func (s *Socket) Close() error { return s.conn.Close() }
// caller:
s, _ := Dial("example.com:80")
defer s.Close()
Task 5: Fix defer Inside a Loop¶
Goal: Process many files without holding all of them open at once.
Go (the fix)¶
// ❌ Broken: all files held open until the function returns
// for _, p := range paths { f, _ := os.Open(p); defer f.Close(); use(f) }
// ✅ Fixed: each iteration is its own scope
func processAll(paths []string) error {
for _, p := range paths {
if err := processOne(p); err != nil {
return err
}
}
return nil
}
func processOne(p string) error {
f, err := os.Open(p)
if err != nil {
return err
}
defer f.Close() // closed at end of THIS call
return use(f)
}
Python (already correct)¶
Java (already correct)¶
Task 6: Idempotent Close + Use-After-Close Guard¶
Goal: Calling close() twice is safe; using after close fails fast.
Java¶
public final class Handle implements AutoCloseable {
private final Resource res;
private boolean closed = false;
public Handle() { this.res = Resource.acquire(); }
public void use() {
if (closed) throw new IllegalStateException("handle is closed");
res.doWork();
}
@Override public void close() {
if (closed) return; // idempotent
closed = true;
res.release();
}
}
Python¶
class Handle:
def __init__(self): self._res = acquire(); self._closed = False
def use(self):
if self._closed: raise RuntimeError("handle is closed")
self._res.do_work()
def close(self):
if self._closed: return # idempotent
self._closed = True
self._res.release()
def __enter__(self): return self
def __exit__(self, *exc): self.close(); return False
Go¶
type Handle struct {
res *Resource
closed bool
}
func (h *Handle) Use() error {
if h.closed { return errors.New("handle is closed") }
return h.res.DoWork()
}
func (h *Handle) Close() error {
if h.closed { return nil } // idempotent
h.closed = true
return h.res.Release()
}
Task 7: Multiple Resources, LIFO Cleanup¶
Goal: Open three dependent resources; close them in reverse order; partial failures still clean up.
Python (ExitStack)¶
from contextlib import ExitStack
def pipeline(src_path, dst_path):
with ExitStack() as stack:
src = stack.enter_context(open(src_path))
dst = stack.enter_context(open(dst_path, "w"))
tx = stack.enter_context(transaction(db))
dst.write(transform(src.read()))
tx.execute("INSERT INTO log VALUES ('done')")
# closed LIFO: tx, dst, src — even if a later enter_context raised
Java (try-with-resources, declared in dependency order)¶
try (Connection conn = pool.get();
PreparedStatement ps = conn.prepareStatement(SQL);
ResultSet rs = ps.executeQuery()) {
while (rs.next()) consume(rs);
} // rs, then ps, then conn — reverse order
Go (LIFO multi-closer)¶
type closers []io.Closer
func (cs closers) Close() error {
var err error
for i := len(cs) - 1; i >= 0; i-- { // LIFO
if e := cs[i].Close(); e != nil && err == nil {
err = e // keep closing, keep first error
}
}
return err
}
Task 8: Dispose Pattern with Finalizer Backstop¶
Goal: Deterministic disposal plus a GC-time safety net that logs the leak.
Python (weakref.finalize — no self capture)¶
import weakref, logging
class NativeBuffer:
def __init__(self, size):
self._handle = c_alloc(size)
self._fin = weakref.finalize(self, NativeBuffer._release, self._handle)
@staticmethod
def _release(handle, *, _leaked=False):
c_free(handle)
if _leaked:
logging.warning("NativeBuffer not closed()!")
def close(self):
self._fin() # deterministic; cancels the backstop
def __enter__(self): return self
def __exit__(self, *exc): self.close(); return False
Java (Cleaner — static state class)¶
public final class NativeBuffer implements AutoCloseable {
private static final Cleaner CLEANER = Cleaner.create();
private final Cleaner.Cleanable cleanable;
private static final class State implements Runnable {
final long handle;
State(long h) { this.handle = h; }
public void run() { free(handle); } // backstop
}
public NativeBuffer(int size) {
long h = alloc(size);
this.cleanable = CLEANER.register(this, new State(h));
}
@Override public void close() { cleanable.clean(); } // deterministic, once
}
C# (canonical Dispose pattern, for reference)¶
public sealed class NativeBuffer : IDisposable {
private IntPtr handle; private bool disposed;
public NativeBuffer(int size) => handle = Alloc(size);
public void Dispose() { Dispose(true); GC.SuppressFinalize(this); }
private void Dispose(bool disposing) {
if (disposed) return;
Free(handle); disposed = true;
}
~NativeBuffer() => Dispose(false); // backstop
}
Task 9: Pool Lease (close == return)¶
Goal: Hand out a pooled resource with RAII ergonomics; close returns it to the pool.
Python¶
from contextlib import contextmanager
@contextmanager
def lease(pool):
conn = pool.acquire()
try:
yield conn
finally:
pool.release(conn) # "close" == return to pool
with lease(pool) as conn:
conn.execute("SELECT 1")
Java¶
public final class Lease implements AutoCloseable {
private final Pool pool; private final Conn conn; private boolean closed;
public Lease(Pool pool) { this.pool = pool; this.conn = pool.acquire(); }
public Conn get() { return conn; }
@Override public void close() {
if (closed) return;
closed = true;
pool.release(conn); // returns, does not destroy
}
}
Go¶
type Lease struct{ pool *Pool; conn *Conn }
func (p *Pool) Lease() *Lease { return &Lease{pool: p, conn: p.acquire()} }
func (l *Lease) Close() error { l.pool.release(l.conn); return nil }
// caller:
l := pool.Lease()
defer l.Close() // returns conn to pool
Task 10: Surface a Close/Flush Error on Writes¶
Goal: For writable files, don't silently drop a flush/close error.
Go (named return captures close error)¶
func writeReport(path string, data []byte) (err error) {
f, err := os.Create(path)
if err != nil {
return err
}
defer func() {
if cerr := f.Close(); cerr != nil && err == nil {
err = cerr // a flush failure becomes the result
}
}()
_, err = f.Write(data)
return err
}
Java (try-with-resources surfaces close failure)¶
void writeReport(String path, byte[] data) throws IOException {
try (var out = new BufferedOutputStream(new FileOutputStream(path))) {
out.write(data);
} // close()/flush() failure propagates (or is suppressed onto a body error)
}
Python (with propagates close failure)¶
def write_report(path, data):
with open(path, "wb") as f:
f.write(data)
# f.close() on exit; an OSError from flush propagates out of the with-block
Practice Tips¶
- Audit every error/early-return path for a missed
close. - Never
deferinside a loop — extract a per-iteration function. - Make
close()idempotent, and guard use-after-close. defer Close()only after a successful acquire.- For writes, surface the close/flush error — it can mean lost data.
- Use
ExitStack/LIFO closers for a dynamic number of resources.
← Interview · Resource & Type-Safety · Roadmap · Next: Find-Bug
In this topic