· 7 min read ·

What You Actually Get When You Bolt Rust Syntax onto the Go Runtime

Source: lobsters

When a new language describes itself as “Rust syntax, Go runtime”, it’s making a very specific claim about where the value in each language lives. The framing implies that Go’s runtime is good enough to keep, and that Rust’s syntax and type system are good enough to steal, but that neither language as a whole is the right tool. That’s a defensible position. It’s worth examining what it actually means in practice.

What the Go Runtime Actually Gives You

Go’s runtime is not just a garbage collector. It’s a scheduler, a memory allocator, a stack manager, and an I/O multiplexer, all integrated into a coherent unit that the language was designed around from the start.

The goroutine scheduler uses an M:N model: many goroutines mapped onto a smaller pool of OS threads. Initial goroutine stack size is around 2-8 KB, growing dynamically as needed, compared to the 1-8 MB typical of OS threads. This means you can spawn hundreds of thousands of goroutines without exhausting memory. The scheduler became fully preemptive in Go 1.14, meaning goroutines can be interrupted at any safe point rather than only at function calls, which eliminated a class of latency bugs in CPU-bound workloads.

The garbage collector is concurrent, tri-color mark-and-sweep, targeting stop-the-world pauses below one millisecond in typical workloads. It’s tunable via GOGC and GOMEMLIMIT. For network services, this is genuinely competitive. Go’s runtime integrates with the OS I/O poller (epoll on Linux, kqueue on macOS, IOCP on Windows) at a level that most language runtimes don’t reach, so async I/O in Go is fast without the programmer managing any of the machinery.

Channels give you CSP-style concurrency as a first-class primitive. select lets you wait on multiple channel operations simultaneously. These aren’t syntax sugar over POSIX threads; they’re expressions of how the scheduler thinks about work.

If you’re building a concurrent network service, Go’s runtime is one of the most production-tested options available. Java’s virtual threads (Project Loom, finally stabilized in Java 21) are the closest competition. The BEAM (Erlang’s VM) has similar per-process isolation but different GC characteristics. Python’s asyncio and JavaScript’s event loop are fundamentally single-threaded. Rust’s async story is powerful but requires explicit async/await plumbing and an external executor like Tokio.

What People Actually Want from Rust’s Syntax

This is the more interesting question, because Rust’s syntax and Rust’s semantics are harder to separate than Go’s. The borrow checker isn’t syntax; it’s a type system layer that enforces ownership. The question is what you get if you take Rust’s surface language without the ownership tracking.

The honest answer: quite a lot.

Rust’s enum system gives you genuine algebraic data types:

enum Shape {
    Circle { radius: f64 },
    Rectangle { width: f64, height: f64 },
    Triangle { base: f64, height: f64 },
}

This is meaningfully different from Go’s approach, which requires interface types or struct embedding to model the same thing, without exhaustiveness checking. In Go, you can switch on an interface and the compiler won’t tell you when you’ve missed a case.

Rust’s match expression is exhaustive by default:

let area = match shape {
    Shape::Circle { radius } => std::f64::consts::PI * radius * radius,
    Shape::Rectangle { width, height } => width * height,
    Shape::Triangle { base, height } => 0.5 * base * height,
};

In Go, the equivalent using a type switch looks like:

switch s := shape.(type) {
case Circle:
    area = math.Pi * s.Radius * s.Radius
case Rectangle:
    area = s.Width * s.Height
// You can forget Triangle and the compiler says nothing
}

Option<T> and Result<T, E> replace nil and multi-return error values. The ? operator threads errors through a call chain without explicit if err != nil at every step:

fn process(path: &str) -> Result<String, io::Error> {
    let content = fs::read_to_string(path)?;
    let trimmed = content.trim().to_string();
    Ok(trimmed)
}

In Go, this is:

func process(path string) (string, error) {
    content, err := os.ReadFile(path)
    if err != nil {
        return "", err
    }
    return strings.TrimSpace(string(content)), nil
}

Neither is obviously wrong. But the Rust version encodes the fallibility into the return type in a way that forces callers to handle it, and the ? operator composes without cluttering the happy path. The Go version is more explicit, sometimes to its detriment.

Traits give you zero-cost polymorphism with explicit interface implementation, which is more structured than Go’s implicit satisfaction of interfaces. impl blocks separate method definitions from struct declarations, which scales better in large files.

