Skip to main content

What changed

v4 makes the API smaller and more uniform:
  • One actor reference. *PID stands for local and remote actors alike.
  • One lookup. ActorOf finds an actor on this node or anywhere in the cluster.
  • One scheduler. The same Schedule* methods serve local and remote actors.
  • Any message type. Messages are any instead of proto.Message. Protocol Buffers still work, and plain Go types can cross the network through the CBOR or JSON serializer.
  • Configured remoting. You enable remoting with a remote.Config. The client the actor system uses to reach other nodes is internal.
  • Paths instead of addresses. The Path interface replaces *address.Address.

Quick reference

Breaking changes

1. Module path

The module is github.com/tochemey/goakt/v4. Change every import from github.com/tochemey/goakt/v3/... to github.com/tochemey/goakt/v4/... and run go mod tidy. v4 requires Go 1.26.

2. Messages are any

Every message-passing API of the actor system uses any instead of proto.Message for messages and replies:
  • actor.Tell, actor.Ask, PID.Tell, PID.Ask and their batch forms.
  • ReceiveContext.Message, ReceiveContext.Response, ReceiveContext.Tell and ReceiveContext.Ask.
  • PID.PipeTo and PID.PipeToName: the task is func() (any, error).
  • ScheduleOnce, Schedule and ScheduleWithCron.
  • AskGrain and TellGrain.
Messages between actors on the same node are passed as they are and never serialized. A message that crosses the network needs a serializer: Protocol Buffers messages have one by default, and other types must be registered (see Serializers for any Go type). The standalone client package is the exception. client.Client.Tell and client.Client.TellGrain still take a proto.Message. client.Client.Ask and client.Client.AskGrain take any. What to do: replace proto.Message with any in handlers and at call sites. Protocol Buffers messages need no other change.

3. System messages moved to the actor package

The goaktpb package is gone. System messages are plain Go structs in the actor package and arrive as pointers: Messages with data keep it in unexported fields. Build them with their constructors, such as actor.NewDeadletter(sender, receiver, message, sendTime, reason) or actor.NewTerminated(actorPath), and read them through accessor methods, such as Terminated.ActorPath() or Deadletter.Reason(). Actors in these messages are identified by actor.Path values and nodes by address strings. Timestamps are time.Time instead of *timestamppb.Timestamp. Messages without data, such as PostStart and PoisonPill, are empty structs: send new(actor.PoisonPill).

4. *PID is the only actor reference

ActorRef is gone. *PID is the reference for local and remote actors alike. Methods that returned ActorRef values now return *PID values. A remote PID is a lightweight handle: Tell, Ask and the scheduler route it through remoting. When location matters, ask the PID with pid.IsLocal() or pid.IsRemote(). Most methods work on a remote PID by asking its node: Stop, Shutdown, Restart, Reinstate, SpawnChild, Child, Children and Parent all do. A few make sense only locally and return errors.ErrNotLocal for a remote PID: ReinstateNamed, SendAsync, SendSync, PipeTo, PipeToName and DiscoverActor. Watch and UnWatch called on a remote PID do nothing; to watch a remote actor, call them on a local PID with the remote PID as argument. What to do: replace ActorRef with *PID.

5. One lookup: ActorOf

LocalActor and RemoteActor are gone, and ActorOf has a new signature:
To reach an actor by name on a given node without a cluster, use PID.RemoteLookup(ctx, host, port, name) from a running actor, or ReceiveContext.RemoteLookup(host, port, name) inside Receive.

6. Actors replaces ActorRefs

Actors and ActorRefs are merged into one method:
Local actors come back as local PIDs. In cluster mode, actors on other nodes come back as remote PIDs. timeout bounds the cluster scan and is ignored outside cluster mode. The cluster scan has a cost, so do not call Actors on a hot path.

7. Remote scheduling uses the ordinary scheduler

RemoteScheduleOnce, RemoteSchedule and RemoteScheduleWithCron are gone. ScheduleOnce, Schedule and ScheduleWithCron accept a remote PID and deliver through remoting. They return errors.ErrRemotingDisabled when the PID is remote and remoting is not enabled. In cluster mode, ScheduleWithCron requires actor.WithReference: without it the call fails with errors.ErrScheduleReferenceRequired. The reference is the key that makes each tick deliver once across the cluster.

8. GetPartition is now Partition

Replace system.GetPartition(name) with system.Partition(ctx, name). It returns the registry partition that holds the actor’s record as a uint64, or an error: ErrActorNotFound when no actor is registered under the name, ErrClusterDisabled outside cluster mode and ErrActorSystemNotStarted on a stopped actor system.

9. Addresses became paths

pid.Address() is replaced by pid.Path(), which returns the actor.Path interface. A path gives the actor’s Host, Port, HostPort, Name, QualifiedName, System, Parent and String, and compares with Equals. You cannot implement Path yourself. The address package is internal and has no public replacement. Path() returns nil on a nil PID. The address accessors of ReceiveContext and of the test probe are gone too:
  • ctx.SenderAddress() becomes ctx.Sender().Path().
  • ctx.ReceiverAddress() becomes ctx.Self().Path().
  • testkit.Probe.SenderAddress() becomes Probe.Sender(), then Path() or IsRemote() on the PID it returns. Probe.Sender() is nil until an Expect method of the probe has taken a message.
