Generic building blocks for CSP/actor-model concurrency in Go.
Each type maps to a common inter-goroutine communication pattern, eliminating the per-actor boilerplate of defining request structs, response channels, and caller methods.
| Type | Caller | Actor | Pattern |
|---|---|---|---|
Func[Req, Resp] | Call(req) -> resp | msg.Req, msg.Reply(resp) | function call |
Query[Resp] | Call() -> resp | msg.Reply(resp) | getter |
Proc[Req] | Call(req) | msg.Req, msg.Done() | setter / command |
Signal | Call() | msg.Done() | trigger / reset |
Inbox[T] | Send(v) / TrySend(v) | <-i.Recv() | fire-and-forget |
Lifecycle | Stop() | <-Stopping(), defer MarkDone() | shutdown |
All synchronous types use unbuffered channels. The caller blocks until the actor processes the message. This eliminates races where a buffered write appears to succeed but the actor hasn't seen it yet.
type Counter struct {
inc actor.Signal
get actor.Query[int]
actor.Lifecycle
}
func NewCounter() *Counter {
c := &Counter{
inc: actor.NewSignal(),
get: actor.NewQuery[int](),
Lifecycle: actor.NewLifecycle(),
}
actor.Go(c.Lifecycle, c.run)
return c
}
func (c *Counter) run() {
var n int
for {
select {
case <-c.Stopping():
return
case msg := <-c.inc.Recv():
n++
msg.Done()
case msg := <-c.get.Recv():
msg.Reply(n)
}
}
}
func (c *Counter) Inc() { c.inc.Call() }
func (c *Counter) Get() int { return c.get.Call() }
coordination is via channels and select.
happens-before: when Call returns, the actor has processed the message. Use Inbox for intentionally async paths.
Callers get copies via response channels, never references.
interface to satisfy, no registration.
lookup, filtered query)
config)
(set value, record event)
flush, increment)
Use TrySend to drop on backpressure.
go get git.smesh.lol/actor
AGPL-3.0-or-later