Reference / language + runtime
The exact shapes.
This is the page you keep open in another tab. You know what you want to do and you just need to know how it’s spelled. It’s a lookup page, so it’s terse on purpose. If you want the why, the tutorials and the architecture page do that better.
Goer has two sides, and so does this page. imgo is the cut-down Go you write processes in (a process being one of Goer’s small private workers). The Go API is what your regular Go code calls to start them and look after them.
Values
imgo looks like Go, but it treats data differently. A value belongs to whoever holds it. Nobody shares it and nobody changes it behind your back. Structs, slices, strings, and maps all live in the memory of the one process that owns them.
| What | How you write it | Good to know |
|---|---|---|
| numbers and basics | int · int8…int64 · uint…uint64 · float32/float64 · bool · string · []byte | They behave like Go’s at every size, wraparound included. One thing imgo pins down that Go leaves to the platform: turning a float into an integer type that can’t hold it gives the same answer everywhere (NaN becomes 0, too-big clamps to the limit). |
| structs | struct { Name string } | You can read and write nested fields (a.B.C). Changing a struct you hold never changes anyone else’s copy. |
| methods | func (a Account) Deposit(n int) Account | Value receivers only, so a method returns the new value. Method names have to be unique in the package. |
| slices and arrays | []T | make([]T, n), append, len, range, slicing, copy, clear. A slice is a plain value: assigning or passing one gives a separate copy. Nested writes work: s.Items[i] = v, grid[i][j] = v, xs[i].F = v. |
| maps | map[K]T | Keys can be strings, integers, floats, or bools, including named types. delete and clear work. A map is a plain value too. |
| operators | + - * / % · & | ^ &^ << >> · every op= · ++/-- · min/max | All of Go’s arithmetic and bit operators, on variables, fields, and elements. |
| variables | var · := · a, b = b, a · const · iota | var with or without a value, several names at once, swapping, and constants (your own and imported ones). |
| strings | len · s[i] · s[a:b] · + · range · []rune(s) · []byte(s) | Same as Go. Length, indexing, and slicing count bytes. range goes character by character. Escapes like \n in a literal work. |
| sending | pid <- m · select { case pid <- m: … default: … } · Monitor(pid) | A send never waits. If the process has stopped, the message is dropped. If its mailbox is full, the sender crashes, unless you used the select form, in which case default runs. Monitor gets you a Down message when a process stops. |
| control flow | if · switch · for · range · break · continue · goto | Value and type switches, fallthrough, labels, and goto inside one function. A loop that never ends gets cut off after a set amount of work. |
| several results | func Split(n int) (A, B) | Any mix of basic values and structs. Named results and functions that return nothing work too. |
| type assertions | x.(T) · v, ok := x.(T) | Both forms, on variables, fields, map and slice reads, and call results. |
| packages | import "myapp/pricing" | An imgo package can import other imgo packages and the small goer/std library (strings, slices, sort, strconv, math). Nothing else. |
| functions as values | func(x int) int { … } · x.M · T.M | A function literal gets its own copy of the variables it uses, and can’t change them afterwards. Method values work too. |
| generics | func F[T any](x T) T · Pair[K, V] | Each use is turned into ordinary code at compile time. Works for your own and imported generics, with the types spelled out or worked out for you. |
| defer · select · recover | defer f() · select { case x := <-M: } · recover() | defer runs when its own function returns or panics. select waits for a message of a given type, with an optional timeout or default. recover only catches a panic from its own function. |
| range over a function | for v := range it | Go 1.23’s range-over-func, with one or two values. |
Messages
A message is data you send to a process. The process reads it when its turn comes. In imgo a message is a struct, and by habit the behaviour’s second parameter is Msg interface{} so a type switch can sort them out. From Go the same message is a msg.Spec: a type name and a list of fields. Goer converts between the two at the mailbox.
// imgo: declare and match it
type PlaceOrder struct{ OrderID string; Quantity int }
case PlaceOrder:
s.Quantity = x.Quantity
// native Go: send it
pid.Tell(&msg.Spec{Type: "PlaceOrder", Fields: []msg.Field{
msg.Str("OrderID", "order-1001"), msg.Int("Quantity", 1),
}})A field can be an int64, float64, bool, string, or Pid. By habit, a Pid field called ReplyTo means “send the answer here”.
A field can also hold a list, a struct, or a map, nested as deep as you like. Build it with msg.Encode (a slice for a list, a msg.Record for a struct, a Go map for a map) and attach it with msg.Val. The receiver unpacks it into its own memory, so nothing is shared. Functions and interfaces can’t be fields; that’s a compile error.
Native actors get the *msg.Spec as it is. The slimmed-down processes used by the megapark benchmark only take runtime.CompactMsg.
What you can call inside a process
These come from package goer. Import it normally (goer.Spawn) or with import . "goer". A few of them borrow Go’s own syntax: pid <- M{} sends, go Worker() starts a process, and close(pid) stops one.
Spawn(Worker) · go Worker(W{})Start a child process. Spawn gives you its address; go doesn’t. The starting state comes from WorkerInit() or from a value you pass.pid <- M{} / Send(pid, m)Send a message. It never waits. If the process has stopped, the message is dropped. If its mailbox is full, the sender crashes.select { case pid <- m: … default: … } / TrySend(pid, m)A send that tells you how it went. The default branch runs (or TrySend returns false) when the mailbox is full or the process is gone.Ask(pid, Req{}).(Reply)Send a request and wait for the answer. Only this process waits. Works with ports and with processes that call Reply.Reply(m)Answer the Ask you’re handling right now.func Handle(s S, m Msg) (S, Reply)A behaviour that returns two things always replies with the second. Don’t Send to one of these: there’s nobody to reply to, and it crashes.Self()This process’s own address.Link(pid)Tie yourself to another process. If either crashes, both do.Monitor(pid)Get one Down{Pid, Reason} message when that process stops. Declare Down and handle it like any message.close(pid) / Stop(pid)Ask a process to stop once it has finished what’s in its mailbox.After(ms, M{})Send yourself a message in ms milliseconds. Good for timeouts, retries, and ticks.Port("fs")The address of a named port. The name has to be a constant.Native("name")Something to pass to Spawn: a registered native Go actor.Remote("node", Worker)Something to pass to Spawn: start the child on another node.What you call from Go
Your Go code is in charge of starting things, watching them, and shutting down. With goer run, the system is made for you and handed to Main.
goer.NewSystem() · sys.Shutdown()Make a system, and stop it cleanly. Shutdown stops every process, closes connections, and gives the memory back.imgo.Executor(GoerModule, "Name", fields)Find a behaviour and its init function in the compiled code. fields overrides parts of the starting state.imgo.Migration(GoerModule, "Fn")Find a migration function.sys.Spawn(name, init, behaviour, policy)Start a supervised imgo process and get its Pid. The policy is RestartOnCrash, RestartAlways, or RestartNever.sys.SpawnNative(name, init, behaviour, policy)Start a supervised native Go actor.pid.Tell(message)Send a message. You get an error back if it couldn’t be delivered or the mailbox is full.sys.HotSwap(name, behaviour)Change a running process’s code between two messages. Its data stays. A later restart goes back to the old code.sys.Migrate(name, behaviour, init, migrate)Change the code and convert the data. A later restart uses the new code too.sys.Stop(name)Ask a process to stop once its mailbox is empty.sys.Kill(pid, reason)Crash a process right now, with a reason.sys.SetMailboxLimit(pid, n)Cap a mailbox. Sends past the cap get ErrMailboxFull.sys.SendAfter(pid, d, m) · sys.StartTask(pid, fn)A timer, and a slow job you can cancel. Both report back as a message.sys.SetBreakpoint(module, file, line) · ClearBreakpoint · BreakpointsPause an imgo process when it reaches a line. While any breakpoint is set, everything runs interpreted.Ports
A port is how process code reaches files, sockets, clocks, and other services. It’s regular Go on the other side. The process sends a request and gets the reply as a message, or uses Ask and waits for it.
// native Go
sys.RegisterPort("fs", ports.FS())
// imgo
data := Ask(Port("fs"), ReadFile{Path: "notes.txt"}).(FileData)A port is a func(msg.Spec) msg.Spec. It runs on its own goroutine, so it’s allowed to be slow: only the process that asked is kept waiting. On the imgo side you declare structs that match what the port sends back.
Distribution
Joining two nodes takes two calls. Listen(name, addr) names this node and accepts connections. Connect(addr) connects to a node that’s listening. Call Listen first. Connected nodes show up in Snapshot().Peers, and sys.Peers() gives you just their names.
sys.Listen(name, addr) · sys.ListenTLS(name, addr, cfg)Name this node and start accepting connections. Returns the address it’s on.sys.Connect(addr) · sys.ConnectTLS(addr, cfg)Connect to a listening node. Returns its name.sys.MonitorPid(pid) · sys.LinkPids(a, b)Watch a process, or tie two together. Works across nodes.sys.RemoteSnapshots(limit)Ask each connected node for a summary of what it’s running.Spawn(Remote("node", Worker))From imgo: start a process on another node. The code is sent along if needed.You send to a remote Pid exactly like a local one. Only the message crosses the network; a process’s data never does. If you care whether the other node is still there, use a link or a monitor.
Tools
The goer command sits on top of go. It compiles the //go:build goer files in whatever you’re building and hands everything else to Go. Any command it doesn’t know goes straight through, so goer mod tidy is just go mod tidy. A leading -C dir works the way it does for go.
goer run ./pkg · goer debug ./pkgBuild and run an app (a package with func Main(*goer.System) error). -dash turns on the dashboard, -trace turns on tracing, -addr picks the dashboard’s address. debug is run -dash.goer build ./pkg [-o out]Build a program you can deploy. -debug and -trace build the dashboard and tracing in. GOOS and GOARCH work as usual.goer test [packages]Check every imgo file compiles, then run go test. Plain go test skips imgo files.goer vet [packages]Tell you which Go packages your imgo code leans on and how, warn about maps used as if they were shared, then run go vet.goer dashboard --attach URL [-token T]Open a local window onto the dashboard of a system that’s already running.goer bridge -app ./pkg -- child …Run an app together with a child program (usually a WebAssembly node) joined to it as a cluster member.goer-lspEditor support for imgo files, alongside gopls. You get the real compiler’s errors as you type, hover, go-to-definition, and completion. Build it with go build ./cmd/goer-lsp.goer/stdA small standard library written in imgo: strings, strconv, slices, sort, and math. It’s tested against Go’s own on awkward inputs. Parsers return (value, ok), and anything that would change its input returns a new value.What imgo leaves out
imgo is smaller than Go on purpose. These are missing: goroutines, real channels, closures that change the variables they captured, pointers, unsafe, cgo, direct file or network access, and package-level variables you can change. A chan T can appear as a type, but only as a stand-in for “somewhere to reply”; you never make one or receive from one.
Go that won’t compile, with an error pointing at the spot:
cap(s)A slice is a plain value with no hidden spare room, so there’s no capacity to ask about.copy inside a bigger expressionWrite n := copy(dst, src) on its own line.case nil in a type switchNot supported.f().x = vYou can’t write through a function’s result.s.A, s.B = f()A call’s several results have to go into plain names first, then into fields.Type switches go by shape, with one exception. A switch on the message a behaviour receives (even if you pass that message to a helper) goes by the struct’s type. A switch on any other interface value can only tell an integer from a string from a list from a struct. So two struct cases there won’t compile, and an int64 matches case int.
Three familiar words mean something slightly different. select picks a message out of the mailbox by type, with an optional timeout (SelTimeout) and default. defer runs when its own function returns or panics. And recover() only catches a panic from its own function, and all you can ask it is whether there was one (recover() != nil).
These limits are the reason Goer can pause a process, step through it, convert its data, and ship it to another machine.