WebSocket in Go: Advanced Techniques for Real-Time Applications
\n\nDeveloping real-time applications in Go is one of the language's strongest points. Built-in concurrency support through goroutines and channels makes Go an ideal choice for WebSocket servers. However, when it comes to production systems with thousands of concurrent connections, a simple "Hello, WebSocket" example no longer works. In this article, we will cover advanced techniques: from optimizing the connection pool to scaling through message brokers.
\n\n1. Connection Pool Architecture: Managing Thousands of Clients
\n\nIn a basic example, we create a separate goroutine for each connection. This is fine for 100-200 clients, but with 10,000+ connections, memory consumption and context switching overhead become critical. An advanced approach is using a connection pool with a fixed number of workers and channels.
\n\n// Pool — connection pool with goroutine limit\ntype Pool struct {\n connections map[*websocket.Conn]bool\n broadcast chan []byte\n register chan *websocket.Conn\n unregister chan *websocket.Conn\n}\n\nfunc NewPool(maxGoroutines int) *Pool {\n return &Pool{\n connections: make(map[*websocket.Conn]bool),\n broadcast: make(chan []byte, 256),\n register: make(chan *websocket.Conn),\n unregister: make(chan *websocket.Conn),\n }\n}\n\nfunc (pool *Pool) Start() {\n for {\n select {\n case conn := <-pool.register:\n pool.connections[conn] = true\n case conn := <-pool.unregister:\n if _, ok := pool.connections[conn]; ok {\n delete(pool.connections, conn)\n conn.Close()\n }\n case message := <-pool.broadcast:\n for conn := range pool.connections {\n go func(c *websocket.Conn) {\n if err := c.WriteMessage(websocket.TextMessage, message); err != nil {\n pool.unregister <- c\n }\n }(conn)\n }\n }\n }\n}\n\nWhy does this work? Channels act as buffers, preventing blocking. A single select manages all events, and goroutines for writing are created only when needed. This reduces peak load on the GC (garbage collector).
\n\n2. Graceful Shutdown and Connection State Management
\n\nWhen restarting the server, it is important to properly close all WebSocket connections by sending clients a shutdown message. Ignoring this step leads to "dangling" connections and memory leaks.
\n\n// Server — WebSocket server with graceful shutdown\ntype Server struct {\n pool *Pool\n httpSrv *http.Server\n}\n\nfunc (s *Server) Shutdown(ctx context.Context) error {\n // 1. Shut down the HTTP server (new connections are not accepted)\n if err := s.httpSrv.Shutdown(ctx); err != nil {\n return err\n }\n\n // 2. Send a shutdown message to all clients\n shutdownMsg := []byte(`{"type":"shutdown","message":"server is going down"}`)\n s.pool.broadcast <- shutdownMsg\n\n // 3. Close all connections in the pool\n for conn := range s.pool.connections {\n conn.WriteMessage(websocket.CloseMessage, \n websocket.FormatCloseMessage(websocket.CloseNormalClosure, "server shutdown"))\n conn.Close()\n }\n\n return nil\n}\n\n// Usage in main\nfunc main() {\n pool := NewPool(1000)\n go pool.Start()\n\n srv := &Server{pool: pool}\n srv.httpSrv = &http.Server{Addr: ":8080"}\n\n // Signal handling\n go func() {\n sigint := make(chan os.Signal, 1)\n signal.Notify(sigint, os.Interrupt, syscall.SIGTERM)\n <-sigint\n ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)\n defer cancel()\n srv.Shutdown(ctx)\n }()\n\n log.Fatal(srv.httpSrv.ListenAndServ