When a message has no sender, ctx.Sender() returns the actor system’s no-sender PID, not nil: compare it with ctx.ActorSystem().NoSender(). A message from another node has a remote PID as its sender.

10. Remoting is configured, not constructed

remote.Remoting, remote.Client, remote.NewRemoting() and remote.NewClient() are gone. The client the actor system uses to reach other nodes is internal. The remote package now holds configuration and protocol types: Config, its options, Serializer, Compression and ContextPropagator. What to do:
  • Replace WithRemoting with actor.WithRemote(remote.NewConfig(bindAddr, bindPort, opts...)).
  • Message remote actors through PIDs: get one with ActorOf, PID.RemoteLookup or ReceiveContext.RemoteLookup, then use Tell and Ask as for a local actor. PID.RemoteStop and PID.RemoteReSpawn stop and restart an actor on a given node.
  • To spawn an actor on a given node, pass actor.WithHostAndPort(host, port) to Spawn. The target node must have registered the actor type with system.Register(ctx, actor). Spawn forwards these options to that node: WithRelocationDisabled, WithDependencies, WithStashing, WithPassivationStrategy, WithReentrancy, WithSupervisor, WithRole and WithInitTimeout, and, in cluster mode, the reliable-delivery options such as AsReliableProducer and AsReliableConsumer.
  • From a program that does not run an actor system, use the client package: client.New(ctx, nodes) with nodes from client.NewNode(address, client.WithRemoteConfig(config)).
  • Configure TLS with remote.WithTLS on the remote config. actor.WithTLS is deprecated, and when both are set the remote config wins.
The main options of remote.NewConfig:

11. The Logger interface

The log.Logger interface has new methods. A custom implementation must add:
  • InfoContext, InfofContext, WarnContext, WarnfContext, ErrorContext, ErrorfContext, DebugContext and DebugfContext, which take a context.Context first so trace data can reach the log.
  • LogLevel() Level: the minimum level the logger writes.
  • Enabled(level Level) bool: whether a level is written; check it before expensive work.
  • With(keyValues ...any) Logger: a logger that adds the given fields to every entry. Keys and values alternate, and keys are strings.
  • Flush() error: writes buffered entries.
  • StdLogger() *log.Logger: a standard library logger for dependencies that need one.
The interface has no Fatal, Panic or LogOutput methods: a library must not stop the process. log.Zap and log.Slog still have them, so call them on the concrete type if you need them. What to do: add the missing methods and check with var _ log.Logger = (*MyLogger)(nil). Use log.DiscardLogger where you need a logger that writes nothing, such as in tests. It is a log.Logger value, so it does not expose the Fatal, Panic and LogOutput methods of the concrete types.

New in v4

Serializers for any Go type

A message that crosses the network is encoded by the serializer registered for its type on the remote config:
  • ProtoSerializer is registered for every proto.Message by default. Protocol Buffers users change nothing.
  • CBORSerializer and JSONSerializer encode plain Go types. Register types with remote.WithSerializables(new(MyMessage)) for CBOR or remote.WithJSONSerializables(new(MyMessage)) for JSON. A typed nil interface pointer, such as (*MyInterface)(nil), registers every type that implements the interface.
  • A custom serializer implements remote.Serializer and is registered with remote.WithSerializers(new(MyMessage), mySerializer).
Register the same serializable types on every node that sends or receives them: a node cannot decode a type it has not registered.

New error sentinels

  • errors.ErrRemotingDisabled: a remote operation was attempted but remoting is not enabled.
  • errors.ErrNotLocal: the operation needs a local PID but got a remote one.

PID.Kind

pid.Kind() returns the type name of the actor behind the PID. For a remote PID it asks the actor’s node, and returns an empty string when that fails.

Logger implementations

  • log.NewSlog(level, writers...): a logger built on the standard library slog that writes JSON. log.NewSlogFrom(logger, level) wraps a *slog.Logger you already have.
  • log.NewZap(level, writers...): a logger built on Zap. Debug, info and warning entries written to files are buffered, so call Flush at shutdown.
  • log.DiscardLogger: a logger that writes nothing.
Pass a logger to the actor system with actor.WithLogger.

Migration checklist

  • Change imports from github.com/tochemey/goakt/v3 to github.com/tochemey/goakt/v4.
  • Replace proto.Message with any in handlers and call sites.
  • Replace goaktpb.* with actor.*; build messages with their constructors and read them with their accessors.
  • Replace ActorRef with *PID.
  • Replace LocalActor and RemoteActor with ActorOf(ctx, name), and update callers of ActorOf to its two return values.
  • Replace ActorRefs with Actors(ctx, timeout).
  • Replace RemoteSchedule* with Schedule* on a PID from ActorOf; give ScheduleWithCron a WithReference in cluster mode.
  • Replace GetPartition with Partition.
  • Replace pid.Address() with pid.Path(), and remove imports of the address package.
  • Replace ctx.SenderAddress() and ctx.ReceiverAddress() with ctx.Sender().Path() and ctx.Self().Path().
  • Replace Probe.SenderAddress() with Probe.Sender() and Path().
  • Replace WithRemoting and any use of remote.Remoting or remote.Client with WithRemote(config), PID methods, or the client package.
  • Register serializers for every message type that crosses the network and is not Protocol Buffers.
  • Add the new methods to custom Logger implementations.