Top 30 Intermediate Go & Rust Interview Questions and Answers
30 intermediate multiple-choice Go & Rust interview questions, past the definitions: how the pieces fit together, what breaks in practice, and the trade-off behind a choice. They come from 30 bites in the Go & Rust library, the middle slice of the 132 Go & Rust interview questions in the library. Answer them here or read straight down. Every question carries the correct option, why it is correct, and a link to the bite it came from.
Go web services, Rust backends, systems programming
30 questions. Pick an answer, or open “Show the answer” to read it.
Answers are graded in your browser. Nothing is saved, and no XP or streak is earned here. The app keeps score.
Question 1 of 30
When a developer wants to abstract over a third-party type, which difference between Go interfaces and Rust traits is most significant?
Show the answer
Answer: d · Go lets a consumer define an interface that the third-party type implicitly satisfies, while Rust requires an explicit impl block to bind the trait to the type.
Go uses structural typing, so a consumer can define an interface that an existing third-party type automatically satisfies without any declaration; Rust uses nominal typing, requiring an explicit impl block. Distractor A reverses these exact mechanics, reflecting the common misconceptions that Go needs explicit declarations and that Rust traits are implicit.
Question 2 of 30
Which accurately describes a key difference between Go's switch and Rust's match?
Show the answer
Answer: d · Rust match is an expression that yields a value and requires exhaustive patterns, while Go switch is a statement with implicit breaks and no exhaustiveness check.
Rust match evaluates to a uniform value and the compiler rejects non-exhaustive patterns, whereas Go switch is statement-oriented, auto-breaks, and never checks exhaustiveness. Option B is tempting but wrong because Go implicitly breaks unless fallthrough is explicit, and Rust match arms never fall through.
Question 3 of 30
When passing a string to a function by value, why is Go's operation cheap while Rust's String may require explicit cloning?
Show the answer
Answer: c · Go strings are small two-word headers copied by value with immutable backing data, whereas Rust String moves ownership and must be cloned to duplicate its heap buffer.
Go strings are small two-word headers (pointer and length) that are cheaply copied by value while remaining immutable, whereas a Rust String owns its heap buffer and moves ownership by default, so duplicating it requires an explicit clone. Option D is tempting because copy-on-write is common in other languages, but Rust String is uniquely owned and Go strings do not use reference counting.
Read the full bite: Compare Go string and Rust &str/String types, mutability, UTF-8, ownership
Question 4 of 30
What happens if a caller ignores the error when parsing a string to an integer in Go versus Rust?
Show the answer
Answer: d · Go compiles silently because error checking is purely conventional, while Rust emits a must-use warning since Result is enforced by the type system
Go relies on programmer discipline to check the second error return value, so ignored errors compile silently. Rust's Result is must-use, so the compiler warns if it is not handled via match or ?; distractor A reverses these roles.
Read the full bite: Parse a string to integer in Go and Rust with errors
Question 5 of 30
When you create b[1:4] from a Go slice b, how does the resulting value compare to borrowing &v[1..4] from a Rust Vec?
Show the answer
Answer: d · The Go result is a new three-word header sharing the original array, while the Rust result is a two-word borrowed slice with no capacity field.
Go slicing creates a new header (pointer, length, capacity) that points into the same underlying array, whereas Rust's &[T] is a two-word fat pointer without capacity because it is only a borrowed view. Distractor B is tempting because one might assume slicing copies data, but Go only copies the descriptor and shares the backing array.
Read the full bite: Describe Go slice internals and compare to Rust slice and Vec
Question 6 of 30
Which solution best supports many concurrent readers on a typed Go map while keeping compile-time type safety?
Show the answer
Answer: d · Wrap the map in a struct with sync.RWMutex, using RLock for reads and Lock for writes
An RWMutex-wrapped map allows multiple simultaneous readers and retains compile-time type safety. sync.Map is tempting because it handles concurrency internally, but it stores interface{} values and therefore sacrifices compile-time type checking.
Read the full bite: How do you safely share a Go map across goroutines?
Question 7 of 30
You want a Rust function to read a string without taking ownership and accept both a String variable and a literal. Which parameter type achieves this?
Show the answer
Answer: b · &str
&str is a borrowed slice that coerces from &String and accepts literals directly, avoiding clones. &String is wrong because it rejects string literals, forcing callers to allocate a String they do not need.
Read the full bite: What type replaces String for read-only function parameters in Rust?
Question 8 of 30
What happens when you call .sum() directly on an iterator of Option<i32> that includes a None?
Show the answer
Answer: c · It returns None, short-circuiting the summation
The Sum<Option<U>> implementation returns None if any element is None, short-circuiting the entire operation. The other choices confuse this behavior with unwrap panics or assume automatic coercion of missing values.
Read the full bite: Sum Some values in Vec<Option<i32>>, ignoring None
Question 9 of 30
An abstract domain package and a concrete storage package import each other. Which refactor best restores clean architecture?
Show the answer
Answer: d · Define a storage interface in the domain layer and inject the concrete implementation at startup
Defining the interface in the domain layer and injecting the concrete implementation at startup inverts the dependency, which is the recommended pattern for layer violations. Merging is better reserved for packages with no clear hierarchy, and build tags are a red-flag workaround that avoids fixing the design.
Read the full bite: Why does Go forbid circular dependencies, and how do you resolve them?
Question 10 of 30
Why is putting core logic in lib.rs and keeping main.rs thin the idiomatic structure for a tool that is both a CLI and a reusable library?
Show the answer
Answer: b · Logic in lib.rs is importable by other crates and unit-testable, while main.rs just wires it to the CLI
Code in lib.rs forms a public, importable, testable API, so other crates and integration tests can use it; main.rs becomes a thin adapter. A package can hold both a binary and a library, contrary to the other options.
Read the full bite: Rust binary and library crates in one project
Question 11 of 30
You move a helper package into an internal directory to restrict its use to your module. Which statement about the effects is true?
Show the answer
Answer: a · External modules are blocked from importing it by the compiler, while sibling packages in the same module can still import it freely
The Go compiler rejects imports of internal packages from outside the module, yet packages inside the same module may import them normally. Option B is tempting because unexported identifiers also limit visibility, but they only restrict access within a single package, not across packages in a module.
Read the full bite: What is the purpose of the internal directory in Go?
Question 12 of 30
When building a multi-file Rust crate, what is the key difference between mod and use?
Show the answer
Answer: d · mod adds a module to the crate tree and tells the compiler where to find its source, whereas use merely creates a local shortcut to a path already in the tree.
mod tells the compiler to include a new module in the crate tree and locate its source file, while use only creates a local shortcut to an item already in that tree. Distractor A is wrong because use never inserts modules into the crate tree or declares anything new; calling both keywords imports is a common misconception.
Read the full bite: Explain the difference between mod and use in Rust
Question 13 of 30
What is the key difference in how Rust and Go prevent data races on shared mutable state?
Show the answer
Answer: a · Rust makes them a compile error via aliasing-XOR-mutability and Send/Sync; Go relies on runtime discipline and the race detector
Rust's borrow checker plus Send and Sync turn data races into compile errors, while Go has no compile-time guarantee and instead uses channels, mutexes, and the runtime race detector. The languages do not share identical rules.
Read the full bite: Rust borrow rules versus Go race prevention
Question 14 of 30
Why can Rust reclaim heap memory at a specific moment without the latency spikes typical of Go's GC?
Show the answer
Answer: b · Rust enforces ownership at compile time, so Drop deallocates heap memory deterministically when values go out of scope.
Rust's compiler tracks ownership and lifetimes, guaranteeing that heap memory is freed immediately when a value goes out of scope via Drop, eliminating non-deterministic pauses. Distractor D confuses Rust's default compile-time ownership with opt-in reference counting types like Rc and Arc, which do incur runtime overhead but are not the standard mechanism.
Read the full bite: How does Rust ownership avoid Go GC's non-deterministic pauses?
Question 15 of 30
An error wrapped with fmt.Errorf using %w contains an underlying *os.PathError. Which approach lets you safely extract its Path field?
Show the answer
Answer: d · Use errors.As with a pointer to a *os.PathError variable
errors.As traverses the unwrap chain and copies a matching *os.PathError into the target pointer so you can read its Path field. A direct type assertion only inspects the top-level error and silently fails when wrapping is present.
Read the full bite: Difference between errors.Is and errors.As in Go
Question 16 of 30
When is it appropriate to use unwrap or expect in production Rust code?
Show the answer
Answer: b · When an invariant is statically guaranteed, such as parsing a compile-time embedded asset that must exist
unwrap and expect are intended for unrecoverable invariant violations, not routine errors, and the card explicitly cites statically guaranteed cases like compile-time assets as legitimate production uses. Distractor B is tempting because many developers believe panicking is never acceptable in production, but the card identifies this as dogmatic overcorrection that ignores justified scenarios like poisoned mutexes or bundled static assets.
Read the full bite: What is the difference between unwrap and expect on Option and Result?
Question 17 of 30
What must you implement to let the ? operator automatically convert std::io::Error into a custom enum error type?
Show the answer
Answer: b · Implementing From<std::io::Error> for the custom enum
The ? operator desugars to a match that returns Err(From::from(err)) on failure, so implementing From<std::io::Error> for the custom enum is what enables automatic conversion. Manually using map_err on every call site is a common misconception that creates unnecessary boilerplate instead of leveraging the type system.
Read the full bite: How does the question mark operator use From to unify error types?
Question 18 of 30
When a nil pointer is boxed into an interface{}, why does a subsequent nil check on that interface return false?
Show the answer
Answer: b · The interface value carries type information, so it is not nil
An interface{} holding a nil pointer is non-nil because it stores dynamic type metadata alongside the data pointer. Distractor D is wrong because a bare type assertion panics only when the dynamic type does not match, not when the underlying concrete value is nil.
Read the full bite: Explain Go's empty interface, safe usage, and runtime risks
Question 19 of 30
When using impl Trait in argument position rather than Box<dyn Trait>, what trade-off does Rust make?
Show the answer
Answer: c · It generates a specialized function copy per concrete type, speeding up execution but bloating the binary.
impl Trait in argument position desugars to a generic parameter, triggering monomorphization that creates a specialized function per concrete type for zero-cost abstraction at the expense of binary size. Option D describes Box<dyn Trait>, which relies on a fat pointer and vtable lookup for runtime polymorphism instead of compile-time code duplication.
Read the full bite: Static dispatch with impl Trait versus dynamic dispatch with Box<dyn Trait>
Question 20 of 30
A Go type has a method with a pointer receiver that an interface requires. Why does a plain value of that type fail to satisfy the interface?
Show the answer
Answer: d · Pointer-receiver methods are in the method set of *T only, not T, so only a pointer satisfies the interface
The method set of T contains only value-receiver methods, while *T's set adds the pointer-receiver ones, so only a pointer satisfies an interface needing that method. Interfaces do not declare receivers, and values can satisfy interfaces when methods use value receivers.
Read the full bite: Value versus pointer receivers and interface satisfaction
Question 21 of 30
What is the key safety difference between Go's sync.Mutex idiom and Rust's Arc<Mutex<T>> when sharing mutable state across threads?
Show the answer
Answer: c · Rust makes locking structurally mandatory and rejects races at compile time, while Go relies on convention plus an optional runtime detector
Rust hides the data behind the lock guard and enforces Send/Sync at compile time, whereas Go's lock-data pairing is a convention checked only at runtime via the race detector. The 'equivalent' and 'no overhead' claims are false.
Read the full bite: Sharing mutable state: Go mutex vs Rust Arc Mutex
Question 22 of 30
In Rust, what happens to an async function's returned future immediately after you call the function but before awaiting or spawning it?
Show the answer
Answer: c · Nothing executes; the future is inert until a runtime polls it
Rust futures are lazy state machines that do nothing until polled by an executor like Tokio. Go's eager goroutine scheduling, not Rust's, runs work automatically when launched.
Question 23 of 30
Which pattern correctly uses context.WithCancel to manage a worker goroutine?
Show the answer
Answer: b · Derive a child context, pass it to the worker, and call cancel from the caller to unblock ctx.Done
The caller derives the child context, passes it to the worker, and later calls cancel to close ctx.Done so the worker exits cleanly. Option D is tempting because Done appears to signal cancellation automatically, but ignoring the CancelFunc leaks the child context until the parent is canceled.
Read the full bite: What is Go's context package and how do you use WithCancel?
Question 24 of 30
Which statement accurately contrasts Go's select and Tokio's select! when multiple branches are ready in a loop?
Show the answer
Answer: d · Go pseudo-randomly selects among ready cases to prevent starvation, while Tokio defaults to randomized polling but offers biased; for strict top-down evaluation with starvation risk.
Go's select mandatorily pseudo-randomizes among ready cases, making source order irrelevant, whereas Tokio's select! randomizes by default for fairness but accepts the biased; annotation for deterministic top-down polling, though this can starve later branches if earlier ones are always ready. Option C is tempting because it correctly describes Tokio's biased; mode but incorrectly claims Go uses source order.
Read the full bite: Go select vs Rust select! fairness and determinism
Question 25 of 30
What is the correct way to detect an unsynchronized concurrent memory access in a Go package during development?
Show the answer
Answer: d · Compile and run tests with go test -race to dynamically detect concurrent unsynchronized accesses
The -race flag is built into the go command and instruments binaries at compile time to dynamically detect concurrent unsynchronized memory accesses during test execution. The most tempting distractor, go vet, performs only static analysis and cannot catch actual runtime data races.
Read the full bite: What Go tool detects data races and how do you invoke it?
Question 26 of 30
Why do unit tests inside src/ typically require #[cfg(test)] while integration tests in the top-level tests/ directory do not?
Show the answer
Answer: d · Unit tests need #[cfg(test)] because they live inside the library crate, whereas integration tests in tests/ are compiled as separate crates only during testing.
Unit tests reside within the library crate, so #[cfg(test)] is needed to exclude them from normal builds; integration tests in tests/ are separate crates compiled only during cargo test by Cargo convention. Option C is wrong because Cargo already isolates the tests directory, making the attribute unnecessary.
Read the full bite: How does cargo differentiate unit and integration tests by location?
Question 27 of 30
On macOS, which command produces a pure Go ARM64 Linux binary in the current directory?
Show the answer
Answer: d · GOOS=linux GOARCH=arm64 go build
go build leaves the compiled binary in the current directory, whereas go install moves it to $GOBIN. Pure Go cross-compilation requires only GOOS and GOARCH; external C cross-compilers or QEMU are unnecessary.
Read the full bite: Difference between go build and go install? Cross-compile for ARM64 Linux?
Question 28 of 30
You add an optional dependency libwebp-sys to your crate. Which statement accurately describes how consumers can enable it and how it affects compilation?
Show the answer
Answer: b · Consumers enable it by adding features = ["libwebp-sys"] to their dependency, and your code can gate WebP support with #[cfg(feature = "libwebp-sys")]
Optional dependencies implicitly create a Cargo feature with the same name, so consumers enable them via features = [...] and authors gate code with #[cfg(feature = ...)]. The dep: prefix is used to hide or group optional dependencies behind custom feature names, not to expose them.
Read the full bite: Explain Cargo features and how to define and enable them
Question 29 of 30
Which combination correctly implements memory-efficient, line-by-line streaming for a multi-gigabyte file in Go and Rust?
Show the answer
Answer: b · Go: bufio.Scanner with ScanLines, checking scanner.Err() after the loop; Rust: BufReader with read_line into a reused String buffer
Go's bufio.Scanner streams lines with a hidden buffer but requires an explicit scanner.Err() check after the loop, while Rust's BufReader paired with read_line and a reused String avoids per-line allocations. Distractor D is tempting because it names the right types, yet skipping scanner.Err() misses I/O errors and collecting lines into a Vec<String> loads the entire file into RAM, defeating streaming.
Read the full bite: Compare efficient line-by-line file reading in Go and Rust
Question 30 of 30
Which statement accurately contrasts the ergonomics of propagating an I/O error up the call stack in Go and Rust?
Show the answer
Answer: c · Go requires explicit if err != nil checks and manual return statements, while Rust's ? operator can propagate a compatible Result with minimal boilerplate.
Go makes error checking explicit and voluntary, allowing callers to ignore the error tuple, whereas Rust's Result and ? operator enforce propagation or handling at compile time. Option D is tempting because it sounds like a plausible enforcement story, but it reverses the two languages: Go does not stop you from discarding errors, while Rust warns or errors on an unhandled Result.
Read the full bite: Compare Go's error tuples to Rust's Result for I/O
Could you explain these out loud?
That is what an interview actually tests. Tezvyn gives you questions like these with what the interviewer is really checking, the answer that lands, and the mistake that ends the conversation, in the four minutes before your next meeting.
The iPhone app is on the way
We are building it. Until it lands, nothing here is held back from you: every interview card, your saved cards, streaks and the job board all work in Safari, plus hundreds of free practice quizzes of thirty questions each. Sign in and it all carries over to the app the day it arrives.
Want it as an icon? Tap Share at the bottom of Safari, then Add to Home Screen. It opens full screen and the cards you have read stay available offline.