Web Socket #
HTTP bersifat request-response: klien meminta, server menjawab, koneksi selesai. Untuk aplikasi real-time seperti chat, notifikasi live, atau dashboard yang terupdate otomatis, model ini tidak efisien — klien harus terus polling server. WebSocket memecahkan ini dengan mengubah koneksi HTTP menjadi koneksi dua arah yang persisten: setelah handshake awal, server bisa mengirim data kapan saja tanpa klien meminta. Go Standard Library tidak menyediakan implementasi WebSocket yang lengkap, tapi gorilla/websocket adalah library yang sangat matang dan menjadi standar de-facto di ekosistem Go.
Cara Kerja WebSocket #
WebSocket dimulai dengan HTTP request biasa yang meminta “upgrade”:
Klien → Server:
GET /ws HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Server → Klien:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Setelah handshake berhasil (status 101), koneksi TCP yang sama digunakan untuk komunikasi WebSocket — bukan HTTP lagi. Data dikirim dalam frame yang bisa berupa teks atau binary.
Instalasi #
go get github.com/gorilla/websocket
Upgrader — Mengubah HTTP ke WebSocket #
import "github.com/gorilla/websocket"
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
// CheckOrigin mencegah Cross-Site WebSocket Hijacking
// Di development boleh return true, di production periksa origin!
CheckOrigin: func(r *http.Request) bool {
origin := r.Header.Get("Origin")
return origin == "https://myapp.com" || origin == "http://localhost:3000"
},
}
Read dan Write Loop Terpisah #
Ini adalah pola terpenting dalam WebSocket Go. Write ke connection TIDAK thread-safe — jika dua goroutine menulis bersamaan, terjadi panic. Solusinya: satu goroutine khusus untuk write, goroutine lain mengirim pesan via channel:
type Client struct {
conn *websocket.Conn
sendCh chan []byte // antrian pesan yang akan dikirim
}
func wsHandler(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Println("Upgrade error:", err)
return
}
client := &Client{
conn: conn,
sendCh: make(chan []byte, 256),
}
go client.writePump() // goroutine khusus write
client.readPump() // read di goroutine ini
}
// writePump — SATU-SATUNYA goroutine yang boleh menulis ke conn
func (c *Client) writePump() {
ticker := time.NewTicker(54 * time.Second) // untuk ping heartbeat
defer func() {
ticker.Stop()
c.conn.Close()
}()
for {
select {
case msg, ok := <-c.sendCh:
c.conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
if !ok {
// Channel ditutup — kirim close message
c.conn.WriteMessage(websocket.CloseMessage, []byte{})
return
}
if err := c.conn.WriteMessage(websocket.TextMessage, msg); err != nil {
return
}
case <-ticker.C:
// Kirim ping secara periodik untuk deteksi koneksi mati
c.conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil {
return
}
}
}
}
// readPump — membaca pesan dari klien
func (c *Client) readPump() {
defer func() {
close(c.sendCh)
c.conn.Close()
}()
c.conn.SetReadLimit(512 * 1024) // max 512KB per pesan
c.conn.SetReadDeadline(time.Now().Add(60 * time.Second))
// Reset deadline setiap kali ada pong dari klien
c.conn.SetPongHandler(func(string) error {
c.conn.SetReadDeadline(time.Now().Add(60 * time.Second))
return nil
})
for {
_, msg, err := c.conn.ReadMessage()
if err != nil {
if websocket.IsUnexpectedCloseError(err,
websocket.CloseGoingAway,
websocket.CloseAbnormalClosure) {
log.Printf("WebSocket error: %v", err)
}
break
}
log.Printf("Diterima: %s", msg)
c.sendCh <- msg // echo balik
}
}
Jangan pernah menulis ke*websocket.Conndari lebih dari satu goroutine. Ini akan menyebabkan panic dengan pesanconcurrent write to websocket connection. Selalu gunakan satu goroutine writePump yang menerima pesan dari channel.
Hub Pattern — Broadcast ke Banyak Klien #
Untuk aplikasi multi-klien (chat, notifikasi real-time), gunakan Hub yang mengelola semua koneksi dalam satu goroutine:
type Hub struct {
clients map[*Client]bool
register chan *Client
unregister chan *Client
broadcast chan []byte
}
func NewHub() *Hub {
return &Hub{
clients: make(map[*Client]bool),
register: make(chan *Client),
unregister: make(chan *Client),
broadcast: make(chan []byte, 256),
}
}
// Run dijalankan sebagai goroutine tunggal — satu-satunya yang mengakses clients map
// tidak perlu mutex karena hanya satu goroutine yang mengakses map ini
func (h *Hub) Run() {
for {
select {
case c := <-h.register:
h.clients[c] = true
log.Printf("Klien baru. Total: %d", len(h.clients))
case c := <-h.unregister:
if _, ok := h.clients[c]; ok {
delete(h.clients, c)
close(c.sendCh)
}
case msg := <-h.broadcast:
for c := range h.clients {
select {
case c.sendCh <- msg:
default:
// sendCh penuh — klien terlalu lambat, putus koneksi
close(c.sendCh)
delete(h.clients, c)
}
}
}
}
}
ReadJSON dan WriteJSON #
Helper untuk encode/decode JSON tanpa langkah manual:
// Kirim struct sebagai JSON
type Event struct {
Type string `json:"type"`
Payload interface{} `json:"payload"`
Time string `json:"time"`
}
err := conn.WriteJSON(Event{
Type: "notification",
Payload: "Pesanan kamu sudah dikemas!",
Time: time.Now().Format(time.RFC3339),
})
// Terima dan decode JSON
var cmd Event
if err := conn.ReadJSON(&cmd); err != nil {
log.Println("ReadJSON error:", err)
break
}
fmt.Println("Command:", cmd.Type)
WebSocket Client dalam Go #
Untuk testing atau service-to-service via WebSocket:
func connectWS(serverURL string) {
conn, _, err := websocket.DefaultDialer.Dial(serverURL, nil)
if err != nil {
log.Fatal("Dial:", err)
}
defer conn.Close()
// Goroutine untuk menerima pesan dari server
go func() {
for {
_, msg, err := conn.ReadMessage()
if err != nil {
log.Println("Read error:", err)
return
}
log.Printf("Server: %s", msg)
}
}()
// Kirim pesan ke server
for i := 0; i < 5; i++ {
msg := fmt.Sprintf(`{"seq":%d,"text":"Halo!"}`, i)
if err := conn.WriteMessage(websocket.TextMessage, []byte(msg)); err != nil {
log.Println("Write error:", err)
return
}
time.Sleep(time.Second)
}
}
Contoh Program Lengkap — Real-Time Dashboard #
Program berikut membangun dashboard real-time yang mengirim data metrik ke semua klien yang terhubung setiap detik, termasuk halaman HTML yang langsung bisa dibuka di browser:
package main
import (
"encoding/json"
"fmt"
"log"
"math/rand"
"net/http"
"time"
"github.com/gorilla/websocket"
)
type Metrics struct {
Timestamp string `json:"timestamp"`
CPUUsage float64 `json:"cpu_usage"`
MemoryUsage float64 `json:"memory_usage"`
RequestsPS int `json:"requests_per_second"`
ActiveConns int `json:"active_connections"`
}
type WSMessage struct {
Type string `json:"type"`
Payload interface{} `json:"payload"`
}
type Client struct {
conn *websocket.Conn
sendCh chan []byte
hub *Hub
}
type Hub struct {
clients map[*Client]bool
register chan *Client
unregister chan *Client
broadcast chan []byte
}
func NewHub() *Hub {
return &Hub{
clients: make(map[*Client]bool),
register: make(chan *Client),
unregister: make(chan *Client),
broadcast: make(chan []byte, 64),
}
}
func (h *Hub) Run() {
for {
select {
case c := <-h.register:
h.clients[c] = true
log.Printf("[HUB] Klien terhubung. Total: %d", len(h.clients))
welcome, _ := json.Marshal(WSMessage{Type: "welcome",
Payload: fmt.Sprintf("%d klien aktif", len(h.clients))})
c.sendCh <- welcome
case c := <-h.unregister:
if _, ok := h.clients[c]; ok {
delete(h.clients, c)
close(c.sendCh)
log.Printf("[HUB] Klien disconnect. Total: %d", len(h.clients))
}
case msg := <-h.broadcast:
for c := range h.clients {
select {
case c.sendCh <- msg:
default:
close(c.sendCh)
delete(h.clients, c)
}
}
}
}
}
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool { return true },
}
func (c *Client) writePump() {
ticker := time.NewTicker(30 * time.Second)
defer func() { ticker.Stop(); c.conn.Close() }()
for {
select {
case msg, ok := <-c.sendCh:
c.conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
if !ok {
c.conn.WriteMessage(websocket.CloseMessage, []byte{})
return
}
if err := c.conn.WriteMessage(websocket.TextMessage, msg); err != nil {
return
}
case <-ticker.C:
c.conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil {
return
}
}
}
}
func (c *Client) readPump() {
defer func() { c.hub.unregister <- c; c.conn.Close() }()
c.conn.SetReadLimit(4096)
c.conn.SetReadDeadline(time.Now().Add(60 * time.Second))
c.conn.SetPongHandler(func(string) error {
c.conn.SetReadDeadline(time.Now().Add(60 * time.Second))
return nil
})
for {
_, _, err := c.conn.ReadMessage()
if err != nil {
break
}
}
}
func serveWS(hub *Hub, w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
return
}
c := &Client{conn: conn, sendCh: make(chan []byte, 256), hub: hub}
hub.register <- c
go c.writePump()
c.readPump()
}
func generateMetrics(hub *Hub) {
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
cpu, mem, rps := 45.0, 60.0, 150
for range ticker.C {
if len(hub.clients) == 0 {
continue
}
cpu += (rand.Float64() - 0.5) * 5
if cpu < 5 { cpu = 5 } else if cpu > 98 { cpu = 98 }
mem += (rand.Float64() - 0.5) * 3
if mem < 20 { mem = 20 } else if mem > 95 { mem = 95 }
rps += int((rand.Float64() - 0.5) * 30)
if rps < 50 { rps = 50 } else if rps > 1000 { rps = 1000 }
msg, _ := json.Marshal(WSMessage{Type: "metrics", Payload: Metrics{
Timestamp: time.Now().Format("15:04:05"),
CPUUsage: float64(int(cpu*10)) / 10,
MemoryUsage: float64(int(mem*10)) / 10,
RequestsPS: rps,
ActiveConns: len(hub.clients),
}})
hub.broadcast <- msg
}
}
const dashboardHTML = `<!DOCTYPE html><html>
<head><meta charset="UTF-8"><title>Go Dashboard</title>
<style>
body{font-family:monospace;background:#0d1117;color:#c9d1d9;margin:40px}
h1{color:#58a6ff}
.grid{display:flex;gap:16px;flex-wrap:wrap;margin-top:20px}
.card{background:#161b22;border:1px solid #30363d;border-radius:8px;padding:20px;min-width:160px;text-align:center}
.label{color:#8b949e;font-size:12px;margin-bottom:8px}
.value{font-size:32px;font-weight:bold;color:#58a6ff}
#status{padding:6px 12px;border-radius:4px;display:inline-block;font-size:13px}
.ok{background:#1f6feb33;color:#58a6ff}
.err{background:#f8514933;color:#f85149}
#log{margin-top:20px;background:#161b22;border:1px solid #30363d;border-radius:8px;
padding:12px;height:120px;overflow-y:auto;font-size:12px;color:#8b949e}
</style></head>
<body>
<h1>⚡ Go Real-Time Dashboard</h1>
<span id="status" class="err">● Menghubungkan...</span>
<div class="grid">
<div class="card"><div class="label">CPU Usage</div><div class="value" id="cpu">-</div></div>
<div class="card"><div class="label">Memory</div><div class="value" id="mem">-</div></div>
<div class="card"><div class="label">Req/s</div><div class="value" id="rps">-</div></div>
<div class="card"><div class="label">Klien WS</div><div class="value" id="conn">-</div></div>
<div class="card"><div class="label">Waktu</div><div class="value" id="ts" style="font-size:20px">-</div></div>
</div>
<div id="log"></div>
<script>
const ws=new WebSocket('ws://'+location.host+'/ws');
const log=(msg)=>{const el=document.getElementById('log');el.innerHTML+=msg+'<br>';el.scrollTop=el.scrollHeight};
ws.onopen=()=>{document.getElementById('status').className='ok';document.getElementById('status').textContent='● Terhubung';log('✓ WebSocket terhubung')};
ws.onclose=()=>{document.getElementById('status').className='err';document.getElementById('status').textContent='○ Terputus';log('✗ Koneksi terputus')};
ws.onmessage=(e)=>{
const msg=JSON.parse(e.data);
if(msg.type==='welcome'){log('ℹ '+msg.payload);return}
if(msg.type==='metrics'){
const d=msg.payload;
document.getElementById('cpu').textContent=d.cpu_usage+'%';
document.getElementById('mem').textContent=d.memory_usage+'%';
document.getElementById('rps').textContent=d.requests_per_second;
document.getElementById('conn').textContent=d.active_connections;
document.getElementById('ts').textContent=d.timestamp;
}
};
</script></body></html>`
func main() {
hub := NewHub()
go hub.Run()
go generateMetrics(hub)
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
fmt.Fprint(w, dashboardHTML)
})
mux.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
serveWS(hub, w, r)
})
srv := &http.Server{
Addr: ":8080",
Handler: mux,
ReadTimeout: 15 * time.Second,
WriteTimeout: 15 * time.Second,
}
log.Println("Dashboard: http://localhost:8080")
log.Fatal(srv.ListenAndServe())
}
Ringkasan #
- WebSocket mengubah HTTP menjadi koneksi dua arah persisten via handshake upgrade — klien dan server bisa saling mengirim kapan saja.
gorilla/websocketadalah library standar de-facto —go get github.com/gorilla/websocket.- Write TIDAK thread-safe — gunakan satu goroutine
writePumpyang menerima pesan dari channel; jangan pernah write dari lebih dari satu goroutine.CheckOriginharus diimplementasikan dengan benar di production untuk mencegah CSRF via WebSocket.SetReadLimituntuk mencegah klien mengirim pesan raksasa yang menghabiskan memori.- Ping/pong heartbeat — kirim ping dari
writePumpsecara periodik; set read deadline dan reset diPongHandler.- Hub pattern mengelola semua klien di satu goroutine — tidak perlu mutex karena map clients hanya diakses dari satu goroutine.
IsUnexpectedCloseErroruntuk membedakan close yang normal dari error nyata.WriteJSON/ReadJSONsebagai shortcut encode/decode JSON.- sendCh yang penuh adalah sinyal klien lambat — putus koneksi daripada membuat server hang.