| Tested on | RHEL 10.2 with Go 1.27.0 |
|---|---|
| Package | go 1.27.0github.com/gorilla/websocket v1.5.3 |
| Applies to | Any host with Go 1.20+ installed |
| Privilege | Normal user |
| Scope | Runnable Gorilla WebSocket echo server and Go client, text and JSON messages, close frames, ping/pong basics, and common errors. Does not cover chat hubs, Redis pub/sub, or reverse-proxy TLS setup. |
| Related guides | Golang HTTP server Golang TCP server and client Golang context Getting started with Go Golang Gin |
WebSocket keeps a connection open so a client and server can exchange messages without sending a new HTTP request for every update. That pattern fits chat, live dashboards, notifications, collaborative editing, and streaming server-pushed data. This tutorial builds a golang WebSocket server and a matching golang WebSocket client with Gorilla WebSocket (github.com/gorilla/websocket v1.5.3): connect, send a text message, echo it back, then cover JSON, closing, ping/pong, and the mistakes that show up next.
Gorilla WebSocket remains mature and widely used (pkg.go.dev lists tens of thousands of known importers; v1.5.3 is the current stable release). github.com/coder/websocket is another actively maintained library with a more modern, context-oriented API. This guide uses Gorilla for all examples.
Install Gorilla WebSocket
Create a working directory for the examples:
mkdir websocket-example && cd websocket-exampleRegister a Go module for this folder:
go mod init websocket-examplego: creating new go.mod: module websocket-examplePull Gorilla WebSocket at the current stable release:
go get github.com/gorilla/websocket@v1.5.3go: added github.com/gorilla/websocket v1.5.3Gorilla provides the pieces you need for a golang WebSocket example:
- HTTP → WebSocket upgrade on the server (
websocket.Upgrader) - client dialing (
websocket.DefaultDialer) - reading and writing application messages (
ReadMessage,WriteMessage,ReadJSON,WriteJSON) - control messages such as close, ping, and pong
Create a WebSocket server in Go
The server listens on HTTP, upgrades /ws to WebSocket, reads one message, and echoes it back.
Save this as server.go:
package main
import (
"log"
"net/http"
"github.com/gorilla/websocket"
)
var upgrader = websocket.Upgrader{}
func handleWebSocket(w http.ResponseWriter, req *http.Request) {
conn, err := upgrader.Upgrade(w, req, nil)
if err != nil {
log.Println("upgrade:", err)
return
}
defer conn.Close()
conn.SetReadLimit(1 << 20) // 1 MiB max incoming message
log.Println("client connected")
for {
messageType, message, err := conn.ReadMessage()
if err != nil {
if websocket.IsUnexpectedCloseError(err, websocket.CloseNormalClosure, websocket.CloseGoingAway) {
log.Println("read:", err)
}
return
}
log.Printf("received: %s", message)
if err := conn.WriteMessage(messageType, message); err != nil {
log.Println("write:", err)
return
}
}
}
func main() {
log.SetFlags(0)
http.HandleFunc("/ws", handleWebSocket)
log.Println("WebSocket server listening on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}Run the WebSocket server
Leave this process running in Terminal 1:
go run server.goWebSocket server listening on :8080The server blocks until a client connects; you run the client in a second terminal next.
How the WebSocket handler works
The flow for a websocket server golang handler is:
HTTP request to /ws
↓
upgrader.Upgrade(w, req, nil)
↓
WebSocket connection (conn)
↓
conn.ReadMessage()
↓
conn.WriteMessage()The first request arrives as normal HTTP. Upgrade() completes the WebSocket handshake; after that, conn speaks the WebSocket protocol—not http.ResponseWriter body writes for application data.
Inside handleWebSocket:
var upgrader = websocket.Upgrader{}is enough for this echo server—zero buffer sizes let Gorilla use buffers from the HTTP server.upgrader.Upgrade(w, req, nil)promotes the HTTP connection to WebSocket. Usewebsocket.Upgrader(not the deprecated package-levelwebsocket.Upgrade).defer conn.Close()releases the connection when the handler returns.ReadMessage()returns the message type (TextMessage,BinaryMessage, etc.) and payload bytes.WriteMessage(messageType, message)echoes the same type and payload.
conn.SetReadLimit(1 << 20) limits each incoming message to 1 MiB.
A read error normally ends the loop. websocket.IsUnexpectedCloseError logs only abnormal disconnects—not a peer that sent a normal close frame.
Create a WebSocket client in Go
The golang WebSocket client dials the server, sends one text message, and prints the echo.
Save this as client.go:
package main
import (
"log"
"github.com/gorilla/websocket"
)
func main() {
log.SetFlags(0)
url := "ws://localhost:8080/ws"
conn, _, err := websocket.DefaultDialer.Dial(url, nil)
if err != nil {
log.Fatal("dial:", err)
}
defer conn.Close()
log.Printf("connected to %s", url)
message := []byte("Hello from client")
if err := conn.WriteMessage(websocket.TextMessage, message); err != nil {
log.Fatal("write:", err)
}
log.Printf("sent: %s", message)
_, reply, err := conn.ReadMessage()
if err != nil {
log.Fatal("read:", err)
}
log.Printf("received: %s", reply)
if err := conn.WriteMessage(
websocket.CloseMessage,
websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""),
); err != nil {
log.Fatal("close:", err)
}
}websocket.DefaultDialer.Dial() (or DialContext when you need cancellation) is the standard client entry point. The URL uses the ws:// scheme for local, non-TLS connections.
Run the client and test the connection
With the server still running in Terminal 1, open Terminal 2 in the same directory:
go run client.goconnected to ws://localhost:8080/ws
sent: Hello from client
received: Hello from clientBack in Terminal 1, the server prints client connected and received: Hello from client with no error line—the client sends a normal close frame before exiting.
log.SetFlags(0) at the start of each main() strips the default date/time prefix from log output so the transcripts above match what you see when you copy the examples.
That end-to-end path—go run server.go plus go run client.go—is enough to verify your go WebSocket example.
Send text, binary, and JSON messages
ReadMessage and WriteMessage use a message type constant:
| Message type | Use |
|---|---|
websocket.TextMessage |
UTF-8 text |
websocket.BinaryMessage |
Raw binary data |
websocket.CloseMessage |
Close control frame |
websocket.PingMessage |
Ping control frame |
websocket.PongMessage |
Pong response |
You do not need separate programs for each type—pick TextMessage or BinaryMessage for application data.
Send and receive JSON
For APIs, JSON is the usual payload. Define a struct and use the connection methods WriteJSON and ReadJSON (not the deprecated package-level websocket.WriteJSON / websocket.ReadJSON):
type Message struct {
User string `json:"user"`
Text string `json:"text"`
}
msg := Message{User: "alice", Text: "hello"}
if err := conn.WriteJSON(msg); err != nil {
log.Println("write json:", err)
}
var incoming Message
if err := conn.ReadJSON(&incoming); err != nil {
log.Println("read json:", err)
}WriteJSON sends a TextMessage frame with marshaled JSON. ReadJSON unmarshals the next JSON text message into your struct.
Close and keep WebSocket connections alive
Close the connection
Always release connections. defer conn.Close() closes the underlying network connection when the function exits.
To initiate a graceful WebSocket close, send a close control frame before closing the underlying connection:
if err := conn.WriteMessage(
websocket.CloseMessage,
websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""),
); err != nil {
log.Println("close:", err)
}A complete close handshake requires handling or waiting for the peer's corresponding close frame. Gorilla processes received close frames through its close handler and read methods.
Common status codes:
| Code | Meaning |
|---|---|
1000 |
Normal closure |
1001 |
Endpoint going away |
1006 |
Abnormal closure reported locally; not sent as a close frame |
Use websocket.IsUnexpectedCloseError(err, websocket.CloseNormalClosure, websocket.CloseGoingAway) to separate expected disconnects from failures.
Ping and pong
Long-lived connections can die without a clean close frame. Deadlines plus ping/pong help detect silent peers. The following fragment assumes time and log are already imported. Periodically send a ping with WriteControl:
conn.SetReadDeadline(time.Now().Add(60 * time.Second))
conn.SetPongHandler(func(string) error {
conn.SetReadDeadline(time.Now().Add(60 * time.Second))
return nil
})
if err := conn.WriteControl(
websocket.PingMessage,
nil,
time.Now().Add(10*time.Second),
); err != nil {
log.Println("ping:", err)
}Gorilla allows only one concurrent caller of its regular write methods such as WriteMessage and WriteJSON. WriteControl is an exception and may be called concurrently with other methods.
Gorilla processes ping, pong, and close control frames while the application reads from the connection. A connection that only writes still needs a read loop (or another reader) so control frames are handled.
ws:// versus wss://
| Scheme | Meaning |
|---|---|
ws:// |
WebSocket without TLS |
wss:// |
WebSocket over TLS |
Local examples use ws://localhost:8080/ws. A page loaded over HTTPS normally needs a wss:// endpoint in production. This article does not walk through TLS certificate setup.
Common Gorilla WebSocket errors and mistakes
| Problem | Cause / fix |
|---|---|
websocket: bad handshake |
The endpoint did not complete a valid WebSocket upgrade—check URL, route, and that the server returned 101 Switching Protocols |
| HTTP 403 during upgrade | CheckOrigin rejected the browser Origin header |
concurrent write to websocket connection |
More than one goroutine called WriteMessage / WriteJSON on the same connection |
| Connection closes unexpectedly | Handle read errors and close frames; use ping/pong and read deadlines on long-lived sockets |
ws:// works locally but fails in production |
HTTPS pages require wss:// for secure WebSocket |
| Client connects but receives nothing | Keep reading the connection—WebSocket messages are not HTTP responses |
| Browser cannot connect but Go client works | Check Origin handling and the WebSocket URL path |
| Large messages consume too much memory | Set conn.SetReadLimit(n) when message size must be bounded |
Concurrency: Gorilla documents one concurrent reader and one concurrent writer per connection for regular read/write methods. Only one goroutine may call WriteMessage or WriteJSON at a time; WriteControl and Close may run concurrently with other methods. Broadcasting or chat rooms still need a single writer goroutine (or a mutex) for application messages—not demonstrated here, but that rule is why concurrent write appears in production logs.
Origin handling: When CheckOrigin is nil, Gorilla rejects browser requests whose Origin host differs from the request Host. That is safer than blindly returning true in every tutorial. Set a custom CheckOrigin only when you intentionally allow cross-origin browser clients:
upgrader := websocket.Upgrader{
CheckOrigin: func(req *http.Request) bool {
return req.Header.Get("Origin") == "https://app.example.com"
},
}Summary
You now have a minimal golang WebSocket server on /ws and a golang WebSocket client that dial, send, and receive text over Gorilla WebSocket. The important shift from plain HTTP is the upgrade handshake: after Upgrade(), the same TCP connection carries WebSocket frames instead of request/response cycles, which is why persistent chat and live updates fit this model.
The pitfalls that appear right after a working echo are predictable: cross-origin browser upgrades, concurrent writes from multiple goroutines, and connections that stop responding without a close frame. Closing with CloseMessage, using read deadlines, and ping/pong address lifecycle issues; CheckOrigin and wss:// matter when you move from go run on localhost to browsers on HTTPS.
Use WebSockets when the client and server need ongoing bidirectional communication or server-pushed updates over one persistent channel—not when occasional REST calls are enough. For HTTP basics that underpin the upgrade handshake, see Golang HTTP server; for raw byte streams without WebSocket framing, see Golang TCP server and client.
References
- Gorilla WebSocket repository
- pkg.go.dev/github.com/gorilla/websocket
- RFC 6455 — The WebSocket Protocol
- pkg.go.dev/net/http
- pkg.go.dev/github.com/coder/websocket

