· 6 min read ·

Rust's Syntax, Go's Runtime, and the Case for Splitting Them Apart

Source: lobsters

There’s a recurring pattern in programming language history: a runtime that works well gets stuck with a syntax that limits its audience, or a syntax that developers love gets paired with a runtime that frustrates them in production. Lisette, reachable at lisette.run, takes a direct swing at that problem by decoupling the two. It offers Rust’s syntax and Go’s runtime as a deliberate combination, not as a compromise or an accident.

That framing deserves unpacking, because “Rust syntax” and “Go runtime” mean specific, concrete things.

What Rust’s Syntax Actually Provides

Rust’s reputation is dominated by its ownership model and borrow checker. But strip those away, and what remains is a genuinely well-designed surface language. Algebraic data types with exhaustive pattern matching, Result<T, E> and the ? operator for error propagation, trait-based generics that resolve at compile time, iterator adapters with clean chaining syntax, and let bindings with strong type inference. These are not tied to the borrow checker at all. You can have all of them in a garbage-collected language.

// Pattern matching on an enum with data
match response {
    Ok(val) if val.status == 200 => process(val),
    Ok(val) => log_unexpected(val),
    Err(e) => return Err(e.into()),
}

This code reads clearly. The exhaustiveness check catches missing cases. The ? short-circuits errors without exception machinery. None of this requires a borrow checker. It requires a compiler that takes types seriously and a syntax that makes the structure of data flow visible.

For contrast, Go’s syntax handles none of this elegantly. Error handling is the famous if err != nil repetition, which works but scales poorly as a reading experience. Go 1.22 and later releases have continued improving the language incrementally, but the core syntax was frozen early and the design choices reflect a deliberate minimalism that not everyone finds ergonomic. The language team has been explicit about this philosophy, and it has served Go’s adoption goals well. But it leaves room for alternatives.

What Go’s Runtime Actually Provides

Go’s runtime is one of the more underrated pieces of systems software in the ecosystem. The M:N goroutine scheduler, often called GMP (Goroutines, OS Threads, Processors), has been refined over more than a decade. Goroutines are cheap to create, typically a few kilobytes of stack that grows on demand, compared to OS thread stacks that start at one to eight megabytes. A program can run hundreds of thousands of goroutines without exhausting memory.

The scheduler uses work stealing across logical processors, and goroutines yield cooperatively at function call boundaries and system calls. Since Go 1.14, asynchronous preemption was added using signals, which solved the long-standing problem of tight loops starving the scheduler. The result is a runtime that handles high concurrency gracefully without requiring the programmer to reason explicitly about thread pools.

Go’s garbage collector has similarly matured. The concurrent tri-color mark-and-sweep GC introduced in Go 1.5 has been incrementally improved to reach sub-millisecond pause times in most workloads by Go 1.19. The runtime also handles stack scanning precisely, which gives the GC accurate roots without conservative scanning overhead.

Channels give you typed, synchronized communication between goroutines, and select gives you a clean multiplexer over multiple channel operations. This concurrency model, communicating sequential processes from Tony Hoare’s CSP paper, is genuinely good. It fits a wide range of problems without requiring the programmer to manage mutexes directly in most cases.

The Prior Art: Languages That Split Syntax from Runtime

Lisette is not the first language to make this kind of bet. The pattern of pairing a new syntax with an existing, proven runtime appears repeatedly.

Gleam pairs a statically typed, ML-influenced syntax with the BEAM virtual machine, the runtime behind Erlang and Elixir. The BEAM’s actor model and fault tolerance are well-proven at scale, running systems like WhatsApp’s backend. Gleam gets to skip building a distributed, fault-tolerant runtime from scratch and instead focuses on giving BEAM a proper type system. The 1.0 release in 2024 validated that approach.

Elixir itself is another example: Ruby-like syntax on the BEAM, with macros for metaprogramming. It expanded the BEAM’s audience significantly without requiring changes to the runtime itself.

Crystal went the other direction, keeping Ruby’s syntax but compiling to native code with a Boehm-Demers-Weiser conservative GC, then adding fibers for concurrency. The syntax-runtime separation there was across a much larger gap.

On the JVM, Scala, Kotlin, and Clojure all demonstrate that a runtime’s properties, type erasure, JIT warm-up characteristics, garbage collection, threading model, can be shared across languages with very different syntaxes and semantics.

What makes Lisette’s specific bet interesting is that Go’s runtime is not a virtual machine with a stable ABI in the way the JVM or BEAM are. Targeting it requires either compiling to Go source code (transpilation), which has significant limitations around FFI, type system fidelity, and compile times, or interfacing with Go’s runtime at a lower level, which is considerably more complex.

The Borrow Checker Absence and What That Means

The most significant thing Lisette gives up, assuming it targets Go’s GC rather than implementing its own memory management, is Rust’s ownership guarantees. The borrow checker exists to enable memory safety without a garbage collector. Without the GC, you need the borrow checker. With the GC, the borrow checker’s primary justification disappears.

This is a reasonable trade. Ownership and lifetimes are the hardest part of Rust for new users. The Rust survey data from 2023 consistently showed lifetimes and the borrow checker as the primary difficulty areas. A language that offers Rust’s pattern matching, algebraic types, trait generics, and ? operator without requiring ownership annotations would be substantially easier to learn.

The downside is that you lose the compile-time data race prevention that comes with Rust’s ownership model. Go relies instead on its race detector, which is a runtime tool rather than a compile-time guarantee. That’s a meaningful difference in safety guarantees, though in practice Go’s concurrency primitives, especially channels and sync.Mutex, are safe enough for most programs when used with discipline.

You also lose the performance characteristics that ownership enables: stack allocation for most values, no GC pauses, deterministic destructors for RAII patterns. Go’s GC is good but not free. Latency-sensitive applications that would benefit from Rust’s zero-cost abstractions and no-GC model would not benefit from Lisette’s runtime choice.

Where This Makes Sense

The combination makes most sense for the same workloads that Go already handles well: network services, API backends, CLI tools, and concurrent data processing pipelines. These are domains where GC pauses below a millisecond are acceptable, where goroutine-based concurrency is a productivity win, and where developer experience and compile times matter more than squeezing the last ten percent of performance.

In those domains, Rust’s syntax genuinely offers ergonomic improvements over Go’s. Error propagation with ? instead of if err != nil chains, pattern matching instead of type switch statements, and sum types that make invalid states unrepresentable all improve code clarity. A language that packages those improvements on top of a runtime that developers already trust and know how to operate has a real value proposition.

Whether Lisette’s implementation delivers on that premise depends heavily on the compiler’s maturity, the quality of the standard library, and whether interop with the Go ecosystem is practical. The Go ecosystem’s package ecosystem through the module system is one of the runtime’s less-discussed assets: years of well-maintained packages for everything from database drivers to HTTP middleware.

Language experiments in this space have mixed track records. Some, like Gleam, reach production use and sustained communities. Others remain perpetually early-stage. Lisette is worth watching because the design thesis is coherent: the pairing it proposes addresses real friction points that real developers encounter, and it does so without requiring a new runtime to be built and proven from scratch.

Was this interesting?