Representative interview topic

Go 1.26 cgo overhead reduction: How do you design a measurable FFI boundary?

CodingHard
Offer.cc Editorial TeamPublished Updated

Question

Go 1.26 reduces baseline cgo overhead by roughly 30%. If a service calls a C library, how do you choose the FFI boundary and prove the upgrade provides real gains?

Prompt and context

The Go 1.26 release announcement reports roughly 30% lower baseline cgo overhead and more cases where the compiler puts slice backing stores on the stack. The interview is not a percentage quiz; it tests how you control call frequency, memory ownership, error propagation, and reproducible experiments across Go and C.

What the interviewer evaluates

  • Whether you explain fixed cgo costs and thread, scheduler, and pointer rules.
  • Whether you reduce boundary crossings with batching, long-lived C calls, or data layout.
  • Whether you define memory ownership, lifetime, concurrency, and cancellation semantics.
  • Whether benchmarks, profiles, and production metrics prove gains rather than one lucky run.

Questions to clarify first

Confirm whether C calls are tiny and frequent or long-running batches, whether data size and format are stable, whether copying is acceptable, and whether the C library is thread-safe. Clarify platforms, compiler, CGO_ENABLED, race detection, and rollback options.

A 30-second answer

I would define a boundary and baseline before optimizing call shape. Batch tiny frequent calls, specify Go/C ownership and error conversion, and use workers or a long-lived context for long tasks. Benchmarks measure end-to-end latency, throughput, allocation, CPU, and tail latency while comparing Go 1.25 and 1.26 on representative hardware and flags. A production canary confirms copying or lock contention did not erase the gain.

Step-by-step deep dive

1. Draw the FFI boundary

Wrap C capabilities in a few coarse-grained functions instead of crossing the boundary inside a Go loop. Pass length-delimited buffers, handles, and status codes; do not pass complex objects containing Go pointers directly to C. Long tasks can keep resources in a C context while Go waits or polls for results.

2. Control memory and lifetime

Define who allocates and frees memory and whether C may retain a pointer. Record byte counts and direction for copies; for zero-copy paths, verify alignment, read-only behavior, and lifetime. Every C allocation needs a symmetric release path, including errors. A slice conversion does not make a cross-language pointer safe.

3. Design concurrency and cancellation

Confirm C thread safety and global locks, then cap workers so many goroutines do not enter a serial C critical section. Cancellation must reach the C API. If the library cannot interrupt, isolate calls in reclaimable workers or a process boundary with timeouts and resource limits.

4. Build an evidence chain

Fix inputs and warm-up behavior with go test -bench, then use CPU, memory, and blocking profiles to locate boundary cost. Compare Go 1.25 and 1.26 with the same compiler, linker flags, and hardware across single-call, batched, and end-to-end tests. A canary watches p95/p99, crashes, C errors, and allocation changes, with a rollback switch.

Example of a strong answer

I would not equate the roughly 30% figure with business gain. First classify the call pattern: for tiny frequent functions, expose a batch C API; for long tasks, let C own a context while Go waits asynchronously. Pass length-delimited buffers, handles, and status codes, and document allocation, release, copying, and pointer lifetime.

For evidence, fix input, hardware, compiler, and linker flags; measure single-call, batched, and end-to-end benchmarks; and use CPU, memory, and blocking profiles to explain the difference. During a canary, watch tail latency, allocations, crashes, and C errors to catch lock or copying costs. Any non-cancellable C call needs isolation, a timeout, and a rollback plan.

Common mistakes

  • Repeating the 30% number without the baseline, hardware, or call shape.
  • Letting C retain Go pointers indefinitely to avoid a copy, violating lifetime rules.
  • Adding goroutines to hide a global C lock or serial bottleneck.
  • Measuring only a microbenchmark while ignoring end-to-end tails, crashes, and cleanup.

Follow-up questions and responses

When can batching make performance worse?

Batch waits increase per-request queueing and memory peaks. If data is small, latency targets are strict, or C already batches internally, the gain can disappear. Choose batch size from end-to-end p99 and throughput together.

How do you prevent a C library from leaking resources?

Wrap handles in an explicit Go lifetime object with an idempotent Close, and release on error, timeout, and cancellation. Long-running tests and native tools should confirm handles, heaps, and threads do not grow continuously.

What if Go 1.26 improves benchmarks but production regresses?

Align compiler, CGO_ENABLED, CPU features, and request mix first; then compare copying, lock waits, GC, and C-internal timings. If the regression is platform-specific, canary or roll back per platform and preserve a reproducible sample.

Public sources

Related questions

Related interview tool

Use Screenshot for a coding prompt

Capture the problem, then work through the constraints, solution, code, edge cases, and complexity in order.

View the tool