Designing a logging abstraction: Go interfaces vs Rust traits
ability to design polymorphic abstractions and explain dispatch.
define a Logger interface/trait with a write method; Go interfaces are always dynamically dispatched; Rust lets you choose static dispatch (impl Trait/generics) or…
WHAT THIS TESTS It checks whether you can model an abstraction over multiple implementations and explain precisely how each language dispatches the call, including the choice Rust gives you.
A GOOD ANSWER COVERS The abstraction is a single-method contract for writing a log line. In Go you declare type Logger interface { Log(msg string) error } and write file, network, and stdout types each with a Log method; the consumer is func Process(l Logger, msg string) error. Conformance is implicit. In Rust you declare trait Logger { fn log(&self, msg: &str) -> std::io::Result<()>; } and impl it explicitly for each type. You then choose the consumer signature: fn process<L: Logger>(l: &L, msg: &str) uses generics and is monomorphized into static, often inlined calls, while fn process(l: &dyn Logger, msg: &str) takes a trait object and dispatches dynamically through a vtable.
KEY DISPATCH DIFFERENCE Go interface method calls are always dynamic: the interface value carries a type descriptor and pointer, and the call indirects through a method table. Rust defaults to static dispatch via monomorphization when you use generics or impl Trait, paying no runtime indirection; you must explicitly write dyn Trait (typically behind a reference or Box) to get dynamic dispatch. To hold a mixed collection of loggers, Go uses []Logger and Rust uses Vec<Box<dyn Logger>>.
COMMON WRONG ANSWERS Saying Rust traits are always dynamically dispatched. Forgetting that a heterogeneous collection in Rust needs dyn. Claiming Go offers a static-dispatch option for interfaces.
LIKELY FOLLOW-UPS What is object safety and why can not every trait be a dyn? What is the performance cost of a vtable indirection? When would you prefer Box<dyn Logger> over generics?
ONE CONCRETE EXAMPLE Logging to all destinations: in Go, loggers := []Logger{file, net, stdout}; for _, l := range loggers { l.Log(msg) } dispatches each call dynamically. In Rust, let loggers: Vec<Box<dyn Logger>> = vec![Box::new(file), Box::new(net), Box::new(stdout)]; for l in &loggers { l.log(msg)?; } also dynamic, but if you instead wrote a generic function over one concrete logger type, those calls would be statically dispatched and inlinable.
Get five bites like this every day.
Tezvyn delivers a daily feed of 60-second tech bites with quizzes to lock in what you learn.