Go: Server-Sent Events(SSE)

This article demonstrates Server-Sent Events(SSE) implementation in Golang. Follow the steps below to implement Server-Sent Events (SSE) in Go.

More details about SSE including all headers and options are discussed in a separate article, check that to know the subject in detail (Server-Sent Events (SSE)).
JS web client and EventSource options and complete usage are discussed in a separate article in detail (JS EventSource SSE Web Client).

Implement Server

Create a new file for the Go code.

Here we are using a file named main.go.

Step #1: Initialize and Import Packages

// main.go

package main

import (
	"encoding/json"
	"fmt"
	"log"
	"net/http"
)

Package notes:

  • net/http: required for HTTP server (request and response handling)
  • encoding/json: required for decoding JSON sent in HTTP request
  • fmt: not required for the server implementation or SSE specifically, here it is used for logging some info.
  • log: not required for the server implementation or SSE specifically, here it is used for logging purpose.

Step #2: Setup HTTP Routers

Define the routers for 2 endpoints:

  1. /events: for subscription
  2. /send-event: for sending data
func main() {
	router := http.NewServeMux()

	router.HandleFunc("/events", sseEventsHandler)
	router.HandleFunc("/send-event", sseSendEvent)

	if err := http.ListenAndServe(":3000", router); err != nil {
		log.Fatal(err)
	}
}

Step #3: Define a Channel (for sending messages)

We need to use a Go channel for sending and receiving messages, so define a channel named sseChannle globally.

var sseChannel chan string

Step #4: Define the Request Struct

To receive the message, the fields of the message need to be defined on the backend. Use Go Structure to define the fields. For this example, we will be sending only one field named “message”, so we will define the structure like below:

type msgReqStruct struct {
	Message string
}

Step #5: Implement SSE Subscription Endpoint

For the endpoint /events where the client will subscribe and receive the message, we will define it like below.

func sseEventsHandler(resWritter http.ResponseWriter, req *http.Request) {
	fmt.Println("New connection established")

	resWritter.Header().Set("Access-Control-Allow-Origin", "*")
	resWritter.Header().Set("Content-Type", "text/event-stream")
	resWritter.Header().Set("Cache-Control", "no-cache")
	resWritter.Header().Set("Connection", "keep-alive")

	sseChannel = make(chan string)

	defer func() {
		close(sseChannel)
		sseChannel = nil
		fmt.Println("Closing connection")
	}()

	flusher, ok := resWritter.(http.Flusher)

	if !ok {
		fmt.Println("Unable to initialize flusher")
	}

	for {
		select {
		case msg := <- sseChannel:
			fmt.Fprintf(resWritter, "data: %s\n\n", msg)
			flusher.Flush()

		case <- req.Context().Done():
			fmt.Println("Connection closed")
			return
		}
	}
}

Here are the key points of this implementation:

  • 3 headers are set in the response, which is required for an SSE endpoint. Much more details about these headers are available here: Server-Sent Events(SSE) Details.
    • “Content-Type” set to “text/event-stream”
    • “Cache-Control” set to “no-cache”
    • “Connection” set to “keep-alive”
  • When a message is received through sseChannel, then send the data like “data: some message\n\n” (the space after the colon and 2 newline characters at the end are required for SSE).
  • Flush after sending the message to ensure that the message is sent immediately and does not wait in the buffer pool.

Step #6: Implement Endpoint for Sending Data

Here is the implementation for the data receiver endpoint /send-event.

func sseSendEvent(resWritter http.ResponseWriter, req *http.Request) {
	var msgReq msgReqStruct

	if sseChannel == nil {
		panic("Channel not initialized")
	}

	if req.Method != http.MethodPost {
		panic("Method does not match")
	}

	err := json.NewDecoder(req.Body).Decode(&msgReq)

	if err != nil {
		panic("Error parsing request JSON")
	}

	sseChannel <- msgReq.Message
}

Here are the key points of this implementation:

  • Check if the channel sseChannel is already initialized and being used by any client or not. If not then it means there is no subscriber yet, so nothing needs to be done in that case.
  • Receive the data from the POST request body. we will be sending JSON, so parse JSON data.
  • Send it to channel sseChannel when the data is received.

Full Source Code for Go Backend

Here is the full code for the backend implementation.

// main.go

package main

import (
	"encoding/json"
	"fmt"
	"log"
	"net/http"
)

type msgReqStruct struct {
	Message string
}

var sseChannel chan string

func main() {
	router := http.NewServeMux()

	router.HandleFunc("/events", sseEventsHandler)
	router.HandleFunc("/send-event", sseSendEvent)

	if err := http.ListenAndServe(":3000", router); err != nil {
		log.Fatal(err)
	}
}

func sseEventsHandler(resWritter http.ResponseWriter, req *http.Request) {
	fmt.Println("New connection established")

	resWritter.Header().Set("Access-Control-Allow-Origin", "*")
	resWritter.Header().Set("Content-Type", "text/event-stream")
	resWritter.Header().Set("Cache-Control", "no-cache")
	resWritter.Header().Set("Connection", "keep-alive")

	sseChannel = make(chan string)

	defer func() {
		close(sseChannel)
		sseChannel = nil
		fmt.Println("Closing connection")
	}()

	flusher, ok := resWritter.(http.Flusher)

	if !ok {
		fmt.Println("Unable to initialize flusher")
	}

	for {
		select {
		case msg := <- sseChannel:
			fmt.Fprintf(resWritter, "data: %s\n\n", msg)
			flusher.Flush()

		case <- req.Context().Done():
			fmt.Println("Connection closed")
			return
		}
	}
}

func sseSendEvent(resWritter http.ResponseWriter, req *http.Request) {
	var msgReq msgReqStruct

	if sseChannel == nil {
		panic("Channel not initialized")
	}

	if req.Method != http.MethodPost {
		panic("Method does not match")
	}

	err := json.NewDecoder(req.Body).Decode(&msgReq)

	if err != nil {
		panic("Error parsing request JSON")
	}

	sseChannel <- msgReq.Message
}

Run Go Backend

Go to the directory where you have your go file. Use the following command to run the backend.

go run main.go

After running the command, the backend server will run on port 3000 and the following endpoints will be available:

  1. /events: used by the client for subscribing and receiving the events.
  2. /send-event: used by the sender to send events or messages.

Implement Client

Now create an HTML file to test, past the following lines in the HTML file:

<html>
  <head>
    <title>Go SSE test</title>
  </head>

  <body>
    Check console for the result
  </body>

  <script>
    const events = new EventSource('http://localhost:3000/events');

    events.onmessage = (event) => {
      console.log(event.data);
    };
  </script>
</html>

Open the HTML file in the browser, and check the console (in developer tools).

Check complete details about EventSource in the article: Web (JS EventSource) Client.

Send Request

Open postman or any other client to send the POST request to http://localhost:3000/send-event for a new message/event. Send any JSON data using the client.

{
    "message":"test message 1"
}

Check Result

Check your browser console tab. The message should be there

Or you can check the Network tab and check the data there. Check the EventStream tab there for the request.

Source Code

All source codes are available in the git repository, follow the link below:

Leave a Comment


The reCAPTCHA verification period has expired. Please reload the page.

Uncover the professional journey shaping your projects. Explore my profile on the About page.