If you’re interested in building a real-time chat application using Go (Golang) and WebSockets, you’re in the right place. In this tutorial, I’ll walk you through the process step by step. We’ll cover setting up the project, implementing the server and client, and testing everything out. Let’s get started!

What are WebSockets? Link to heading

Before we dive in, let’s briefly talk about WebSockets. WebSockets provide a way to open a persistent connection between a client and a server, allowing for real-time, two-way communication. Unlike HTTP, where the client has to constantly poll the server for updates, WebSockets allow the server to send updates to the client as soon as they happen. This makes them perfect for real-time applications like chat apps, live notifications, and games.

1. Setting Up the Project Link to heading

First things first, let’s set up our Go project. Create a new directory for our chat application and initialize a new Go module.

mkdir chat
cd chat
go mod init chat

Next, we need to add the WebSocket package we’ll be using. Run this command to get the package:

go get golang.org/x/net/websocket

Your go.mod file should look like this:

module chat

go 1.22.3

require golang.org/x/net v0.26.0

2. Implementing the Server Link to heading

Now, let’s build our server. The server will handle WebSocket connections, manage clients, and broadcast messages. Create a new directory called server and add two files: server.go and hub.go.

server.go Link to heading

This file will be the main entry point for our server application. Here’s the code:

package main

import (
    "fmt"
    "golang.org/x/net/websocket"
    "log"
    "net/http"
)

func main() {
    h := newHub()
    mux := http.NewServeMux()
    mux.Handle("/", websocket.Handler(func(conn *websocket.Conn) {
        wsHandler(conn, h)
    }))

    server := http.Server{
        Addr:    ":9686",
        Handler: mux,
    }
    err := server.ListenAndServe()
    if err != nil {
        log.Fatalln(err)
    }
}

func wsHandler(conn *websocket.Conn, h *hub) {
    go h.run()

    h.addClientChn <- conn

    for {
        var m Message
        err := websocket.JSON.Receive(conn, &m)
        if err != nil {
            h.removeClientChn <- conn
            fmt.Println("error in Receive Message: ", err.Error())
            continue
        }

        h.broadcastChn <- m
    }
}

hub.go Link to heading

This file will handle managing clients and broadcasting messages. Here’s what it looks like:

package main

import (
    "golang.org/x/net/websocket"
)

type Message struct {
    Text string `json:"text"`
}

type hub struct {
    clients         map[string]*websocket.Conn
    addClientChn    chan *websocket.Conn
    removeClientChn chan *websocket.Conn
    broadcastChn    chan Message
}

func newHub() *hub {
    return &hub{
        clients:         make(map[string]*websocket.Conn),
        addClientChn:    make(chan *websocket.Conn),
        removeClientChn: make(chan *websocket.Conn),
        broadcastChn:    make(chan Message),
    }
}

func (h *hub) run() {
    for {
        select {
        case conn := <-h.addClientChn:
            h.addClient(conn)
        case conn := <-h.removeClientChn:
            h.removeClient(conn)
        case m := <-h.broadcastChn:
            h.broadcast(m)
        }
    }
}

func (h *hub) addClient(conn *websocket.Conn) {
    h.clients[conn.RemoteAddr().String()] = conn
    fmt.Println("Clients, ", h.clients)
}

func (h *hub) removeClient(conn *websocket.Conn) {
    delete(h.clients, conn.RemoteAddr().String())
}

func (h *hub) broadcast(m Message) {
    for _, conn := range h.clients {
        err := websocket.JSON.Send(conn, m)
        if err != nil {
            fmt.Println("error in Broadcast Message: ", err.Error())
            continue
        }
    }
}

In the addClient function, we are using the client’s IP address as a unique identifier. This is why in client.go, we create random IP addresses for clients using the CreateDemoIp function. In a real-world application, you would typically use a user ID or username from your database instead of an IP address.

Why Use a Hub and Channels? Link to heading

You might be wondering why we need a hub and channels. The hub acts as the central point for managing all WebSocket connections. It keeps track of clients and handles broadcasting messages to everyone. Using channels (addClientChn, removeClientChn, and broadcastChn) ensures that our application can handle multiple clients concurrently without any race conditions or other issues.

Why Use an HTTP Mux and wsHandler? Link to heading

We use http.NewServeMux to create an HTTP request multiplexer, which allows us to route incoming HTTP requests to different handlers. In our case, we route all requests to the WebSocket handler. The wsHandler function handles WebSocket connections, registers them with the hub, and listens for incoming messages.

3. Implementing the Client Link to heading

Next, let’s build a simple client that connects to our server, sends messages, and receives broadcast messages. Create a new directory called client and add a client.go file.

client.go Link to heading

Here’s the code for our client application:

package main

import (
    "bufio"
    "fmt"
    "golang.org/x/net/websocket"
    "log"
    "math/rand"
    "os"
    "time"
)

type Message struct {
    Text string `json:"text"`
}

func main() {
    conn, err := websocket.Dial("ws://localhost:9686", "", CreateDemoIp())
    if err != nil {
        log.Fatalln(err)
        return
    }
    defer conn.Close()

    go receive(conn)

    send(conn)
}

func CreateDemoIp() string {
    var arry [4]int
    for i := 0; i < len(arry); i++ {
        rand.Seed(time.Now().UnixNano())
        arry[i] = rand.Intn(256)
    }
    return fmt.Sprintf("http://%d.%d.%d.%d", arry[0], arry[1], arry[2], arry[3])
}

func receive(conn *websocket.Conn) {
    for {
        var m Message
        err := websocket.JSON.Receive(conn, &m)
        if err != nil {
            log.Fatalln("Error in Receive Data: ", err)
            continue
        }
        fmt.Println("Message from Server: ", m.Text)
    }
}

func send(conn *websocket.Conn) {
    scanner := bufio.NewScanner(os.Stdin)

    for scanner.Scan() {
        text := scanner.Text()
        m := Message{
            Text: text,
        }
        err := websocket.JSON.Send(conn, m)
        if err != nil {
            fmt.Println("Error in Send Data: ", err)
            continue
        }
    }
}

4. Running the Application Link to heading

Now it’s time to run our chat application. We’ll start the server and then run the client to connect and chat.

Running the Server Link to heading

Navigate to the server directory and run:

go run .

The server will start listening on port 9686.

Running the Client Link to heading

Open a new terminal window, navigate to the client directory, and run:

go run client.go

You can open multiple terminal windows and run the client in each one to create multiple clients for testing.

5. Testing the Application Link to heading

To test our chat application:

  1. Start the server by running go run . in the server directory.
  2. Open multiple terminal windows.
  3. In each terminal window, navigate to the client directory and run go run client.go.
  4. Type messages in each client terminal. You should see the messages broadcasted to all connected clients.

Conclusion Link to heading

And there you have it! We’ve built a simple chat application using Go and WebSockets. We set up the project, implemented the server and client, and tested it all out. This should give you a solid foundation to build more complex real-time applications.

Feel free to explore and expand on this example. You could add features like user authentication, message persistence, or even a web-based client. Happy coding!