Streaming Server-Sent Events With Go
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!