What changed
v4 makes the API smaller and more uniform:- One actor reference.
*PIDstands for local and remote actors alike. - One lookup.
ActorOffinds 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
anyinstead ofproto.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
Pathinterface replaces*address.Address.
Quick reference
Breaking changes
1. Module path
The module isgithub.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.Askand their batch forms.ReceiveContext.Message,ReceiveContext.Response,ReceiveContext.TellandReceiveContext.Ask.PID.PipeToandPID.PipeToName: the task isfunc() (any, error).ScheduleOnce,ScheduleandScheduleWithCron.AskGrainandTellGrain.
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:
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()becomesctx.Sender().Path().ctx.ReceiverAddress()becomesctx.Self().Path().testkit.Probe.SenderAddress()becomesProbe.Sender(), thenPath()orIsRemote()on the PID it returns.Probe.Sender()isniluntil anExpectmethod of the probe has taken a message.
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
WithRemotingwithactor.WithRemote(remote.NewConfig(bindAddr, bindPort, opts...)). - Message remote actors through PIDs: get one with
ActorOf,PID.RemoteLookuporReceiveContext.RemoteLookup, then useTellandAskas for a local actor.PID.RemoteStopandPID.RemoteReSpawnstop and restart an actor on a given node. - To spawn an actor on a given node, pass
actor.WithHostAndPort(host, port)toSpawn. The target node must have registered the actor type withsystem.Register(ctx, actor).Spawnforwards these options to that node:WithRelocationDisabled,WithDependencies,WithStashing,WithPassivationStrategy,WithReentrancy,WithSupervisor,WithRoleandWithInitTimeout, and, in cluster mode, the reliable-delivery options such asAsReliableProducerandAsReliableConsumer. - From a program that does not run an actor system, use the
clientpackage:client.New(ctx, nodes)with nodes fromclient.NewNode(address, client.WithRemoteConfig(config)). - Configure TLS with
remote.WithTLSon the remote config.actor.WithTLSis deprecated, and when both are set the remote config wins.
remote.NewConfig:
11. The Logger interface
The log.Logger interface has new methods. A custom implementation must add:
InfoContext,InfofContext,WarnContext,WarnfContext,ErrorContext,ErrorfContext,DebugContextandDebugfContext, which take acontext.Contextfirst 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.
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:ProtoSerializeris registered for everyproto.Messageby default. Protocol Buffers users change nothing.CBORSerializerandJSONSerializerencode plain Go types. Register types withremote.WithSerializables(new(MyMessage))for CBOR orremote.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.Serializerand is registered withremote.WithSerializers(new(MyMessage), mySerializer).
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 libraryslogthat writes JSON.log.NewSlogFrom(logger, level)wraps a*slog.Loggeryou already have.log.NewZap(level, writers...): a logger built on Zap. Debug, info and warning entries written to files are buffered, so callFlushat shutdown.log.DiscardLogger: a logger that writes nothing.
actor.WithLogger.
Migration checklist
- Change imports from
github.com/tochemey/goakt/v3togithub.com/tochemey/goakt/v4. - Replace
proto.Messagewithanyin handlers and call sites. - Replace
goaktpb.*withactor.*; build messages with their constructors and read them with their accessors. - Replace
ActorRefwith*PID. - Replace
LocalActorandRemoteActorwithActorOf(ctx, name), and update callers ofActorOfto its two return values. - Replace
ActorRefswithActors(ctx, timeout). - Replace
RemoteSchedule*withSchedule*on a PID fromActorOf; giveScheduleWithCronaWithReferencein cluster mode. - Replace
GetPartitionwithPartition. - Replace
pid.Address()withpid.Path(), and remove imports of theaddresspackage. - Replace
ctx.SenderAddress()andctx.ReceiverAddress()withctx.Sender().Path()andctx.Self().Path(). - Replace
Probe.SenderAddress()withProbe.Sender()andPath(). - Replace
WithRemotingand any use ofremote.Remotingorremote.ClientwithWithRemote(config), PID methods, or theclientpackage. - Register serializers for every message type that crosses the network and is not Protocol Buffers.
- Add the new methods to custom
Loggerimplementations.