Structured WebSockets for Go. You write a struct per message and a method per handler. chanx-go routes each frame to its handler, validates it first, broadcasts to groups across processes, and documents the API as AsyncAPI, from which typed Go, TypeScript and Python clients are generated. It runs on net/http, chi, gorilla/mux, gin, echo and Fiber.
Documentation · Quick start · Tutorial · Why chanx-go? · API reference
gorilla/websocket and coder/websocket stop at the socket. After the upgrade, every team writes the same loop:
for {
var msg struct {
Action string `json:"action"`
Payload json.RawMessage `json:"payload"`
}
if err := conn.ReadJSON(&msg); err != nil {
return
}
switch msg.Action {
case "chat":
var p ChatPayload
if json.Unmarshal(msg.Payload, &p) != nil || p.Text == "" {
conn.WriteJSON(map[string]string{"error": "bad payload"})
continue
}
hub.Broadcast(room, p) // your own hub, and one process only
case "ping":
// ...
}
// no schema, no docs, and the frontend guesses the message shapes
}With chanx-go, each message is a struct and each handler is a typed method:
func (ChatConsumer) HandleChat(ctx *chanx.Context, m ChatMessage) error {
return ctx.Broadcast(ChatNotification{Payload: m.Payload})
}From that, you get:
- Routing by type. A frame's
actionpicks the handler whose message type matches. Noswitch, nojson.RawMessage. - Validation. A JSON Schema is inferred from each struct. A bad frame gets an error frame listing every problem, and your handler never sees it.
- Groups and events across processes. Broadcast to rooms, push from HTTP handlers and workers, over an in-memory layer or Redis.
- Topics. A client subscribes to many streams, such as
ticker:ACME, over one socket. - AsyncAPI 3.0. A document and a docs page built from the handlers that serve traffic, so they cannot drift apart.
- Typed clients. Generate a Go client with
chanx-codegen, or TypeScript and Python clients with the chanx tools. - Testing.
chanxtestruns connections in memory, with no network and no ports.
go get go.chanx.dev/v2Go 1.25 or later. The gin, echo and Fiber adapters and the Redis layer are separate modules, so you only download what you use.
A chat server with rooms, in one file. Save it as main.go:
package main
import (
"log"
"net/http"
chanx "go.chanx.dev/v2"
"go.chanx.dev/v2/asyncapi"
chanxhttp "go.chanx.dev/v2/nethttp"
)
type ChatPayload struct {
Text string `json:"text"`
}
// ChatMessage is what a client sends.
type ChatMessage struct {
chanx.BaseMessage `action:"chat" description:"Send a message to the room"`
Payload ChatPayload `json:"payload"`
}
// ChatNotification is what everyone in the room receives.
type ChatNotification struct {
chanx.BaseMessage `action:"chat_notification"`
Payload ChatPayload `json:"payload"`
}
type ChatConsumer struct{}
// Groups puts each connection in its room, from the {room} path parameter.
func (ChatConsumer) Groups(c *chanx.Conn) []string { return []string{"room." + c.Param("room")} }
func (ChatConsumer) HandleChat(ctx *chanx.Context, m ChatMessage) error {
return ctx.Broadcast(ChatNotification{Payload: m.Payload})
}
// Operations documents what HandleChat broadcasts, for the AsyncAPI document.
func (ChatConsumer) Operations() map[string]chanx.Operation {
return map[string]chanx.Operation{"HandleChat": {Replies: []chanx.Message{ChatNotification{}}}}
}
func (ChatConsumer) HandlePing(ctx *chanx.Context, _ chanx.PingMessage) (chanx.PongMessage, error) {
return chanx.PongMessage{}, nil
}
func main() {
engine := chanx.New(chanx.Config{}) // an in-memory layer; Redis for several processes
engine.Handle("/ws/chat/{room}", ChatConsumer{}, chanx.Name("chat"))
mux := http.NewServeMux()
chanxhttp.Mount(mux, engine)
chanxhttp.MountDocs(mux, engine, "/asyncapi", asyncapi.Info{Title: "Chat"})
log.Fatal(http.ListenAndServe(":8000", mux))
}Run it with go run ., open two connections with npx wscat -c ws://localhost:8000/ws/chat/lobby,
and send from one:
{"action": "chat", "payload": {"text": "hi"}}Both receive:
{"action": "chat_notification", "payload": {"text": "hi"}}A request with a ref gets a reply carrying the same ref:
> {"action": "ping", "ref": "1"}
< {"action": "pong", "payload": null, "version": 1, "ref": "1"}Now misspell the field. HandleChat never runs, and the sender gets an error in the same
format as Python chanx:
> {"action": "chat", "payload": {"txt": "hi"}}
< {"action": "error", "payload": {"type": "https://chanx.dev/errors/validation-failed", "title": "Message failed validation", "detail": "1 validation error", "errors": [{"keyword": "required", "pointer": "/payload", "detail": "Field required", "property": "text"}]}}The interactive docs for this API are at http://localhost:8000/asyncapi.
Clients are generated from the server's AsyncAPI document, so they match the server.
Go, with chanx-codegen:
go run go.chanx.dev/v2/cmd/chanx-codegen@latest -o ./chatclient http://localhost:8000/asyncapi.jsonchat, err := chatclient.DialChat(ctx, "ws://localhost:8000", "lobby")
if err != nil {
log.Fatal(err)
}
defer chat.Close()
err = chat.Send(ctx, chatclient.ChatMessage{Payload: chatclient.ChatPayload{Text: "hi from Go"}})
if err != nil {
log.Fatal(err)
}
msg, _, err := chat.Receive(ctx)
if err != nil {
log.Fatal(err)
}
if n, ok := msg.(chatclient.ChatNotification); ok {
fmt.Println(n.Payload.Text) // hi from Go
}Send only accepts messages the route declares, and ChatPayload{Txt: "hi"} is a
compile error. The generator also reads Python chanx servers' documents.
TypeScript, with chanx-js, for the browser, React, Vue, Svelte, Solid and Node:
npx @chanx-js/codegen http://localhost:8000/asyncapi.json -o src/generatedPython, with chanx:
chanx generate-client --schema http://localhost:8000/asyncapi.json --output ./chat_clientA consumer is written once and mounted on any of these. Routes go into your router, and your middleware runs before the upgrade.
| Framework | Import | Mount |
|---|---|---|
| net/http, chi, gorilla/mux | chanx-go/nethttp |
chanxhttp.Mount(mux, engine) |
| gin | chanx-go/adapters/gin |
chanxgin.Mount(r.Group("/"), engine) |
| echo v5 | chanx-go/adapters/echo |
chanxecho.Mount(e.Group(""), engine) |
| Fiber v3 | chanx-go/adapters/fiber |
chanxfiber.Mount(app.Group("/"), engine) |
Any router that takes an http.Handler (go-zero, Kratos, Huma) works through
chanxhttp.Handler. See Frameworks.
chanx-go is the third implementation of the chanx protocol, after Python and chanx-js. A Go server sends the same frames and publishes the same AsyncAPI document, so existing chanx clients connect to it unchanged.
The Redis layer is wire-compatible with channels_redis, fast-channels and
@chanx-js/layers. A Go worker can push to sockets held by a Django server, and the
other way round.
Parity is checked by tests against values recorded from Python chanx: validation errors,
group names, key casing, the Redis encodings and the full AsyncAPI document of
sandbox_fastapi, which examples/ reproduces in Go. The few deliberate
differences, such as strict type checks on payloads, are listed in
Differences from Python.
| Module | What |
|---|---|
go.chanx.dev/v2 |
The framework: messages, consumers, topics, engine, publisher, nethttp, asyncapi, chanxtest, auth, the Go client and chanx-codegen |
.../layers |
Channel layer interface, in-memory layer and conformance suite. Standard library only, usable without chanx |
.../layers/redis |
Redis pub/sub layer, shared with Python and Node |
.../adapters/gin, echo, fiber |
Framework adapters |
.../examples |
Python's sandbox_fastapi in Go, one entry point per framework |
Every package is listed on pkg.go.dev.
- Tutorial: a chat server: authentication, presence, tests, Redis and a typed client, step by step
- Guides: messages and validation, consumers, groups and events, topics, authentication, testing
- Channel layers and Redis
- Changelog
- chanx: the original, for Django Channels and FastAPI.
- chanx-js: TypeScript clients for the browser and Node, and a TypeScript server on the same protocol.
- chanx-kit: WebSocket components such as notifications, chat and presence, copied into your project in one command.
Issues and pull requests are welcome. Setup, the checks a pull request needs and the commit convention are in CONTRIBUTING.md; code conventions are in AGENTS.md.
MIT
