Skip to content

Limbo for Go programmers

Limbo and Go share an ancestor — Rob Pike and Ken Thompson worked on both — so if you write Go, the concurrency model will feel like home. What differs is mostly modules.

GoLimboNotes
go f(x)spawn f(x)Preemptively scheduled Dis threads
ch := make(chan int)ch := chan of intUnbuffered by default
make(chan int, 8)ch := chan[8] of intBuffered
ch <- v / v := <-chch <-= v / v := <-ch
select { case … }alt { v := <-ch => … }alt predates select
packagemodule: .m interface + .b implementationLike a header, but typed and checked
import "x"include "x.m"; then x = load X X->PATH;Explicit, at runtime
structadtCan carry methods
interface + type switchpick (tagged union)Closed set of variants, not structural
x := 5x := 5Same type-inferring declaration
func f() (int, error)f(): (int, string)Error is conventionally a string; nil means ok
panic / recoverraise "fail:…" / exception blocksException strings matched by pattern
slicesarray of byte, sliced a[1:5]Also list of T with hd/tl/::
rangefor(l := lst; l != nil; l = tl l)No range statement
int64bigint is 32-bit
runeintStrings index to code points as int

A Limbo program is a set of modules linked at runtime with load, each declaring a PATH constant naming its compiled .dis file:

include "sys.m";
sys: Sys;
init(nil: ref Draw->Context, args: list of string)
{
sys = load Sys Sys->PATH;
sys->print("hello\n");
}

load returning nil is a real and common failure — check it.

That explicitness is not ceremony. Loading a module is a filesystem operation, so module access is a capability like any other file — which is what lets a namespace decide which implementation of an interface a process gets. It is the same mechanism as everything else in Namespaces.

worker(results: chan of string)
{
results <-= "done";
}
init(…)
{
results := chan of string;
spawn worker(results);
alt {
r := <-results =>
sys->print("%s\n", r);
<-timeout =>
sys->print("timed out\n");
}
}

>> is a logical shift. In Go you sign-extend with (x << n) >> n. In Limbo >> does not sign-extend, so that idiom silently produces the wrong value. Sign-extend explicitly — if(x > 128) x -= 256; for a byte. This has caused real bugs in this tree.

No defer. Clean up at exit points, or structure with a single return path. { … } exception e { … } exists for the error path.

int is 32 bits. Use big for 64-bit values, and mind the conversions — they are explicit (big x, int b).

spawn is cheap, not free. Dis threads cost more to create than goroutines, though channel operations are comparably fast. Spawn for structure, not per datum.

Errors are strings by convention. nil means success; fallible functions return (result, string) or raise "fail:reason". %r prints the last system error:

sys->fprint(sys->fildes(2), "open failed: %r\n");

Inferno’s sh is rc-style. &&, ||, and POSIX loops are not available and are flagged in review. The full notes are in LIMBO-FOR-GO-PROGRAMMERS.md.


With that, writing a 9P service should read like ordinary code.