· 6 min read ·

What You Keep When You Strip Rust's Borrow Checker and Run on Go

Source: lobsters

The premise of Lisette is stated simply: Rust syntax, Go runtime. It sounds like the language equivalent of putting a sports car body on a minivan chassis, but there is a more principled idea underneath it.

To understand why this combination makes sense, you have to separate two things that Rust conflates out of necessity: the syntax and type system on one side, and the memory management model on the other. Rust’s borrow checker, lifetime annotations, and ownership rules are not decorative features. They exist entirely to enable memory safety without a garbage collector. Remove the GC requirement, and a significant fraction of Rust’s complexity becomes negotiable.

What Rust’s Syntax Actually Gives You

When developers say they appreciate Rust’s ergonomics, they usually mean a cluster of features that have nothing to do with memory management:

  • Algebraic data types with enum variants that carry data
  • match expressions with exhaustive pattern matching
  • Traits as a mechanism for ad-hoc polymorphism without inheritance
  • First-class Result<T, E> and Option<T> for error and null handling
  • Rich type inference that reduces annotation noise
  • Closures with captured environments
  • Iterator combinators that compose cleanly

Consider the difference between error handling in Go and Rust-style languages:

// Go
func process(input string) (string, error) {
    val, err := parse(input)
    if err != nil {
        return "", err
    }
    return fmt.Sprintf("%v", val), nil
}
// Rust
fn process(input: &str) -> Result<String, Error> {
    let val = parse(input)?;
    Ok(format!("{}", val))
}

The Rust version is more readable not because of ownership or lifetimes, but because Result is a proper algebraic type that composes, and the ? operator threads errors through the call stack without ceremony. None of that requires a borrow checker. It requires algebraic data types and good syntax sugar.

Lisette brings exactly this class of ergonomic improvement to a language that compiles to use Go’s runtime. The lifetime annotations, the &, &mut, and explicit borrow distinctions all go away, because with a garbage collector you simply do not need them.

What Go’s Runtime Actually Provides

The Go runtime is not just a garbage collector. It is an M:N scheduler that maps goroutines onto OS threads, a channel implementation for structured communication between concurrent tasks, a preemptive scheduling system that lets long-running goroutines yield without explicit cooperation, and a memory allocator tuned for the allocation patterns of server-side software.

Goroutines start at around 8KB of stack space and grow as needed, which means you can spawn hundreds of thousands of them without exhausting memory. This is a fundamentally different concurrency model than Rust’s async/await, where the compiler transforms your code into a state machine and you must opt into an async executor. Go’s model is simpler to reason about for I/O-bound concurrent workloads, and for most server software, the overhead difference does not matter.

The Go GC has also improved considerably. Go 1.25’s Green Tea GC cuts GC overhead by up to 40% on certain workloads by shifting tracking from the object level to the span level. Pause times are typically sub-millisecond for most applications. For everything except hard real-time systems or latency-critical loops, this is acceptable.

Lisette inherits all of this for free. Goroutines, channels, the scheduler, and the GC are available without having to implement them. This is a significant practical advantage: writing a correct concurrent runtime is one of the hardest problems in systems programming, and Go’s has years of production hardening at scale.

The Prior Art for This Idea

The pattern of taking a better syntax layer and placing it on top of a proven runtime has worked before.

Elixir is the most successful example. It brought Ruby-inspired syntax and a macro system to the Erlang VM, making BEAM’s actor model and fault-tolerance machinery accessible to developers who found Erlang’s Prolog-derived syntax off-putting. The runtime primitives are identical; the ergonomics improved dramatically.

Kotlin did the same for the JVM. Java’s syntax was verbose and its null handling was dangerous; Kotlin addressed both while retaining full JVM interoperability. The JVM’s JIT, GC, and ecosystem were worth keeping even if Java’s language design was not.

Clojure took a more radical approach, bringing Lisp semantics to the JVM, trading syntactic familiarity for a fundamentally different programming model. The runtime, again, was the stable foundation.

In all three cases, the runtime represented years of engineering investment in scheduling, garbage collection, and platform compatibility that would have been impractical to replicate from scratch.

What You Lose and What That Costs

The tradeoff is real. Rust’s ownership system guarantees that data races cannot happen at compile time. It eliminates use-after-free bugs entirely. It lets the compiler prove, statically, that your concurrent code is correct. Lisette, using Go’s runtime and GC, gives up all of these guarantees. You can write code with data races. The compiler will not stop you.

Go addresses this partly through convention (do not share memory, communicate via channels) and partly through its race detector (go build -race). These are runtime tools, not compile-time proofs. For most production software, runtime detection is sufficient; the race detector catches issues in testing before they reach production. But for the class of software where Rust’s guarantees matter most, systems software with tight correctness requirements, Lisette is a different tool for a different problem.

Performance is the other gap. Rust’s zero-cost abstractions and lack of GC make it the right choice for latency-sensitive code, embedded systems, and situations where you cannot afford pause times. Lisette, like Go, accepts GC pauses as a design constraint. The throughput is good; the tail latency profile is different from Rust.

Why This Is Worth Building

For a large category of software, Go’s runtime characteristics are genuinely sufficient. Web services, data pipelines, internal tooling, command-line applications: these workloads tolerate GC pauses and do not require compile-time data race proofs. What they would benefit from is a richer type system than Go provides, proper algebraic data types, better generics, and pattern matching.

Go’s generics, added in Go 1.18, improved the situation but remain more limited than Rust’s trait system. Go still lacks sum types in any first-class form; the idiomatic Go approach to “this value could be one of several things” is either an interface with a type switch, or separate fields with a discriminant. Neither is as expressive or as safe as a proper enum.

There is a real audience of developers who find Rust’s learning curve around lifetimes and the borrow checker a blocker, not because they want to write unsafe code, but because the mental overhead of satisfying the borrow checker does not pay off for their domain. These developers are currently using Go and accepting its type system limitations. A language with Rust’s syntax and expressiveness on Go’s runtime is a reasonable answer to that problem.

The deeper question Lisette raises is whether the two halves can actually be separated cleanly. Rust’s syntax was designed with ownership in mind; some of its conventions, the distinction between &T and &mut T, the way closures capture environments, only make full sense in context of the ownership model. Translating that syntax into a GC’d setting requires decisions about what to keep literally and what to reinterpret. Those decisions define what the language actually is.

Lisette is early-stage, and the answers to those questions are still being worked out. But the design space it is exploring, ergonomic Rust-influenced syntax on a production-proven concurrent runtime, is a legitimate one with clear precedent. The implementation will determine whether it closes the gap that many Go developers feel without introducing new ones of its own.

Was this interesting?