go-actor
is a lightweight library for writing concurrent programs in Go using the Actor model.
The goal of go-actor is to bring the actor model closer to Go developers by providing a design pattern that facilitates the development of scalable and high-performance concurrent applications.
Without reusable design principles, maintaining a complex codebase can be challenging. Golang's unique concurrency model, based on goroutines and channels, naturally aligns with the actor model, making it an ideal fit for building actor-based programs.
- Unified Design Principles: Model the entire codebase using the same principles, where each actor is a fundamental building block.
- Natural Fit with Go: Leverage Go's goroutines and channels, which directly translate to actors and mailboxes.
- Avoid Mutexes: Design systems without the need for mutexes, reducing the potential for deadlocks and improving performance in complex components.
- Optimal Scheduling: Enhance performance by optimizing for Go's goroutine scheduler.
- Easy Transition: Legacy codebases can transition to an actor-based design due to the simple interfaces provided by go-actor, allowing for seamless integration.
- Zero Overhead: Ensure optimal performance in highly concurrent environments.
The core abstraction layer of go-actor consists of three primary interfaces:
actor.Actor
: Represents any entity that implements theStart()
andStop()
methods. Actors created using theactor.New(actor.Worker)
function spawn a dedicated goroutine to execute the suppliedactor.Worker
.actor.Worker
: Encapsulates the executable logic of an actor. This is the primary interface developers need to implement to define an actor's behavior.actor.Mailbox
: An interface for message transport mechanisms between actors, created using theactor.NewMailbox(...)
function.
Explore the examples repository to see go-actor
in action. Reviewing these examples is highly recommended, as they will greatly enhance your understanding of the library.
// This example will demonstrate how to create actors for producer-consumer use case.
// Producer will create incremented number on every 1 second interval and
// consumer will print whatever number it receives.
func main() {
mbx := actor.NewMailbox[int]()
// Produce and consume workers are created with same mailbox
// so that produce worker can send messages directly to consume worker
p := actor.New(&producerWorker{mailbox: mbx})
c1 := actor.New(&consumerWorker{mailbox: mbx, id: 1})
// Note: Example creates two consumers for the sake of demonstration
// since having one or more consumers will produce the same result.
// Message on stdout will be written by first consumer that reads from mailbox.
c2 := actor.New(&consumerWorker{mailbox: mbx, id: 2})
// Combine all actors to singe actor so we can start and stop all at once
a := actor.Combine(mbx, p, c1, c2).Build()
a.Start()
defer a.Stop()
// Stdout output:
// consumed 1 (worker 1)
// consumed 2 (worker 2)
// consumed 3 (worker 1)
// consumed 4 (worker 2)
// ...
select {}
}
// producerWorker will produce incremented number on 1 second interval
type producerWorker struct {
mailbox actor.MailboxSender[int]
num int
}
func (w *producerWorker) DoWork(ctx actor.Context) actor.WorkerStatus {
select {
case <-ctx.Done():
return actor.WorkerEnd
case <-time.After(time.Second):
w.num++
w.mailbox.Send(ctx, w.num)
return actor.WorkerContinue
}
}
// consumerWorker will consume numbers received on mailbox
type consumerWorker struct {
mailbox actor.MailboxReceiver[int]
id int
}
func (w *consumerWorker) DoWork(ctx actor.Context) actor.WorkerStatus {
select {
case <-ctx.Done():
return actor.WorkerEnd
case num := <-w.mailbox.ReceiveC():
fmt.Printf("consumed %d \t(worker %d)\n", num, w.id)
return actor.WorkerContinue
}
}
While go-actor
is designed to be a minimal library with lean interfaces, developers can extend its functionality with domain-specific add-ons. Some notable add-ons include:
- super: An add-on for unifying the testing of actors and workers.
- commence: An add-on that provides a mechanism for waiting for actor execution to begin.
To enhance code quality in projects that heavily rely on the actor model with go-actor
, consider adhering to best practices and reviewing common hurdles for frequently encountered issues.
You can find detailed design decisions here.
The go-actor
library adopts a versioning scheme structured as x.y.z
.
Initially, the library will utilize the format 0.y.z
as it undergoes refinement until it attains a level of stability where fundamental interfaces and core principles no longer necessitate significant alterations. Within this semantic, the y
component signifies a version that is not backward-compatible. It is advisable for developers to review the release notes carefully to gain insight into these modifications. Furthermore, the final component, z
, denotes releases incorporating changes that are backward-compatible.
All contributions are useful, whether it is a simple typo, a more complex change, or just pointing out an issue. We welcome any contribution so feel free to open PR or issue.
Continue reading here.
Happy coding 🌞