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.Conn dari lebih dari satu goroutine. Ini akan menyebabkan panic dengan pesan concurrent 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/websocket adalah library standar de-facto — go get github.com/gorilla/websocket.
  • Write TIDAK thread-safe — gunakan satu goroutine writePump yang menerima pesan dari channel; jangan pernah write dari lebih dari satu goroutine.
  • CheckOrigin harus diimplementasikan dengan benar di production untuk mencegah CSRF via WebSocket.
  • SetReadLimit untuk mencegah klien mengirim pesan raksasa yang menghabiskan memori.
  • Ping/pong heartbeat — kirim ping dari writePump secara periodik; set read deadline dan reset di PongHandler.
  • Hub pattern mengelola semua klien di satu goroutine — tidak perlu mutex karena map clients hanya diakses dari satu goroutine.
  • IsUnexpectedCloseError untuk membedakan close yang normal dari error nyata.
  • WriteJSON / ReadJSON sebagai shortcut encode/decode JSON.
  • sendCh yang penuh adalah sinyal klien lambat — putus koneksi daripada membuat server hang.

← Sebelumnya: Socket   Berikutnya: Web Server →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact