goer Download ↓

Architecture / from the ground up

Start with the failure boundary.

You can use Goer without knowing how it works inside. But if you’re the kind of person who wants to know what happens when you press the button — and if you run services for a living, you probably are — this page is for you. We’ll build it up in five layers. Each one gets a plain-words version first and then what’s really going on. Stop at the plain version any time; it’s enough to use Goer well.

Here’s the picture we’ll keep coming back to. A process is one worker with their own notebook. A message is a note for that worker. A mailbox is the tray where notes wait. And the runtime is the manager: it decides who works next, notices when someone walks out, and carries notes around.

05DistributionMessages cross between machines.
04SupervisionA crash becomes something you can recover from.
03SchedulerLots of processes share a few workers.
02MemoryEvery piece of data has one owner.
01Message + behaviourState in, message through, state out.

01 / Processes

In a normal Go program, a goroutine usually shares things with its neighbours: a map here, a mutex there, a pointer passed around. That sharing is where the hard bugs live. In Goer the unit of work is a process (some people say actor). It runs one thing at a time, keeps its data private, and has a mailbox.

Sending a message doesn’t call the other process’s function. It drops the message in that process’s tray, and the scheduler decides when it gets read. A process has an address, called a Pid, and that’s the only handle anyone else gets.

message→mailbox→behaviour→new state
In plain words

One worker, one notebook, one tray of notes. The worker reads one note at a time and updates the notebook. Nobody else can write in it.

What’s really happening

A behaviour is a function (State, Msg) → State, compiled from imgo to bytecode. imgo has no pointers, goroutines, channels, or shared package variables, so the compiler can promise nothing reaches in. A message is copied into the receiver’s memory when it’s delivered.

02 / Memory and who owns it

Each process gets its own block of memory, called an arena. Picture a workbench with one person’s name on it. Everything that person makes goes on their bench. When they leave, the whole bench is cleared in one go.

An arena is just a way of drawing a line around one process’s memory, so it’s obvious who owns what and easy to clean up. It lives inside your program and goes away when the program stops.

Inside the arena, Goer keeps a count on each value of how many things are still using it. That’s called reference counting. When the count hits zero, the value is freed on the spot.

In plain words

Every process has a box. Throw the box away and everything in it goes too. Inside the box, each item has a tally of who’s still using it, and it’s binned the moment the tally reaches zero.

What’s really happening

Arenas are cut from memory Goer asks the operating system for directly, and Go’s garbage collector doesn’t have to look inside them. That’s why a million processes don’t slow the collector down. The compiler adds the counting for you. And when a value has only one user, updating it changes it in place, so “values never change” doesn’t mean “everything gets copied”.

Reference counting, step by step:

  1. A process makes a value. One thing is using it.
  2. The process stores it somewhere else too. Now two things are.
  3. One of them is done with it. Back to one.
  4. The last one is done. The value is freed, along with anything only it was holding.

That’s different from how Go normally cleans up. Go’s collector stops by every so often and searches for what’s still reachable. Reference counting keeps track as it goes. Go’s collector still runs for your regular Go code and for native actors. Goer just keeps the process data out of its way.

03 / The scheduler

A process is lighter than a goroutine. A goroutine has its own stack and Go decides when it runs. A Goer process is just some data and a function, and Goer’s own scheduler runs it when it has mail.

A small team of workers does the running. Each worker picks up a process with messages waiting, lets it handle a few, and moves on. “A few” is a fixed limit called the reduction budget. It’s there so one chatty process can’t hog a worker while everyone else waits.

In plain words

A few managers walk the floor. Each one stops at a worker who has notes in their tray, lets them deal with a handful, then moves on. A worker with an empty tray costs nothing.

What’s really happening

There are GOMAXPROCS workers sharing one queue of ready processes. A process handles up to 16 messages, steps aside, and rejoins the queue if it has more. Ask doesn’t block a worker: Goer saves where the process was and picks it back up when the reply arrives. Ports and tasks run on ordinary goroutines, off to the side.

04 / Supervision

Processes can fail. When one does, its supervisor is told, follows the restart rule you gave it, and the rest of the system keeps running.

Two more relationships exist. A link ties two processes together so that if one crashes, the other does too. A monitor only watches: it tells you when a process stops, without tying your fate to it.

In plain words

A worker walks out. The manager notices, bins their notebook, and sits a new worker down with a fresh one. Nobody else stops. If the same seat keeps emptying, the manager stops filling it and says so.

What’s really happening

A panic is caught at the edge of the process and written up as a crash report with the call stack. The supervisor gets the reason. The rules are RestartOnCrash, RestartAlways, and RestartNever. By default only the crashed process restarts; you can ask for the ones started after it to restart too. The memory is freed and the address is kept, so senders don’t need to know anything happened.

05 / Distribution

A node is one running Goer system. A Pid knows which node its process is on, so you send to it the same way whether it’s local or on another machine. Goer packs the message up, sends it over, and tells you quickly if the other machine isn’t there.

In plain words

Two shops and a postal service. A note for a worker in the other shop gets posted. If the line to the other shop goes dead, everyone waiting on it is told straight away.

What’s really happening

The node is part of the Pid. Messages to another node go over a TCP connection with a heartbeat. If the heartbeat stops, everything monitoring a process there gets “node unreachable”. Goer doesn’t reconnect on its own. And when you start a process on another node, its compiled code is sent along with the request.

Connect two nodes

Nothing here happens by magic. Every node has a name. One listens on an address and the other connects to it. Goer has no discovery service and no auto-reconnect, and nodes don’t share a registry of names. So pick names that are stable and unique.

main.go · native Go host
func Main(sys *goer.System) error {
    name := os.Getenv("GOER_NODE")
    bind := os.Getenv("GOER_BIND")
    if _, err := sys.Listen(name, bind); err != nil { return err }

    if peer := os.Getenv("GOER_PEER"); peer != "" {
        if _, err := sys.Connect(peer); err != nil { return err }
    }
    // Spawn your local processes here.
    return nil
}

Start the one that listens first, then the one that connects:

terminal
GOER_NODE=orders-a GOER_BIND=127.0.0.1:9101 goer run ./orders
GOER_NODE=orders-b GOER_BIND=127.0.0.1:9102 GOER_PEER=127.0.0.1:9101 goer run ./orders

In imgo, Spawn(Remote("orders-b", Worker)) starts a process on the node you name. The Pid you get back works like any other. If the other node is down, the message can’t arrive, and links and monitors are how your program finds out.

Before you deploy this.The plain listener doesn’t check who’s connecting. Anyone who can reach it can send messages, start processes, read state, and run code inside your program. Keep it on your own machine or a network you fully trust. Anywhere else, use sys.ListenTLS(name, bind, cfg) and sys.ConnectTLS(peer, cfg) with both sides checking certificates (ClientAuth: tls.RequireAndVerifyClientCert).

Seeing it

Every layer can tell you what it’s up to: how long each mailbox is and what kinds of message are waiting, how busy each process has been, how much memory is in use, how many restarts there have been, what crashed and where, and which nodes are connected. None of that includes your actual data.

The dashboard shows it in a browser. The observability API gives you the same thing as Go values and JSON.

Where regular Go is better.Goer is good at keeping lots of small pieces coordinated and alive. For crunching numbers, regular Go is faster, so heavy maths belongs there.