Za-go: what an unofficial messaging client looks like in Go
Za-go is an unofficial Go client for Zalo. Unofficial messaging clients sit in an awkward place: useful for automation experiments, but fragile because they depend on private API behaviour that can change without notice. They can also violate platform terms if used carelessly.
Read this kind of project as a Go architecture study, not as an endorsement of automating a consumer messaging platform without permission.
The public API is one facade
The main type is ZaloAPI:
type ZaloAPI struct {
state *app.State
hub *worker.EventHub
auth *authcore.LoginAuth
// grouped services: send, group, socket, properties, get...
}
The constructor wires a shared state object and an event hub into a set of service packages:
func Zalo(phone, password, imei string, sessionCookies any, userAgent string, autoLogin bool, login int) (*ZaloAPI, error) {
state := app.NewState(...)
hub := worker.NewEventHub()
getSvc := getapi.NewGetAPI(state, login, hub)
return &ZaloAPI{
state: state,
hub: hub,
auth: authcore.NewLoginAuth(state),
send: handle.NewSendAPI(state, login, hub),
group: groupapi.NewGroupAPI(state, login, hub, getSvc),
}, nil
}
That is a common shape for API clients with many endpoints. Keep the user-facing type small, then group endpoint families behind internal services that share state.
Session state is the hard part
Messaging clients are mostly session management. Za-go exposes SetSession, Login, IsLoggedIn, profile helpers, and QR-code authentication methods. The public methods mostly delegate to the state/auth/socket packages.
func (z *ZaloAPI) SetSession(sessionCookies any) bool
func (z *ZaloAPI) Login(phone, password, imei, userAgent string) error
func (z *ZaloAPI) IsLoggedIn() bool
That separation is useful. Login code changes more often than message-sending code when a private platform changes its web flow.
Events are exposed as channels
The library exposes receive-only channels for socket events:
func (z *ZaloAPI) MessageEvents() <-chan worker.MessageEvent
func (z *ZaloAPI) GroupEvents() <-chan worker.GroupEventEnvelope
func (z *ZaloAPI) DeliveryEvents() <-chan worker.DeliveryEvent
func (z *ZaloAPI) SocketErrors() <-chan worker.SocketErrorEvent
That is idiomatic Go for a realtime client. The socket layer owns reads and reconnect behaviour. Consumers range over channels or select on the events they care about.
A channel API also makes backpressure visible. If callers stop reading events, the event hub has to decide whether to block, buffer, or drop. That is an important design decision for any messaging client.
The facade has too many methods, but the grouping helps
ZaloAPI exposes many methods: sending text, voice, video, files, reactions, stickers, local images, friend actions, group actions, QR helpers, upload helpers, and profile fetches.
That can get noisy, but it gives users one import and one client value. The internal services keep implementation grouped even if the public facade is broad.
For a production SDK, I would consider exposing grouped sub-clients directly:
z.Messages.Send(...)
z.Groups.AddMember(...)
z.Auth.Login(...)
The current facade is simpler for quick scripts, which is probably the target audience.
What to take from Za-go
The useful Go lessons are the shared state object, grouped endpoint services, receive-only event channels, and facade API. The operational lesson is caution: unofficial clients are brittle, and automation against private messaging APIs should be limited to accounts and use cases you are allowed to control.
If you build official API clients, the same package structure applies. If you build unofficial ones, expect maintenance to be the main cost.