Notably absent from this list: the borrow checker. Ownership and lifetimes are the parts of Rust’s type system that most people find hardest to learn and that most directly drive the language’s safety properties. A language that takes Rust’s syntax without the borrow checker has either replaced it with GC (which is exactly what Go’s runtime provides) or left that safety gap open.

Gleam as Prior Art

The closest existing language to this design is Gleam, which takes ML/Haskell-style syntax with Rust-inspired type annotations and compiles to either Erlang bytecode (running on the BEAM) or JavaScript. Gleam’s pitch is strikingly similar: take the ergonomics of a modern statically-typed language, run it on a battle-tested concurrent runtime.

Gleam’s error handling looks like this:

pub fn read_username_from_file() -> Result(String, String) {
  use contents <- result.try(simplifile.read("username.txt"))
  Ok(string.trim(contents))
}

The use expression is Gleam’s equivalent of ?, a callback-inversion syntax that flattens nested closures. It’s Rust-flavored in feel without being Rust syntax.

Gleam has been production-ready since version 1.0 in March 2024, and the BEAM’s actor model gives it similar properties to Go’s goroutines: lightweight processes, fast spawning, preemptive scheduling, per-process GC. The main difference is isolation: BEAM processes are isolated by default (no shared memory), while goroutines share the heap. Go’s approach has higher throughput for shared-state workloads; the BEAM’s approach has better fault isolation.

Other relevant predecessors include Hare, which takes a C-like simplicity-first approach but adds tagged unions and proper error types without GC; Vale, which experiments with “generational references” as an alternative to both GC and borrow checking; and Ante, which explores algebraic effects on top of a Rust-like syntax. None of these use Go’s runtime specifically.

There have been earlier “compile to Go” experiments, and tools like gopherjs transpile other languages to Go source. But building a language that natively targets Go’s runtime as a first-class host is a different and more ambitious undertaking.

The Trade-Off in Plain Terms

What you gain with a language like Lisette is the ability to write in a type system that was designed from the start for expressiveness (algebraic types, exhaustive matching, Result/Option) while running on a runtime that was designed for concurrent network services and that has ten-plus years of production tuning behind it. You don’t pay the cognitive overhead of the borrow checker, and you don’t write if err != nil on every third line.

What you give up, or trade away, is coherence. Go’s simplicity is not just syntax; it’s a set of deliberate constraints that make the language easy to read across teams and easy to tool. Adding Rust-style type constructs creates a richer language, but richness has costs. New contributors need to learn enum dispatch and trait-like polymorphism instead of just interfaces. The tooling story for a new language is always thinner than for Go, regardless of how good the underlying runtime is.

There’s also a question of where the borrow checker’s guarantees go. Rust’s ownership model doesn’t just prevent memory leaks; it prevents data races at compile time. With a GC backing you, the memory safety story changes shape but doesn’t disappear entirely. Data races are still possible unless the runtime provides race detection (Go does, via -race), and the type system alone can’t substitute for ownership tracking in low-level code.

For the specific workload of a network service written by a team that finds Go’s error handling and type system limiting but doesn’t need zero-cost abstractions or the safety guarantees of ownership, this trade-off is genuinely attractive. Whether Lisette’s execution delivers on that promise is a question only time and production use will settle.

Why This Keeps Getting Tried

Language designers keep coming back to the “borrow the runtime” pattern because it’s the correct answer to a real problem. Writing a production-grade concurrent runtime from scratch is a multi-year effort with a high failure rate. Go’s runtime works. The BEAM works. The JVM works. V8 works. Building a new language on top of an existing runtime is a tractable engineering problem; building both at once is not.

The gap that projects like Lisette are filling is real: Go’s type system leaves expressiveness on the table, and Rust’s safety guarantees come with a learning curve that many teams aren’t willing to pay. A language that threads between them, using Go’s operational strengths and Rust’s ergonomic strengths, has a coherent target audience.

The execution challenge is that language design involves a thousand small decisions that interact in non-obvious ways, and the languages being borrowed from made those decisions over many years with extensive community feedback. The history of “better Go” and “easier Rust” experiments is long enough that healthy skepticism is warranted. But the underlying motivation is sound, and Lisette is worth watching if the concurrent service space is where you spend your time.

Was this interesting?