Streaming Server-Sent Events With Go

Pascal Allen
4 min readJul 19, 2023

--

Photo by Tobias Carlsson on Unsplash

This publication demonstrates how to stream server-sent events over HTTP with Go. For simplicity, I will assume that you have already installed Go on your host machine and already have an understanding of server-sent events. This publication also uses the gin library, which we will detail below.

Prerequisites

Initialize Module

First, create a new directory for your Go app, and cd into it.

mkdir go-app && cd go-app

Now initialize your Go module by running the following command:

go mod init example/user/go-app

Install gin

gin is a popular web framework library on the Go package registry. It’s maintained regularly, has great documentation, and is easy to use. You can view install instructions, as well as documentation, on their GitHub page here. Let’s add the gin library to our project by running the following command at the root of the project:

$ go get -u github.com/gin-gonic/gin

Writing the Go Program

Now that we’ve initialized our Go module and installed gin, we are ready to write our program. Create a file called main.go at the root of your project and add the following code. This will be the entry point to our application.

package main

import (
"example/user/go-app/http"
"github.com/gin-gonic/gin"
"log"
)

func main() {
ch := make(chan string)

router := gin.Default()
router.POST("/event-stream", func(c *gin.Context) {
http.HandleEventStreamPost(c, ch)
})
router.GET("/event-stream", func(c *gin.Context) {
http.HandleEventStreamGet(c, ch)
})

log.Fatalf("error running HTTP server: %s\n", router.Run(":9990"))
}

This code provisions a web server and listens to requests on port 9990. POST requests made to the URI: /event-stream will send a message to the event stream. A GET request made to the URI: /event-stream will open a persistent connection to the event source (until closed) and display sever-sent events in real-time. But not quite yet since we still need to add more code. So let’s keep going.

At the root of the project, let’s create a new directory called http and add the following 2 files:

requests.go

package http

import (
"errors"
"fmt"
"github.com/gin-gonic/gin"
"io"
)

type EventStreamRequest struct {
Message string `form:"message" json:"message" binding:"required,max=100"`
}

func HandleEventStreamPost(c *gin.Context, ch chan string) {
var request EventStreamRequest
if err := c.ShouldBind(&request); err != nil {
errorMessage := fmt.Sprintf("request validation error: %s", err.Error())
BadRequestResponse(c, errors.New(errorMessage))

return
}

ch <- request.Message

CreatedResponse(c, &request.Message)

return
}

func HandleEventStreamGet(c *gin.Context, ch chan string) {
c.Stream(func(w io.Writer) bool {
if msg, ok := <-ch; ok {
c.SSEvent("message", msg)
return true
}
return false
})

return
}

responses.go

package http

import (
"github.com/gin-gonic/gin"
"net/http"
)

type JSendFailResponse[T any] struct {
Status string `json:"status"`
Data T `json:"data"`
}

type JSendSuccessResponse[T any] struct {
Status string `json:"status"`
Data T `json:"data,omitempty"`
}

func BadRequestResponse(c *gin.Context, error error) {
c.JSON(
http.StatusBadRequest,
JSendFailResponse[string]{
Status: "fail",
Data: error.Error(),
},
)

return
}

func CreatedResponse[T interface{}](c *gin.Context, i *T) {
c.JSON(
http.StatusCreated,
JSendSuccessResponse[T]{
Status: "success",
Data: *i,
},
)

return
}

These two files handle the POST and GET requests made to /event-stream. requests.go is responsible for sending and streaming events, and responses.go is responsible for building clean HTTP responses.

Your project tree should look like this:

go-app/
├─ go.mod
├─ http/
│ ├─ requests.go
│ └─ responses.go
└─ main.go

Running Your Program

We are now ready to run our application. To start our application, run the following command from the root of the project:

go run main.go

Opening an Event Stream

For simplicity, let’s use curl to open a persistent connection to our event stream. Run the following command in a new terminal window:

curl http://localhost:9990/event-stream

This command makes a GET request to /event-stream, opening a persistent connection to the event source to receive event messages.

Alternatively, you may use an EventSource instance with JavaScript to achieve the same:

const eventSource = new EventSource('/event-stream');

eventSource.onmessage = event => {
console.log(event.data);
};

Sending Events From the Server

Now that we’ve opened an event stream, we’re able to send events to it using the POST endpoint that we created earlier in this tutorial. So from a new terminal window, run the following command:

curl -d '{"message":"Hello, Event Stream!"}' -H "Content-Type: application/json" -X POST http://localhost:9990/event-stream

This command makes a POST request to /event-stream, which uses the request payload to add a new event message to the event stream. If successful, you should see the following output:

{"status":"success","data":"Hello, Event Stream!"}

And finally, in your event stream terminal window, you should see the following output — indicating that the event was received:

event:message
data:Hello, Event Stream!

Conclusion

Thanks for reading! I hope this was informative. My goal is to share and teach in a fundamentally succinct format. This publication is but one design approach to handling SSEs, there are many additional approaches. Have a great day!

--

--