Server-Sent Events (SSE) is a powerful technology that enables real-time, unidirectional communication from servers to clients.

In this article, we'll explore how to implement SSE in Go, discussing its benefits, use cases, and providing practical examples. By the end, you should know the basics of building real-time applications with efficient, unidirectional communication.

What are Server-Sent Events?

SSE is a web technology that allows servers to push data to clients over a single HTTP connection.

Unlike WebSockets, SSE is unidirectional, making it simpler to implement and ideal for scenarios where real-time updates from the server are required, but client-to-server communication is not necessary.

Developing a web application that uses SSE is straightforward. You'll need a bit of code on the server to stream events to the front-end, but the client side code works almost identically to websockets when it comes to handling incoming events. This is a one-way connection, so you can't send events from a client to a server.

Benefits of SSE

  1. Simplicity: SSE is easier to implement compared to WebSockets.

  2. Native browser support: Most modern browsers support SSE out of the box.

  3. Automatic reconnection: Clients automatically attempt to reconnect if the connection is lost.

  4. Efficient: Uses a single HTTP connection, reducing overhead.

How to Implement SSE in Go

For our example here, we'll create a simple SSE server in Go which just sends the data to the client every second with a current timestamp. The client can then connect to our server on port 8080 and receive these messages.

A real example could be something more sophisticated like sending notifications, displaying progress bar updates, and so on.

package main

import (
    "fmt"
    "net/http"
    "time"
)

func sseHandler(w http.ResponseWriter, r *http.Request) {
    // Set http headers required for SSE
    w.Header().Set("Content-Type", "text/event-stream")
    w.Header().Set("Cache-Control", "no-cache")
    w.Header().Set("Connection", "keep-alive")

    // You may need this locally for CORS requests
    w.Header().Set("Access-Control-Allow-Origin", "*")

    // Create a channel for client disconnection
    clientGone := r.Context().Done()

    rc := http.NewResponseController(w)
    t := time.NewTicker(time.Second)
    defer t.Stop()
    for {
        select {
        case <-clientGone:
            fmt.Println("Client disconnected")
            return
        case <-t.C:
            // Send an event to the client
            // Here we send only the "data" field, but there are few others
            _, err := fmt.Fprintf(w, "data: The time is %s\n\n", time.Now().Format(time.UnixDate))
            if err != nil {
                return
            }
            err = rc.Flush()
            if err != nil {
                return
            }
        }
    }
}

func main() {
    http.HandleFunc("/events", sseHandler)
    fmt.Println("server is running on :8080")
    if err := http.ListenAndServe(":8080", nil); err != nil {
        fmt.Println(err.Error())
    }
}

Key Components of the SSE Implementation

The event stream is a simple stream of text data which must be encoded using UTF-8. Messages in the event stream are separated by a pair of newline characters – \n\n. A colon as the first character of a line is in essence a comment, and is ignored.

In our server it is done here:

rc := http.NewResponseController(w)
fmt.Fprintf(w, "data: The time is %s\n\n", time.Now().Format(time.UnixDate))
// To make sure that the data is sent immediately
rc.Flush()

The server that sends events needs to respond using the MIME type text/event-stream. We do it by setting the response header here:

w.Header().Set("Content-Type", "text/event-stream")

You may have noticed that we set few other headers as well. One is to keep the HTTP connection open, and another to bypass CORS:

w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
w.Header().Set("Access-Control-Allow-Origin", "*")

And the last important piece is to detect the disconnect. In Go, we'll receive it as a message in a specified channel:

clientGone := r.Context().Done()

for {
    select {
    case <-clientGone:
        fmt.Println("Client disconnected")
        return
    }
}

Each message received has some combination of the following fields, one per line. In our server we send only the data field which is enough, as other fields are optional. More details here.

  • event – a string identifying the type of event described.

  • data – the data field for the message.

  • id – the event ID to set the EventSource object's last event ID value.

  • retry – the reconnection time.

How to Receive the Events on the Client Side

On the front end or client side, you will have to use the EventSource interface. It's a browser API encapsulating Server-Sent Events. In the following example, our browser application receives the events from the server and prints them in a list.

<!doctype html>
<html>
    <body>
        <ul id="list"></ul>
    </body>

    <script type="text/javascript">
        const eventSrc = new EventSource("http://127.0.0.1:8080/events");

        const list = document.getElementById("list");

        eventSrc.onmessage = (event) => {
            const li = document.createElement("li");
            li.textContent = `message: ${event.data}`;

            list.appendChild(li);
        };
    </script>
</html>

Here is how it may look in your browser:

logs

Best Practices for SSE in Golang

Event Formatting

In a real world project, a simple string of data may not be enough. In these cases, using a structured format like JSON can be a good option to send multiple data fields once. Here's an example:

{
  "status": "in_progress",
  "completion": 51.22
}

Reconnection Strategy and Error Handling

Something could always go wrong on both sides: the server might reject the connection for some reason or a client might abruptly disconnect.

In each case, you'll need to implement a backoff strategy for graceful reconnections. It's better to miss one message than completely break the event loop.

In JavaScript, you can check for errors in EventSource and then act accordingly:

eventSrc.onerror = (err) => {
  console.error("EventSource failed:", err);
};

Load Balancing

For high-traffic applications, you may consider using a Load Balancer, for example NGINX. If you plan to have many clients connecting to your server, it's good to test it beforehand by simulating the load from the clients.

Use Cases for SSE

  1. Real-time dashboards

  2. Live sports scores

  3. Social media feeds

  4. Stock market tickers

  5. Progress indicators for long-running tasks

Conclusion

Server-Sent Events provide an efficient and straightforward way to implement real-time, server-to-client communication in Golang applications. By leveraging SSE, developers can create responsive and dynamic web applications with minimal overhead and complexity.

As you build your SSE-powered applications, remember to consider scalability, error handling, and client-side implementation to ensure a robust and efficient real-time communication system.

Explore more articles from packagemain.tech