Go Gorilla WebSocket
last modified October 30, 2026
In this article we show how to work with WebSockets in Go using the Gorilla WebSocket package. WebSocket is a protocol that provides full-duplex communication over a single TCP connection; it is used in chat applications, games and live dashboards.
The examples use gorilla/websocket v1.5.3 and were run with Go 1.27.0. The clients are Go programs so that every response is reproducible; the last example connects from a browser and was checked with headless Chrome.
The WebSocket server
The server upgrades an HTTP request to a WebSocket connection with
Upgrader.Upgrade and then reads and writes messages in a loop. The
program has five endpoints: /echo returns every message unchanged,
/json decodes and encodes JSON, /limit restricts the
message size, /ping answers ping frames, and /origin
accepts only one origin.
package main
import (
"fmt"
"log"
"net/http"
"time"
"github.com/gorilla/websocket"
)
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
}
type Message struct {
Name string `json:"name"`
}
var strict = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return r.Header.Get("Origin") == "http://localhost:8080"
},
}
func main() {
http.HandleFunc("/echo", func(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Println("upgrade:", err)
return
}
defer conn.Close()
for {
mt, msg, err := conn.ReadMessage()
if err != nil {
return
}
log.Printf("received %d bytes", len(msg))
if err := conn.WriteMessage(mt, msg); err != nil {
return
}
}
})
http.HandleFunc("/json", func(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Println("upgrade:", err)
return
}
defer conn.Close()
var msg Message
if err := conn.ReadJSON(&msg); err != nil {
log.Println("read:", err)
return
}
conn.WriteJSON(Message{Name: "Hello " + msg.Name})
})
http.HandleFunc("/limit", func(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Println("upgrade:", err)
return
}
defer conn.Close()
conn.SetReadLimit(16)
if _, _, err := conn.ReadMessage(); err != nil {
log.Println("read:", err)
conn.WriteMessage(websocket.CloseMessage,
websocket.FormatCloseMessage(websocket.CloseMessageTooBig, "limit"))
}
})
http.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Println("upgrade:", err)
return
}
defer conn.Close()
conn.SetPingHandler(func(data string) error {
err := conn.WriteControl(websocket.PongMessage, []byte(data),
time.Now().Add(time.Second))
if err != nil {
return err
}
return conn.WriteMessage(websocket.TextMessage, []byte("got ping"))
})
for {
if _, _, err := conn.ReadMessage(); err != nil {
return
}
}
})
http.HandleFunc("/origin", func(w http.ResponseWriter, r *http.Request) {
conn, err := strict.Upgrade(w, r, nil)
if err != nil {
log.Println("upgrade:", err)
return
}
defer conn.Close()
conn.WriteMessage(websocket.TextMessage, []byte("origin accepted"))
})
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "index.html")
})
fmt.Println("Listening on 127.0.0.1:8080")
log.Fatal(http.ListenAndServe("127.0.0.1:8080", nil))
}
The package is installed with go get github.com/gorilla/websocket.
Upgrade takes over the underlying connection and returns a
*websocket.Conn. Reading and writing on one connection must not
happen concurrently, except for one reader and one writer.
$ go run main.go Listening on 127.0.0.1:8080
Sending and receiving text messages
The client dials the endpoint with websocket.DefaultDialer.Dial and
sends a text frame with WriteMessage. ReadMessage
returns the message type and the payload of the next data frame.
package main
import (
"fmt"
"log"
"github.com/gorilla/websocket"
)
func main() {
conn, _, err := websocket.DefaultDialer.Dial("ws://127.0.0.1:8080/echo", nil)
if err != nil {
log.Fatal(err)
}
defer conn.Close()
err = conn.WriteMessage(websocket.TextMessage, []byte("hello server"))
if err != nil {
log.Fatal(err)
}
mt, msg, err := conn.ReadMessage()
if err != nil {
log.Fatal(err)
}
fmt.Println("type:", mt)
fmt.Println("body:", string(msg))
}
TextMessage has the value 1 and BinaryMessage has the
value 2, which is what the numeric type shows. The echo server sends the same
frame back, so the payload is unchanged.
$ go run main.go type: 1 body: hello server
Binary messages
Binary frames carry arbitrary bytes, such as images or serialized data. The
only difference on the client is the message type passed to
WriteMessage.
package main
import (
"fmt"
"log"
"github.com/gorilla/websocket"
)
func main() {
conn, _, err := websocket.DefaultDialer.Dial("ws://127.0.0.1:8080/echo", nil)
if err != nil {
log.Fatal(err)
}
defer conn.Close()
data := []byte{0x01, 0x02, 0x03, 0xff}
err = conn.WriteMessage(websocket.BinaryMessage, data)
if err != nil {
log.Fatal(err)
}
mt, msg, err := conn.ReadMessage()
if err != nil {
log.Fatal(err)
}
fmt.Println("type:", mt)
fmt.Println("body:", msg)
}
The bytes are printed as a slice of decimal numbers, and the message type is 2. The server does not interpret the payload, so it can echo any binary content.
$ go run main.go type: 2 body: [1 2 3 255]
JSON messages
WriteJSON and ReadJSON marshal and unmarshal a value
with encoding/json and send it as a text frame. The server decodes
a Message and replies with a greeting.
package main
import (
"fmt"
"log"
"github.com/gorilla/websocket"
)
type Message struct {
Name string `json:"name"`
}
func main() {
conn, _, err := websocket.DefaultDialer.Dial("ws://127.0.0.1:8080/json", nil)
if err != nil {
log.Fatal(err)
}
defer conn.Close()
if err := conn.WriteJSON(Message{Name: "John"}); err != nil {
log.Fatal(err)
}
var reply Message
if err := conn.ReadJSON(&reply); err != nil {
log.Fatal(err)
}
fmt.Println("reply:", reply.Name)
}
The struct tags determine the JSON keys, so the frame contains
{"name":"John"} and the reply contains
{"name":"Hello John"}. The client and the server have to agree on
the message schema.
$ go run main.go reply: Hello John
Limiting the message size
SetReadLimit caps the size of a single message. When a client sends
more, the connection is closed with code 1009 and the read returns a
*websocket.CloseError.
package main
import (
"fmt"
"log"
"github.com/gorilla/websocket"
)
func main() {
conn, _, err := websocket.DefaultDialer.Dial("ws://127.0.0.1:8080/limit", nil)
if err != nil {
log.Fatal(err)
}
defer conn.Close()
msg := []byte("this message is longer than sixteen bytes")
if err := conn.WriteMessage(websocket.TextMessage, msg); err != nil {
log.Fatal(err)
}
_, _, err = conn.ReadMessage()
fmt.Println("read error:", err)
}
The server accepts at most 16 bytes on this endpoint, so the 42-byte message is rejected. The close code 1009 means "message too big"; the text of the close frame is not included in the client's error.
$ go run main.go read error: websocket: close 1009 (message too big)
Ping and pong
Ping and pong are control frames. Gorilla handles them inside
ReadMessage, so a ping does not appear as a normal message. The
server installs a custom PingHandler that sends a pong and a text
reply.
package main
import (
"fmt"
"log"
"github.com/gorilla/websocket"
)
func main() {
conn, _, err := websocket.DefaultDialer.Dial("ws://127.0.0.1:8080/ping", nil)
if err != nil {
log.Fatal(err)
}
defer conn.Close()
if err := conn.WriteMessage(websocket.PingMessage, nil); err != nil {
log.Fatal(err)
}
mt, msg, err := conn.ReadMessage()
if err != nil {
log.Fatal(err)
}
fmt.Println("type:", mt)
fmt.Println("body:", string(msg))
}
The pong frame is consumed by the client's read loop, and the next data frame is the text reply from the server. In a long-lived connection the same mechanism is used with read deadlines to detect a peer that stopped responding.
$ go run main.go type: 1 body: got ping
Checking the origin
Browsers send an Origin header with the handshake, and a server
should reject connections from pages it does not trust. The
CheckOrigin function in the upgrader does this; a rejected
handshake is answered with 403.
package main
import (
"fmt"
"log"
"net/http"
"github.com/gorilla/websocket"
)
func main() {
header := http.Header{}
header.Set("Origin", "http://evil.example")
conn, resp, err := websocket.DefaultDialer.Dial("ws://127.0.0.1:8080/origin", header)
if err != nil {
fmt.Println("status:", resp.Status)
fmt.Println("error:", err)
return
}
defer conn.Close()
_, msg, err := conn.ReadMessage()
if err != nil {
log.Fatal(err)
}
fmt.Println(string(msg))
}
The Dial function returns the HTTP response even when the handshake
fails, which makes the status available. The server accepts only
http://localhost:8080, so this origin is refused.
$ go run main.go status: 403 Forbidden error: websocket: bad handshake
Closing the connection
A client closes a connection with a CloseMessage that carries a
status code and a reason. The peer answers with a close frame, and the next read
returns a *websocket.CloseError with the code.
package main
import (
"fmt"
"log"
"github.com/gorilla/websocket"
)
func main() {
conn, _, err := websocket.DefaultDialer.Dial("ws://127.0.0.1:8080/echo", nil)
if err != nil {
log.Fatal(err)
}
defer conn.Close()
err = conn.WriteMessage(websocket.CloseMessage,
websocket.FormatCloseMessage(websocket.CloseNormalClosure, "bye"))
if err != nil {
log.Fatal(err)
}
_, _, err = conn.ReadMessage()
fmt.Println("read error:", err)
}
CloseNormalClosure is the code 1000. The read returns an error, as
it does after any close, and the program prints it instead of treating it as a
failure.
$ go run main.go read error: websocket: close 1000 (normal)
Connecting from a browser
The server also serves a small page at /. The page opens a
connection to /echo, sends the value of the input field when the
connection is ready, and appends every reply to a pre element. The
template literal uses ${e.data}; without the dollar sign the page
would print the text {e.data} instead of the message.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>WebSocket echo</title>
</head>
<body>
<input id="input" type="text" value="hello from the browser">
<button onclick="send()">Send</button>
<pre id="output"></pre>
<script>
const input = document.getElementById("input");
const output = document.getElementById("output");
const socket = new WebSocket("ws://127.0.0.1:8080/echo");
socket.onopen = () => {
output.textContent += "connected\n";
socket.send(input.value);
};
socket.onmessage = (e) => {
output.textContent += `echo: ${e.data}\n`;
};
function send() {
socket.send(input.value);
input.value = "";
}
</script>
</body>
</html>
The browser connects to the same /echo endpoint as the Go clients.
The text below the form is what the page showed after loading in headless
Chrome.
connected echo: hello from the browser
Source
Gorilla WebSocket - Github page
Gorilla WebSocket - package reference
In this article we have worked with WebSockets in Go: the echo server, text and binary messages, JSON, read limits, ping and pong, origin checks, closing a connection, and a browser client.
Author
List all Go tutorials.