A Rust runtime for Go people.
A Go-style standard library and runtime, implemented in
no_std Rust on top of raw Linux syscalls. No
glibc, no std, no Tokio. Goish ships its
own _start, page allocator, size-class heap, M:N
scheduler, channels, select!, sync
primitives, async preemption, and ~80 ports of Go standard-library
and golang.org/x packages. Because it's
no_std, Rust compiles the whole thing, so you can ship
a static binary just like Go.
use goish::{go, KB}; use goish::sync::WaitGroup; #[goish::main] fn main() { let wg = &WaitGroup::new(); wg.Add(1_000_000); for i in 0..1_000_000 { // Explicit 2 KiB stack, sub-page allocated from the chunked // stackpool - the opt-in for extreme spawn density. Everyday // code just writes go!(move || ...) and never sizes a stack. go!(stack(2 * KB), move || { do_work(i); wg.Done(); }); } wg.Wait(); }
A million real goroutines on 13 OS threads: ~2 GiB virtual, ~2.4 KiB peak RSS per goroutine at sub-page density. 16-core x86_64, kernel 6.8.
reads like Go
Multi-return, if err != nil, channels, select!
Public API surfaces use Go's lowercase types: string,
slice<T>, map<K, V>,
chan<T>, int. Vec<u8>
and &str don't appear in public signatures. These
excerpts are verbatim from
goginx, the nginx clone in the examples tree.
fn get(url: string) -> (int, string, string) { let (mut resp, err) = http::Get(url.clone()); if err != nil { return (-1, fmt::Sprintf!("get %s: %v", url, err), string("")); } let (body, _) = io::ReadAll(&mut resp.Body); let _ = io::Closer::Close(&mut resp.Body); return ( resp.StatusCode, string(body), resp.Header.Get("Content-Type"), ); }
select! { let _ = done.Recv() => {}, let _ = (time::After(time::Second * 10)).Recv() => { fail("drain: timed out waiting for done"); }, }
built like a runtime
Go 1.25's runtime idioms, down to raw syscalls
The G/M/P scheduler is ported verbatim from Go 1.25's
runtime/proc.go: lock-free per-P run queues,
coprime-permuted work stealing, SIGURG async preemption, sysmon, and a
per-P epoll netpoller. Underneath sit Go's radix-tree page allocator and
67 size classes, reimplemented over raw mmap.
what's inside
What you get
goroutines & scheduler
M:N stackful goroutines with async preemption. Bare
go!() costs ~one physical page; overflow hits a guard
page with a spawn-site diagnostic. go!(stack(N), ...)
opts into sub-page density.
channels, select!, sync
Unbuffered, buffered, nil, close. Intrusive sudog wait queues mean
zero allocator round-trips on park/unpark.
sync::{Mutex, RWMutex, WaitGroup, Once},
context, and a sysmon-driven timer heap.
net/http, production-hardened
HTTP/1.1 with keep-alive, Go 1.22 ServeMux wildcards,
middleware, streaming, reverse proxy, and graceful
Shutdown(ctx). Blocking reads park the goroutine, not
the thread. HTTPS via goish's own TLS 1.3 stack.
crypto, ported ground-up
aes, sha256, ecdh,
ed25519, rsa, x509,
chacha20poly1305 and more. These back the client TLS
1.2/1.3 and server TLS 1.3 handshakes, plus an SSH-2.0 client.
memory without a GC
Go's page allocator and size-class heap, minus the collector:
demand-paged MAP_NORESERVE arenas, per-P caches, and a
lock-free hot path. Rust ownership does the reclaiming.
~80 stdlib packages
fmt, io, bufio,
encoding/json (v2), regexp,
compress, archive/tar,
testing, log/slog,
math/big, reflect and more, all Go
1.25-faithful, plus golang.org/x ports.
comparison
Where goish sits
| goish | Go | Pure Rust async | |
|---|---|---|---|
| Concurrency | M:N, stackful Gs | M:N, growable stacks | stackless futures |
| Stack per G | 1 MiB reserved, lazy-commit (2 KiB opt-in) | 2 KiB growable | one Future per task |
| Preemption | SIGURG (async) | SIGURG (async) | cooperative .await |
| 1M goroutines | yes, in 2 GiB virtual | yes | needs runtime tuning |
| Standalone binary | yes, no glibc, no ld.so | static linkable | needs std |
| GC | none (manual mheap) | concurrent mark+sweep | none |
Goish is not a clone of Go. It ports the runtime
idioms into a Rust ownership model. Go's
morestack is impossible here, so goish grows the other
way: virtual reservations the kernel commits on touch. No GC either
way. It is single-target
(x86_64-unknown-linux-gnu) and under active development.
The e2e suite spans 406 examples.
build & run
One target. One binary.
Rust 1.90+, Linux x86_64. Binaries are statically linked:
cat /proc/<pid>/maps shows only the binary itself plus
mmap'd arenas.
# the library cargo build --target x86_64-unknown-linux-gnu --release # the million-goroutine demo cargo build --target x86_64-unknown-linux-gnu --release --example spawn_million ./examples/spawn_million.sh # goginx: the nginx clone written in goish cargo build --target x86_64-unknown-linux-gnu --release --example goginx