Tutorial 01 / 10 minutes
Make your first process.
If you’ve ever had one stuck goroutine or one bad request knock over a whole Go service, this is for you. In ten minutes you’ll install Goer, run a tiny program that crashes on purpose and keeps going anyway, and read the two short files that make that happen. By the end you’ll know what a process is and how to start your own.
Here’s a picture to hold on to. It’s made up, but it helps. Imagine a small shop with one order counter. Customers drop notes in a tray. One clerk reads them, one at a time, and writes in a ledger nobody else is allowed to touch. In Goer that clerk is a process, the tray is its mailbox, and the ledger is its state.
1. Install the tool and get the examples
The goer command builds and runs Goer programs. It sits on top of the normal go command, so install Go first if you haven’t. Then grab the build for your computer from the download page, unpack it, and put goer somewhere on your PATH. Check it worked:
goer versionThe examples live in the Goer repository itself, so clone it and work from the top folder:
git clone https://gitlab.com/earnest-ventures/goer.git
cd goer2. Run the counter
goer run ./examples/counterThat one command compiles the process code, builds the example, starts everything, and keeps it running until you press Ctrl-C. Twice a second the example sends the counter a message. Most of them add one. Every seventh one tells it to crash on purpose. This is a recording of what you’ll see:
- goer run ./examples/counter
- tick 1 — supervised restarts: 0
- tick 2 — supervised restarts: 0
- tick 3 — supervised restarts: 0
- tick 4 — supervised restarts: 0
- tick 5 — supervised restarts: 0
- tick 6 — supervised restarts: 0
- [supervisor] "counter" crashed: imgo panic: counter asked to crash
- [supervisor] restarting "counter" (restart #1)
- tick 8 — supervised restarts: 1
- tick 9 — supervised restarts: 1
- tick 10 — supervised restarts: 1
- tick 11 — supervised restarts: 1
- ^C
- goer: shutting down
See that? It crashed, and then it just kept counting. Nothing else in the program had to know or care. That’s the whole point, and the next two sections show you the two files behind it.
3. Read the process: the clerk’s rule book
The process is written in imgo, a cut-down version of Go that Goer compiles itself. An imgo file looks like ordinary Go with one extra line at the top: //go:build goer. That line tells the normal Go compiler to skip the file and tells goer to pick it up. Press run on this one if you like; it works right here.
//go:build goer && goer_source
package counter
type State struct{ Count int }
type Tick struct{}
type Boom struct{}
type Msg interface{}
func CounterInit() State { return State{} }
func Counter(s State, m Msg) State {
switch m.(type) {
case Tick:
s.Count = s.Count + 1
case Boom:
panic("counter asked to crash")
}
return s
}Read it like the clerk’s rule book:
State is the ledgerOne struct holds everything this process remembers. Nobody else can touch it.
Tick and Boom are the notesMessages are plain structs. One with no fields is still a message. Its type is the request.
Counter is the ruleState in, one message through, state out. The switch decides what the message means, and the function hands back the updated ledger.
CounterInit is page oneA function named <Behaviour>Init gives the starting state. Here, the count starts at zero.
Now look at what isn’t there. No goroutines. No locks. No channels. No pointers. imgo has no way to reach into another process’s memory, so you don’t have to be careful about it. It just can’t happen.
And that panic in the Boom case? That’s the clerk throwing their hands up and walking out. The next tutorial is about what happens next.
4. Read the Go side: hiring the clerk
The second file is regular Go. Its job is to start the process and send it messages.
// Package counter is a minimal Goer application: it is run with
//
// goer run ./examples/counter
//
// There is no func main here. The package exposes Main(*goer.System), and the
// goer CLI owns process lifecycle — NewSystem, signal handling, Shutdown.
package counter
import (
"fmt"
"time"
"goer"
"goer/imgo"
"goer/msg"
)
// Start adds the counter process to an existing Goer system. A package can
// expose this kind of explicit wiring function without starting global state
// from init.
func Start(sys *goer.System) error {
behaviour, init, err := imgo.Executor(GoerModule, "Counter", nil)
if err != nil {
return err
}
c := sys.Spawn(
"counter",
goer.Init(init),
goer.Behavior(behaviour),
goer.RestartOnCrash,
)
go func() {
t := time.NewTicker(500 * time.Millisecond)
defer t.Stop()
n := 0
for range t.C {
n++
if n%7 == 0 {
_ = c.Tell(&msg.Spec{Type: "Boom"}) // supervisor restarts it
continue
}
_ = c.Tell(&msg.Spec{Type: "Tick"})
fmt.Printf("tick %d — supervised restarts: %d\n", n, sys.Restarts())
}
}()
return nil
}
// Main makes the example runnable with `goer run ./examples/counter`.
func Main(sys *goer.System) error { return Start(sys) }Four lines do the real work:
imgo.Executor(GoerModule, "Counter", nil)Finds the Counter behaviour and its CounterInit in the compiled code. GoerModule is made for you by goer run from every //go:build goer file in the package. You never compile it by hand.
sys.Spawn("counter", init, behaviour, goer.RestartOnCrash)Hires the clerk. The name is how the rest of your program and the dashboard find it. The last bit says what to do if it crashes.
c.Tell(&msg.Spec{Type: "Tick"})Drops a note in the tray. From Go you name the message by its type and, if it has any, a list of fields like msg.Int("Amount", 1). Goer turns that into the Tick the process expects.
Main(sys *goer.System) errorThe one function Goer looks for. It sets things up and returns. goer run handles creating the system, catching Ctrl-C, and shutting down cleanly.
The Start function is there to be polite. It lets another program import the counter and add it to its own system, without the counter starting anything behind its back.
5. Make your own
A Goer app is just a Go package with a Main function. Make a package, put your process code next to it in a //go:build goer file, and connect the two in Main:
package mysystem
import (
"goer"
"goer/imgo"
)
func Main(sys *goer.System) error {
behaviour, init, err := imgo.Executor(GoerModule, "Counter", nil)
if err != nil {
return err
}
sys.Spawn("counter", goer.Init(init), goer.Behavior(behaviour), goer.RestartOnCrash)
return nil
}goer run ./mysystemIf Main returns nil, the system stays up until you stop it. If it returns an error, the program exits with that error. And if you’d rather write a normal package main, that still works. The tool leaves it alone.
sys := goer.NewSystem(), call your Main, and defer sys.Shutdown(). You can write those three lines yourself whenever you need to be in charge.What you just built
One process that owns one piece of data. Messages that ask it to do things. A runtime that gives it somewhere to run and someone to watch over it.
That’s the shape. Everything else in Goer builds on it.