Prompt and context
Go 1.24 provides weak.Pointer and runtime.AddCleanup. Design a cache that reuses memory-mapped objects by filename: the cache must not keep an object alive forever, callers must use a safely acquired object, and concurrent creation must not corrupt the index. Do not treat a weak reference as a deterministic destructor notification.
What the interviewer is testing
The key signals are distinguishing strong and weak reachability, handling Value returning nil, and reasoning about object lifetime after lookup, concurrent loads, cleanup callbacks that capture objects, and nondeterministic test timing. Strong answers also explain when an ordinary cache is preferable.
Clarifying questions to ask first
Object ownership
Confirm whether callers hold a strong reference during use and whether the mapped object has an explicit close method. A weak reference solves cache retention, not application ownership.
Concurrency and duplicate creation
Ask whether multiple goroutines may create a mapping for the same file, whether single-flight behavior is required, and how expensive recreation is after collection.
Cleanup guarantees
Determine whether cleanup is a hint for resource release or a correctness condition. AddCleanup runs at a garbage-collector-determined time and cannot implement a transaction that must happen on schedule.
A 30-second answer framework
“Store a weak.Pointer in the index and call Value on a hit to obtain a strong reference; that strong reference protects the object while the caller uses it. If Value returns nil, create an object and publish the weak pointer with concurrency-safe coordination. Use cleanup as an auxiliary release mechanism, never assuming it runs promptly or at all. Avoid capturing the target object from the cleanup callback or map value, and test final states rather than exact GC timing.”
Deep-dive answer steps
Step 1: Define the cache record
Use the filename as the key and store weak.Pointer[MappedFile] plus creation metadata. No field, closure, or reverse index may hold a strong MappedFile reference, or the weak cache becomes a strong cache.
Step 2: Load and upgrade the reference
Read the weak pointer from a concurrent map, then call Value. If it succeeds, immediately keep the result in a local strong reference and return it; nil is a cache miss. Do not store the returned address in another long-lived structure without defining ownership.
Step 3: Handle concurrent creation
Several goroutines may observe nil and create temporarily duplicate objects. Use compare-and-swap or single-flight coordination to publish one index entry. A displaced object can remain valid while its caller holds a strong reference; replacing the index is not an immediate close.
Step 4: Arrange external-resource cleanup
For resources that need closing, register runtime.AddCleanup with only the required handle or identifier. The callback must not capture the target object or receive it as an argument that recreates a strong reachability path, or cleanup may never run.
Step 5: State race and memory boundaries
Value returning nil is an allowed result, not an exception. The object may disappear from the cache between operations, but once a local strong reference is acquired, it owns the current use interval. The object API still defines its close protocol.
Step 6: Explain nondeterminism
The garbage collector may delay cleanup or never run it before process exit. Cache capacity, file-descriptor limits, and latency budgets cannot rely on cleanup happening “soon”; add explicit eviction, close, or background quota controls.
Step 7: Design tests
Test concurrent hits, recreation after collection, duplicate creation, map replacement, and explicit close. GC pressure only helps exercise a path; it cannot prove cleanup occurs by a deadline. Observe resource counts, eventual state, and race-detector results.
High-quality sample answer
I would store only weak.Pointer[MappedFile] in the index, call Value on a hit, and pass the result as a local strong reference to the caller. On nil, create an object and publish the weak pointer under concurrency coordination. runtime.AddCleanup can be a backstop for external handles, but the callback must not capture or receive the target object; explicit close and capacity controls remain. Tests cover concurrency, recreation, resource counts, and races without relying on exact GC timing.
Common mistakes
- Mistake: Assuming a weak pointer guarantees eventual cleanup. → Why: Collection and cleanup scheduling depend on the GC. → Improve: Keep explicit lifetime controls and treat cleanup as auxiliary.
- Mistake: Capturing the target in a cleanup closure. → Why: The closure creates a strong reachability path. → Improve: Pass only an independent handle or identifier.
- Mistake: Keeping only the weak pointer after
Valuesucceeds. → Why: Later use may lose its strong reference. → Improve: Keep a local strong reference for the use interval. - Mistake: Using GC pressure to prove a fixed cleanup deadline. → Why: Cleanup has no timing guarantee. → Improve: Assert eventual state and explicit-close behavior.
Follow-up questions and answers
Follow-up 1: What is the key difference between weak.Pointer and a normal pointer?
A normal pointer keeps its target reachable. weak.Pointer does not participate in reachability, so Value may return nil. Once a normal pointer is acquired, that strong reference owns the use interval.
Follow-up 2: Why must the cache value avoid pointing back to the keyed object?
If a field or closure strongly references the target, it remains reachable and the weak cache cannot release it. Inspect every reverse path in the weak-reference structure.
Follow-up 3: Can cleanup replace defer Close?
No. Cleanup is a fallback or auxiliary release mechanism with nondeterministic timing. When the caller knows the lifetime, use explicit close or defer.
Follow-up 4: When should you avoid weak.Pointer?
Avoid it when hits must be stable, resource release has a hard deadline, or ordinary bounded eviction is simple enough. Prefer an explainable strong-reference cache with explicit eviction.